aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/TV/SeriesMetadataService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers/TV/SeriesMetadataService.cs')
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs109
1 files changed, 106 insertions, 3 deletions
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 078c396730..b350f482c3 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -78,11 +78,73 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+ // Note that this only updates the children's SeriesPresentationUniqueKey and SeasonId, not the ParentIndexNumber
+ if (LibraryManager.GetLibraryOptions(item).EnableAutomaticSeriesGrouping)
+ {
+ await UpdateSeriesChildrenInfoAsync(item, cancellationToken).ConfigureAwait(false);
+ }
+
RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item);
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
+ /// <summary>
+ /// Reconciles seasons and episodes with the series' finalized state.
+ /// </summary>
+ /// <remarks>
+ /// The series' presentation unique key can change during a refresh once provider ids become
+ /// available - notably with <c>EnableAutomaticSeriesGrouping</c>, where the key is derived from
+ /// the provider id and the owning libraries instead of the (immutable) item id. Seasons and
+ /// episodes cache this value in <see cref="IHasSeries.SeriesPresentationUniqueKey"/> and are
+ /// matched to (and displayed under) the series by it, so any child left with a stale key - or an
+ /// episode not yet linked to a freshly created season - stays hidden until a later scan. Syncing
+ /// them against the series here lets everything appear within a single scan.
+ /// </remarks>
+ /// <param name="series">The series.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ private async Task UpdateSeriesChildrenInfoAsync(Series series, CancellationToken cancellationToken)
+ {
+ // Reload children so episode numbers / seasons persisted earlier in the refresh are seen.
+ series.Children = null;
+ var seriesKey = series.GetPresentationUniqueKey();
+ var children = series.GetRecursiveChildren(i => i is Season || i is Episode);
+ var seasons = children.OfType<Season>().ToList();
+
+ foreach (var child in children)
+ {
+ var updateType = ItemUpdateType.None;
+
+ if (child is IHasSeries hasSeries
+ && !string.Equals(hasSeries.SeriesPresentationUniqueKey, seriesKey, StringComparison.Ordinal))
+ {
+ hasSeries.SeriesPresentationUniqueKey = seriesKey;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+
+ if (child is Episode episode)
+ {
+ var seasonId = episode.FindSeasonId();
+ if (seasonId.IsEmpty() && episode.ParentIndexNumber.HasValue)
+ {
+ seasonId = seasons.Find(s => s.IndexNumber == episode.ParentIndexNumber)?.Id ?? Guid.Empty;
+ }
+
+ if (!seasonId.IsEmpty() && !episode.SeasonId.Equals(seasonId))
+ {
+ episode.SeasonId = seasonId;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ }
+
+ if (updateType > ItemUpdateType.None)
+ {
+ await child.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
/// <inheritdoc />
protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
@@ -235,7 +297,30 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
{
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
+
+ // CreateSeasonsAsync can run before the episodes themselves have been refreshed during an
+ // initial scan, so their ParentIndexNumber may still be unset. Resolve the season number
+ // from the path first to avoid creating a premature "Season Unknown" instead of the real
+ // season for episodes that live directly in a flat series folder.
+ foreach (var episode in seriesChildren.OfType<Episode>())
+ {
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ continue;
+ }
+
+ try
+ {
+ LibraryManager.FillMissingEpisodeNumbersFromPath(episode, false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error resolving season number from path for {Path}", episode.Path);
+ }
+ }
+
var seasons = seriesChildren.OfType<Season>().ToList();
+ var episodes = seriesChildren.OfType<Episode>().ToList();
var physicalSeasonIds = seasons
.Where(e => e.LocationType != LocationType.Virtual)
@@ -261,11 +346,12 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
if (existingSeason is null)
{
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
- await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
+ var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
+ seasons.Add(season);
}
else if (existingSeason.IsVirtualItem)
{
- var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
+ var episodeCount = episodes.Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode);
if (episodeCount > 0)
{
existingSeason.IsVirtualItem = false;
@@ -273,6 +359,21 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
}
}
}
+
+ // Loop through episodes
+ foreach (var episode in episodes)
+ {
+ var season = seasons.FirstOrDefault(i => i.IndexNumber == episode.ParentIndexNumber);
+ if (season is null || episode.SeasonId.Equals(season.Id))
+ {
+ continue;
+ }
+
+ // Assign the correct season id and name to episode.
+ episode.SeasonId = season.Id;
+ episode.SeasonName = season.Name;
+ await episode.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
}
/// <summary>
@@ -283,7 +384,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
/// <param name="seasonNumber">The season number.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly created season.</returns>
- private async Task CreateSeasonAsync(
+ private async Task<Season> CreateSeasonAsync(
Series series,
string? seasonName,
int? seasonNumber,
@@ -306,6 +407,8 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
series.AddChild(season);
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+
+ return season;
}
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)