From dbb841f027f5b2b8ec42b0291fd4631d9b0366ee Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 23 Dec 2025 12:06:04 -0800 Subject: [PATCH] Use 'includeSubResources' to include sub resources in responses --- .../Calendar/CalendarController.cs | 6 ++++- .../Calendar/CalendarSubresource.cs | 8 +++++++ .../Episodes/EpisodeController.cs | 10 ++++++-- .../Episodes/EpisodeSubresource.cs | 8 +++++++ .../History/HistoryController.cs | 22 +++++++++++++---- .../History/HistorySubresource.cs | 7 ++++++ src/Sonarr.Api.V5/Queue/QueueController.cs | 5 +++- .../Queue/QueueDetailsController.cs | 4 +++- src/Sonarr.Api.V5/Queue/QueueSubresource.cs | 7 ++++++ src/Sonarr.Api.V5/Series/SeriesController.cs | 24 +++++++++++++------ src/Sonarr.Api.V5/Series/SeriesSubresource.cs | 6 +++++ src/Sonarr.Api.V5/Wanted/CutoffController.cs | 6 ++++- src/Sonarr.Api.V5/Wanted/CutoffSubresource.cs | 8 +++++++ src/Sonarr.Api.V5/Wanted/MissingController.cs | 5 +++- .../Wanted/MissingSubresource.cs | 7 ++++++ 15 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 src/Sonarr.Api.V5/Calendar/CalendarSubresource.cs create mode 100644 src/Sonarr.Api.V5/Episodes/EpisodeSubresource.cs create mode 100644 src/Sonarr.Api.V5/History/HistorySubresource.cs create mode 100644 src/Sonarr.Api.V5/Queue/QueueSubresource.cs create mode 100644 src/Sonarr.Api.V5/Series/SeriesSubresource.cs create mode 100644 src/Sonarr.Api.V5/Wanted/CutoffSubresource.cs create mode 100644 src/Sonarr.Api.V5/Wanted/MissingSubresource.cs diff --git a/src/Sonarr.Api.V5/Calendar/CalendarController.cs b/src/Sonarr.Api.V5/Calendar/CalendarController.cs index 9bced6fa3..3eb37d4da 100644 --- a/src/Sonarr.Api.V5/Calendar/CalendarController.cs +++ b/src/Sonarr.Api.V5/Calendar/CalendarController.cs @@ -28,7 +28,7 @@ public CalendarController(IBroadcastSignalRMessage signalR, [HttpGet] [Produces("application/json")] - public List GetCalendar(DateTime? start, DateTime? end, bool includeUnmonitored = false, bool includeSpecials = true, string tags = "", bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false) + public List GetCalendar(DateTime? start, DateTime? end, bool includeUnmonitored = false, bool includeSpecials = true, string tags = "", [FromQuery] CalendarSubresource[]? includeSubresources = null) { var startUse = start ?? DateTime.Today; var endUse = end ?? DateTime.Today.AddDays(2); @@ -59,6 +59,10 @@ public List GetCalendar(DateTime? start, DateTime? end, bool in result.Add(episode); } + var includeSeries = includeSubresources.Contains(CalendarSubresource.Series); + var includeEpisodeFile = includeSubresources.Contains(CalendarSubresource.EpisodeFile); + var includeEpisodeImages = includeSubresources.Contains(CalendarSubresource.Images); + var resources = MapToResource(result, includeSeries, includeEpisodeFile, includeEpisodeImages); return resources.OrderBy(e => e.AirDateUtc).ToList(); diff --git a/src/Sonarr.Api.V5/Calendar/CalendarSubresource.cs b/src/Sonarr.Api.V5/Calendar/CalendarSubresource.cs new file mode 100644 index 000000000..e5a682d72 --- /dev/null +++ b/src/Sonarr.Api.V5/Calendar/CalendarSubresource.cs @@ -0,0 +1,8 @@ +namespace Sonarr.Api.V5.Calendar; + +public enum CalendarSubresource +{ + Series, + EpisodeFile, + Images +} diff --git a/src/Sonarr.Api.V5/Episodes/EpisodeController.cs b/src/Sonarr.Api.V5/Episodes/EpisodeController.cs index dc02f0929..bb6e105ef 100644 --- a/src/Sonarr.Api.V5/Episodes/EpisodeController.cs +++ b/src/Sonarr.Api.V5/Episodes/EpisodeController.cs @@ -23,8 +23,12 @@ public EpisodeController(ISeriesService seriesService, [HttpGet] [Produces("application/json")] - public List GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List episodeIds, int? episodeFileId, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false) + public List GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List episodeIds, int? episodeFileId, [FromQuery] EpisodeSubresource[]? includeSubresources = null) { + var includeSeries = includeSubresources.Contains(EpisodeSubresource.Series); + var includeEpisodeFile = includeSubresources.Contains(EpisodeSubresource.EpisodeFile); + var includeImages = includeSubresources.Contains(EpisodeSubresource.Images); + if (seriesId.HasValue) { if (seasonNumber.HasValue) @@ -59,8 +63,10 @@ public ActionResult SetEpisodeMonitored([FromRoute] int id, [Fr [HttpPut("monitor")] [Consumes("application/json")] - public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource, [FromQuery] bool includeImages = false) + public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource, [FromQuery] EpisodeSubresource[]? includeSubresources = null) { + var includeImages = includeSubresources.Contains(EpisodeSubresource.Images); + if (resource.EpisodeIds.Count == 1) { _episodeService.SetEpisodeMonitored(resource.EpisodeIds.First(), resource.Monitored); diff --git a/src/Sonarr.Api.V5/Episodes/EpisodeSubresource.cs b/src/Sonarr.Api.V5/Episodes/EpisodeSubresource.cs new file mode 100644 index 000000000..c0f67f33a --- /dev/null +++ b/src/Sonarr.Api.V5/Episodes/EpisodeSubresource.cs @@ -0,0 +1,8 @@ +namespace Sonarr.Api.V5.Episodes; + +public enum EpisodeSubresource +{ + Series, + EpisodeFile, + Images +} diff --git a/src/Sonarr.Api.V5/History/HistoryController.cs b/src/Sonarr.Api.V5/History/HistoryController.cs index aa61edf9b..38d805009 100644 --- a/src/Sonarr.Api.V5/History/HistoryController.cs +++ b/src/Sonarr.Api.V5/History/HistoryController.cs @@ -62,7 +62,7 @@ protected HistoryResource MapToResource(EpisodeHistory model, bool includeSeries [HttpGet] [Produces("application/json")] - public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, [FromQuery(Name = "eventType")] int[]? eventTypes, int? episodeId, string? downloadId, [FromQuery] int[]? seriesIds = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null) + public PagingResource GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[]? eventTypes, int? episodeId, string? downloadId, [FromQuery] int[]? seriesIds = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec( @@ -94,21 +94,29 @@ public PagingResource GetHistory([FromQuery] PagingRequestResou pagingSpec.FilterExpressions.Add(h => seriesIds.Contains(h.SeriesId)); } + var includeSeries = includeSubresources.Contains(HistorySubresource.Series); + var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); + return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeSeries, includeEpisode)); } [HttpGet("since")] [Produces("application/json")] - public List GetHistorySince(DateTime date, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) + public List GetHistorySince(DateTime date, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { + var includeSeries = includeSubresources.Contains(HistorySubresource.Series); + var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); + return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); } [HttpGet("series")] [Produces("application/json")] - public List GetSeriesHistory(int seriesId, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) + public List GetSeriesHistory(int seriesId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var series = _seriesService.GetSeries(seriesId); + var includeSeries = includeSubresources.Contains(HistorySubresource.Series); + var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); return _historyService.GetBySeries(seriesId, eventType).Select(h => { @@ -120,9 +128,11 @@ public List GetSeriesHistory(int seriesId, EpisodeHistoryEventT [HttpGet("season")] [Produces("application/json")] - public List GetSeasonHistory(int seriesId, int seasonNumber, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) + public List GetSeasonHistory(int seriesId, int seasonNumber, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var series = _seriesService.GetSeries(seriesId); + var includeSeries = includeSubresources.Contains(HistorySubresource.Series); + var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); return _historyService.GetBySeason(seriesId, seasonNumber, eventType).Select(h => { @@ -134,10 +144,12 @@ public List GetSeasonHistory(int seriesId, int seasonNumber, Ep [HttpGet("episode")] [Produces("application/json")] - public List GetEpisodeHistory(int episodeId, EpisodeHistoryEventType? eventType = null, bool includeSeries = false, bool includeEpisode = false) + public List GetEpisodeHistory(int episodeId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var episode = _episodeService.GetEpisode(episodeId); var series = _seriesService.GetSeries(episode.SeriesId); + var includeSeries = includeSubresources.Contains(HistorySubresource.Series); + var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); return _historyService.GetByEpisode(episodeId, eventType) .Select(h => diff --git a/src/Sonarr.Api.V5/History/HistorySubresource.cs b/src/Sonarr.Api.V5/History/HistorySubresource.cs new file mode 100644 index 000000000..ccb7add69 --- /dev/null +++ b/src/Sonarr.Api.V5/History/HistorySubresource.cs @@ -0,0 +1,7 @@ +namespace Sonarr.Api.V5.History; + +public enum HistorySubresource +{ + Series, + Episode +} diff --git a/src/Sonarr.Api.V5/Queue/QueueController.cs b/src/Sonarr.Api.V5/Queue/QueueController.cs index 5418b898e..f3c946363 100644 --- a/src/Sonarr.Api.V5/Queue/QueueController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueController.cs @@ -135,7 +135,7 @@ public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] stri [HttpGet] [Produces("application/json")] - public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisodes = false, [FromQuery] int[]? seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] QueueStatus[]? status = null) + public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, [FromQuery] int[]? seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] QueueStatus[]? status = null, [FromQuery] QueueSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec( @@ -164,6 +164,9 @@ public PagingResource GetQueue([FromQuery] PagingRequestResource "timeleft", SortDirection.Ascending); + var includeSeries = includeSubresources.Contains(QueueSubresource.Series); + var includeEpisodes = includeSubresources.Contains(QueueSubresource.Episodes); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet() ?? [], protocol, languages?.ToHashSet() ?? [], quality?.ToHashSet() ?? [], status?.ToHashSet() ?? [], includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisodes)); } diff --git a/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs b/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs index 3d2afc427..c6d5526a9 100644 --- a/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs @@ -37,11 +37,13 @@ protected override QueueResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List GetQueue(int? seriesId, [FromQuery]List episodeIds, bool includeSeries = false, bool includeEpisodes = false) + public List GetQueue(int? seriesId, [FromQuery]List episodeIds, [FromQuery] QueueSubresource[]? includeSubresources = null) { var queue = _queueService.GetQueue(); var pending = _pendingReleaseService.GetPendingQueue(); var fullQueue = queue.Concat(pending); + var includeSeries = includeSubresources.Contains(QueueSubresource.Series); + var includeEpisodes = includeSubresources.Contains(QueueSubresource.Episodes); if (seriesId.HasValue) { diff --git a/src/Sonarr.Api.V5/Queue/QueueSubresource.cs b/src/Sonarr.Api.V5/Queue/QueueSubresource.cs new file mode 100644 index 000000000..2fb885198 --- /dev/null +++ b/src/Sonarr.Api.V5/Queue/QueueSubresource.cs @@ -0,0 +1,7 @@ +namespace Sonarr.Api.V5.Queue; + +public enum QueueSubresource +{ + Series, + Episodes +} diff --git a/src/Sonarr.Api.V5/Series/SeriesController.cs b/src/Sonarr.Api.V5/Series/SeriesController.cs index 34e487e7f..60123af9d 100644 --- a/src/Sonarr.Api.V5/Series/SeriesController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesController.cs @@ -19,7 +19,6 @@ using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; using Sonarr.Http; -using Sonarr.Http.Extensions; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; @@ -108,10 +107,11 @@ public SeriesController(IBroadcastSignalRMessage signalRBroadcaster, [HttpGet] [Produces("application/json")] - public List AllSeries(int? tvdbId, bool includeSeasonImages = false) + public List AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null) { var seriesStats = _seriesStatisticsService.SeriesStatistics(); var seriesResources = new List(); + var includeSeasonImages = includeSubresources.Contains(SeriesSubresource.SeasonImages); if (tvdbId.HasValue) { @@ -138,8 +138,10 @@ public override ActionResult GetResourceByIdWithErrorHandler(int [RestGetById] [Produces("application/json")] - public ActionResult GetResourceByIdWithErrorHandler(int id, [FromQuery]bool includeSeasonImages = false) + public ActionResult GetResourceByIdWithErrorHandler(int id, [FromQuery] SeriesSubresource[]? includeSubresources = null) { + var includeSeasonImages = includeSubresources.Contains(SeriesSubresource.SeasonImages); + try { var series = GetSeriesResourceById(id, includeSeasonImages); @@ -154,17 +156,25 @@ public ActionResult GetResourceByIdWithErrorHandler(int id, [Fro protected override SeriesResource? GetResourceById(int id) { - var includeSeasonImages = Request?.GetBooleanQueryParameter("includeSeasonImages", false) ?? false; + var includeSubresources = Request.Query["includeSubresources"].Select(v => + { + if (Enum.TryParse(v, true, out var enumValue)) + { + return enumValue; + } + + throw new BadRequestException($"The value '{v}' is not valid."); + }); + + var includeSeasonImages = includeSubresources.Contains(SeriesSubresource.SeasonImages); - // Parse IncludeImages and use it return GetSeriesResourceById(id, includeSeasonImages); } - private SeriesResource? GetSeriesResourceById(int id, bool includeSeasonImages = false) + private SeriesResource? GetSeriesResourceById(int id, bool includeSeasonImages) { var series = _seriesService.GetSeries(id); - // Parse IncludeImages and use it return GetSeriesResource(series, includeSeasonImages); } diff --git a/src/Sonarr.Api.V5/Series/SeriesSubresource.cs b/src/Sonarr.Api.V5/Series/SeriesSubresource.cs new file mode 100644 index 000000000..5104b3aab --- /dev/null +++ b/src/Sonarr.Api.V5/Series/SeriesSubresource.cs @@ -0,0 +1,6 @@ +namespace Sonarr.Api.V5.Series; + +public enum SeriesSubresource +{ + SeasonImages +} diff --git a/src/Sonarr.Api.V5/Wanted/CutoffController.cs b/src/Sonarr.Api.V5/Wanted/CutoffController.cs index 866927c0e..cf9f3cece 100644 --- a/src/Sonarr.Api.V5/Wanted/CutoffController.cs +++ b/src/Sonarr.Api.V5/Wanted/CutoffController.cs @@ -28,7 +28,7 @@ public CutoffController(IEpisodeCutoffService episodeCutoffService, [HttpGet] [Produces("application/json")] - public PagingResource GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeEpisodeFile = false, bool includeImages = false, bool monitored = true) + public PagingResource GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] CutoffSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec( @@ -50,6 +50,10 @@ public PagingResource GetCutoffUnmetEpisodes([FromQuery] Paging pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } + var includeSeries = includeSubresources.Contains(CutoffSubresource.Series); + var includeEpisodeFile = includeSubresources.Contains(CutoffSubresource.EpisodeFile); + var includeImages = includeSubresources.Contains(CutoffSubresource.Images); + var resource = pagingSpec.ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); return resource; diff --git a/src/Sonarr.Api.V5/Wanted/CutoffSubresource.cs b/src/Sonarr.Api.V5/Wanted/CutoffSubresource.cs new file mode 100644 index 000000000..138a765fa --- /dev/null +++ b/src/Sonarr.Api.V5/Wanted/CutoffSubresource.cs @@ -0,0 +1,8 @@ +namespace Sonarr.Api.V5.Wanted; + +public enum CutoffSubresource +{ + Series, + EpisodeFile, + Images +} diff --git a/src/Sonarr.Api.V5/Wanted/MissingController.cs b/src/Sonarr.Api.V5/Wanted/MissingController.cs index 752d0ac6c..d9cb6aee2 100644 --- a/src/Sonarr.Api.V5/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V5/Wanted/MissingController.cs @@ -24,7 +24,7 @@ public MissingController(IEpisodeService episodeService, [HttpGet] [Produces("application/json")] - public PagingResource GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool includeSeries = false, bool includeImages = false, bool monitored = true) + public PagingResource GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] MissingSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec( @@ -46,6 +46,9 @@ public PagingResource GetMissingEpisodes([FromQuery] PagingRequ pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } + var includeSeries = includeSubresources.Contains(MissingSubresource.Series); + var includeImages = includeSubresources.Contains(MissingSubresource.Images); + var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages)); return resource; diff --git a/src/Sonarr.Api.V5/Wanted/MissingSubresource.cs b/src/Sonarr.Api.V5/Wanted/MissingSubresource.cs new file mode 100644 index 000000000..d3c4e9e1e --- /dev/null +++ b/src/Sonarr.Api.V5/Wanted/MissingSubresource.cs @@ -0,0 +1,7 @@ +namespace Sonarr.Api.V5.Wanted; + +public enum MissingSubresource +{ + Series, + Images +}