From db9a9a6d95a437bf93dfbf0ab2ed0d1b9e2ba253 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 22 Oct 2024 14:25:02 +0200 Subject: [PATCH] feat: add query filter for unaired episodes --- .../API/v3/Controllers/EpisodeController.cs | 15 ++++++- .../API/v3/Controllers/SeriesController.cs | 41 +++++++++++++++---- Shoko.Server/Services/AnimeSeriesService.cs | 9 +++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/Shoko.Server/API/v3/Controllers/EpisodeController.cs b/Shoko.Server/API/v3/Controllers/EpisodeController.cs index 54fff6e4a..8e22e1a06 100644 --- a/Shoko.Server/API/v3/Controllers/EpisodeController.cs +++ b/Shoko.Server/API/v3/Controllers/EpisodeController.cs @@ -88,6 +88,7 @@ TmdbMetadataService tmdbMetadataService /// The page size. Set to 0 to disable pagination. /// The page index. /// Include missing episodes in the list. + /// Include unaired episodes in the list. /// Include hidden episodes in the list. /// Include data from selected s. /// Include watched episodes in the list. @@ -104,6 +105,7 @@ public ActionResult> GetAllEpisodes( [FromQuery, Range(0, 1000)] int pageSize = 20, [FromQuery, Range(1, int.MaxValue)] int page = 1, [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, + [FromQuery] IncludeOnlyFilter includeUnaired = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet includeDataFrom = null, [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, @@ -160,8 +162,17 @@ public ActionResult> GetAllEpisodes( // If we should hide missing episodes and the episode has no files, then hide it. // Or if we should only show missing episodes and the episode has files, the hide it. var shouldHideMissing = includeMissing == IncludeOnlyFilter.False; - var noFiles = shoko.VideoLocals.Count == 0; - if (shouldHideMissing == noFiles) + var isMissing = shoko.VideoLocals.Count == 0 && anidb.HasAired; + if (shouldHideMissing == isMissing) + return false; + } + if (includeUnaired != IncludeOnlyFilter.True) + { + // If we should hide unaired episodes and the episode has no files, then hide it. + // Or if we should only show unaired episodes and the episode has files, the hide it. + var shouldHideUnaired = includeUnaired == IncludeOnlyFilter.False; + var isUnaired = shoko.VideoLocals.Count == 0 && !anidb.HasAired; + if (shouldHideUnaired == isUnaired) return false; } diff --git a/Shoko.Server/API/v3/Controllers/SeriesController.cs b/Shoko.Server/API/v3/Controllers/SeriesController.cs index e8815e793..69fe43eaa 100644 --- a/Shoko.Server/API/v3/Controllers/SeriesController.cs +++ b/Shoko.Server/API/v3/Controllers/SeriesController.cs @@ -1780,6 +1780,7 @@ public ActionResult> GetTMDBSeasonsBySeriesID( /// The page size. Set to 0 to disable pagination. /// The page index. /// Include missing episodes in the list. + /// Include unaired episodes in the list. /// Include hidden episodes in the list. /// Include watched episodes in the list. /// Include manually linked episodes in the list. @@ -1798,6 +1799,7 @@ public ActionResult> GetEpisodes( [FromQuery, Range(0, 1000)] int pageSize = 20, [FromQuery, Range(1, int.MaxValue)] int page = 1, [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, + [FromQuery] IncludeOnlyFilter includeUnaired = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, [FromQuery] IncludeOnlyFilter includeManuallyLinked = IncludeOnlyFilter.True, @@ -1818,7 +1820,7 @@ public ActionResult> GetEpisodes( if (!User.AllowedSeries(series)) return Forbid(SeriesForbiddenForUser); - return GetEpisodesInternal(series, includeMissing, includeHidden, includeWatched, includeManuallyLinked, type, search, fuzzy) + return GetEpisodesInternal(series, includeMissing, includeUnaired, includeHidden, includeWatched, includeManuallyLinked, type, search, fuzzy) .ToListResult(a => new Episode(HttpContext, a, includeDataFrom, includeFiles, includeMediaInfo, includeAbsolutePaths, includeXRefs), page, pageSize); } @@ -1828,6 +1830,7 @@ public ActionResult> GetEpisodes( /// Series ID /// The new watched state. /// Include missing episodes in the list. + /// Include unaired episodes in the list. /// Include hidden episodes in the list. /// Include watched episodes in the list. /// Filter episodes by the specified s. @@ -1839,6 +1842,7 @@ public async Task MarkSeriesWatched( [FromRoute, Range(1, int.MaxValue)] int seriesID, [FromQuery] bool value = true, [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, + [FromQuery] IncludeOnlyFilter includeUnaired = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet type = null, @@ -1855,7 +1859,7 @@ public async Task MarkSeriesWatched( var userId = User.JMMUserID; var now = DateTime.Now; // this has a parallel query to evaluate filters and data in parallel, but that makes awaiting the SetWatchedStatus calls more difficult, so we ToList() it - await Task.WhenAll(GetEpisodesInternal(series, includeMissing, includeHidden, includeWatched, IncludeOnlyFilter.True, type, search, fuzzy).ToList() + await Task.WhenAll(GetEpisodesInternal(series, includeMissing, includeUnaired, includeHidden, includeWatched, IncludeOnlyFilter.True, type, search, fuzzy).ToList() .Select(episode => _watchedService.SetWatchedStatus(episode, value, true, now, false, userId, true))); _seriesService.UpdateStats(series, true, false); @@ -1867,6 +1871,7 @@ await Task.WhenAll(GetEpisodesInternal(series, includeMissing, includeHidden, in public ParallelQuery GetEpisodesInternal( SVR_AnimeSeries series, IncludeOnlyFilter includeMissing, + IncludeOnlyFilter includeUnaired, IncludeOnlyFilter includeHidden, IncludeOnlyFilter includeWatched, IncludeOnlyFilter includeManuallyLinked, @@ -1912,8 +1917,17 @@ public ParallelQuery GetEpisodesInternal( // If we should hide missing episodes and the episode has no files, then hide it. // Or if we should only show missing episodes and the episode has files, the hide it. var shouldHideMissing = includeMissing == IncludeOnlyFilter.False; - var noFiles = shoko.VideoLocals.Count == 0; - if (shouldHideMissing == noFiles) + var isMissing = shoko.VideoLocals.Count == 0 && anidb.HasAired; + if (shouldHideMissing == isMissing) + return false; + } + if (includeUnaired != IncludeOnlyFilter.True) + { + // If we should hide unaired episodes and the episode has no files, then hide it. + // Or if we should only show unaired episodes and the episode has files, the hide it. + var shouldHideUnaired = includeUnaired == IncludeOnlyFilter.False; + var isUnaired = shoko.VideoLocals.Count == 0 && !anidb.HasAired; + if (shouldHideUnaired == isUnaired) return false; } @@ -1979,6 +1993,7 @@ public ParallelQuery GetEpisodesInternal( /// The page size. Set to 0 to disable pagination. /// The page index. /// Include missing episodes in the list. + /// Include unaired episodes in the list. /// Include hidden episodes in the list. /// Include watched episodes in the list. /// Filter episodes by the specified s. @@ -1991,6 +2006,7 @@ public ParallelQuery GetEpisodesInternal( [FromQuery, Range(0, 1000)] int pageSize = 20, [FromQuery, Range(1, int.MaxValue)] int page = 1, [FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, + [FromQuery] IncludeOnlyFilter includeUnaired = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet type = null, @@ -2043,9 +2059,17 @@ public ParallelQuery GetEpisodesInternal( // If we should hide missing episodes and the episode has no files, then hide it. // Or if we should only show missing episodes and the episode has files, the hide it. var shouldHideMissing = includeMissing == IncludeOnlyFilter.False; - var files = shoko?.VideoLocals.Count ?? 0; - var noFiles = files == 0; - if (shouldHideMissing == noFiles) + var isMissing = shoko.VideoLocals.Count == 0 && anidb.HasAired; + if (shouldHideMissing == isMissing) + return false; + } + if (includeUnaired != IncludeOnlyFilter.True) + { + // If we should hide unaired episodes and the episode has no files, then hide it. + // Or if we should only show unaired episodes and the episode has files, the hide it. + var shouldHideUnaired = includeUnaired == IncludeOnlyFilter.False; + var isUnaired = shoko.VideoLocals.Count == 0 && !anidb.HasAired; + if (shouldHideUnaired == isUnaired) return false; } @@ -2101,6 +2125,7 @@ public ParallelQuery GetEpisodesInternal( /// Include specials in the search. /// Include other type episodes in the search. /// Include missing episodes in the list. + /// Include unaired episodes in the list. /// Include already watched episodes in the /// search if we determine the user is "re-watching" the series. /// Include files with the episodes. @@ -2115,6 +2140,7 @@ public ActionResult GetNextUnwatchedEpisode([FromRoute, Range(1, int.Ma [FromQuery] bool includeSpecials = true, [FromQuery] bool includeOthers = false, [FromQuery] bool includeMissing = true, + [FromQuery] bool includeUnaired = false, [FromQuery] bool includeRewatching = false, [FromQuery] bool includeFiles = false, [FromQuery] bool includeMediaInfo = false, @@ -2133,6 +2159,7 @@ public ActionResult GetNextUnwatchedEpisode([FromRoute, Range(1, int.Ma { IncludeCurrentlyWatching = !onlyUnwatched, IncludeMissing = includeMissing, + IncludeUnaired = includeUnaired, IncludeRewatching = includeRewatching, IncludeSpecials = includeSpecials, IncludeOthers = includeOthers, diff --git a/Shoko.Server/Services/AnimeSeriesService.cs b/Shoko.Server/Services/AnimeSeriesService.cs index d24fc156a..49dc700c1 100644 --- a/Shoko.Server/Services/AnimeSeriesService.cs +++ b/Shoko.Server/Services/AnimeSeriesService.cs @@ -893,6 +893,11 @@ public class NextUpQueryOptions /// public bool IncludeMissing { get; set; } = false; + /// + /// Include unaired episodes in the search. + /// + public bool IncludeUnaired { get; set; } = false; + /// /// Include already watched episodes in the search if we determine the /// user is "re-watching" the series. @@ -981,7 +986,7 @@ public SVR_AnimeEpisode GetNextEpisode(SVR_AnimeSeries series, int userID, NextU var (nextEpisode, _) = episodeList .Skip(nextIndex) - .FirstOrDefault(options.IncludeMissing ? _ => true : tuple => tuple.episode.VideoLocals.Count > 0); + .FirstOrDefault(options.IncludeUnaired ? _ => true : options.IncludeMissing ? tuple => tuple.AniDB_Episode.HasAired : tuple => tuple.episode.VideoLocals.Count > 0 && tuple.AniDB_Episode.HasAired); return nextEpisode; } } @@ -996,7 +1001,7 @@ public SVR_AnimeEpisode GetNextEpisode(SVR_AnimeSeries series, int userID, NextU return !episodeUserRecord.WatchedDate.HasValue; }) - .FirstOrDefault(options.IncludeMissing ? _ => true : tuple => tuple.episode.VideoLocals.Count > 0); + .FirstOrDefault(options.IncludeUnaired ? _ => true : options.IncludeMissing ? tuple => tuple.AniDB_Episode.HasAired : tuple => tuple.episode.VideoLocals.Count > 0 && tuple.AniDB_Episode.HasAired); // Disable first episode from showing up in the search. if (options.DisableFirstEpisode && anidbEpisode is not null && anidbEpisode.EpisodeType == (int)EpisodeType.Episode && anidbEpisode.EpisodeNumber == 1)