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)