Add v5 episode, missing and cutoff unmet endpoints

This commit is contained in:
Mark McDowall 2025-11-13 20:06:28 -08:00
parent 317cdf1558
commit 5a702dec12
4 changed files with 194 additions and 0 deletions

View file

@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Tv;
using NzbDrone.SignalR;
using Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V5.Episodes;
[V5ApiController]
public class EpisodeController : EpisodeControllerWithSignalR
{
public EpisodeController(ISeriesService seriesService,
IEpisodeService episodeService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IBroadcastSignalRMessage signalRBroadcaster)
: base(episodeService, seriesService, upgradableSpecification, formatCalculator, signalRBroadcaster)
{
}
[HttpGet]
[Produces("application/json")]
public List<EpisodeResource> GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List<int> episodeIds, int? episodeFileId, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false)
{
if (seriesId.HasValue)
{
if (seasonNumber.HasValue)
{
return MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), includeSeries, includeEpisodeFile, includeImages);
}
return MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), includeSeries, includeEpisodeFile, includeImages);
}
else if (episodeIds.Any())
{
return MapToResource(_episodeService.GetEpisodes(episodeIds), includeSeries, includeEpisodeFile, includeImages);
}
else if (episodeFileId.HasValue)
{
return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), includeSeries, includeEpisodeFile, includeImages);
}
throw new BadRequestException("seriesId or episodeIds must be provided");
}
[RestPutById]
[Consumes("application/json")]
public ActionResult<EpisodeResource> SetEpisodeMonitored([FromRoute] int id, [FromBody] EpisodeResource resource)
{
_episodeService.SetEpisodeMonitored(id, resource.Monitored);
resource = MapToResource(_episodeService.GetEpisode(id), false, false, false);
return Accepted(resource);
}
[HttpPut("monitor")]
[Consumes("application/json")]
public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource, [FromQuery] bool includeImages = false)
{
if (resource.EpisodeIds.Count == 1)
{
_episodeService.SetEpisodeMonitored(resource.EpisodeIds.First(), resource.Monitored);
}
else
{
_episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored);
}
var resources = MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages);
return Accepted(resources);
}
}

View file

@ -0,0 +1,7 @@
namespace Sonarr.Api.V5.Episodes;
public class EpisodesMonitoredResource
{
public required List<int> EpisodeIds { get; set; }
public bool Monitored { get; set; }
}

View file

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Tv;
using NzbDrone.SignalR;
using Sonarr.Api.V5.Episodes;
using Sonarr.Http;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V5.Wanted;
[V5ApiController("wanted/cutoff")]
public class CutoffController : EpisodeControllerWithSignalR
{
private readonly IEpisodeCutoffService _episodeCutoffService;
public CutoffController(IEpisodeCutoffService episodeCutoffService,
IEpisodeService episodeService,
ISeriesService seriesService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IBroadcastSignalRMessage signalRBroadcaster)
: base(episodeService, seriesService, upgradableSpecification, formatCalculator, signalRBroadcaster)
{
_episodeCutoffService = episodeCutoffService;
}
[HttpGet]
[Produces("application/json")]
public PagingResource<EpisodeResource> GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false, bool monitored = true)
{
var pagingResource = new PagingResource<EpisodeResource>(paging);
var pagingSpec = pagingResource.MapToPagingSpec<EpisodeResource, Episode>(
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"episodes.airDateUtc",
"episodes.lastSearchTime",
"series.sortTitle"
},
"episodes.airDateUtc",
SortDirection.Ascending);
if (monitored)
{
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true);
}
else
{
pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false);
}
var resource = pagingSpec.ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages));
return resource;
}
}

View file

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Tv;
using NzbDrone.SignalR;
using Sonarr.Api.V5.Episodes;
using Sonarr.Http;
using Sonarr.Http.Extensions;
namespace Sonarr.Api.V5.Wanted;
[V5ApiController("wanted/missing")]
public class MissingController : EpisodeControllerWithSignalR
{
public MissingController(IEpisodeService episodeService,
ISeriesService seriesService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IBroadcastSignalRMessage signalRBroadcaster)
: base(episodeService, seriesService, upgradableSpecification, formatCalculator, signalRBroadcaster)
{
}
[HttpGet]
[Produces("application/json")]
public PagingResource<EpisodeResource> GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeImages = false, bool monitored = true)
{
var pagingResource = new PagingResource<EpisodeResource>(paging);
var pagingSpec = pagingResource.MapToPagingSpec<EpisodeResource, Episode>(
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"episodes.airDateUtc",
"episodes.lastSearchTime",
"series.sortTitle"
},
"episodes.airDateUtc",
SortDirection.Ascending);
if (monitored)
{
pagingSpec.FilterExpressions.Add(v => v.Monitored == true && v.Series.Monitored == true);
}
else
{
pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false);
}
var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages));
return resource;
}
}