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.cs91
1 files changed, 90 insertions, 1 deletions
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 02040653d1..803fab538f 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,6 +297,28 @@ 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();
@@ -280,7 +364,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
foreach (var episode in episodes)
{
var season = seasons.FirstOrDefault(i => i.IndexNumber == episode.ParentIndexNumber);
- if (season is null || episode.SeasonId.Equals(season.Id))
+ if (season is null || (episode.SeasonId.Equals(season.Id) && episode.ParentId.Equals(season.Id)))
{
continue;
}
@@ -288,6 +372,11 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
// Assign the correct season id and name to episode.
episode.SeasonId = season.Id;
episode.SeasonName = season.Name;
+
+ // We need to set ParentId here for episodes in virtual seasons (e.g., flat structures), otherwise it retains the
+ // ParentId from the series.
+ episode.SetParent(season);
+
await episode.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
}