diff options
Diffstat (limited to 'MediaBrowser.Providers/TV')
| -rw-r--r-- | MediaBrowser.Providers/TV/EpisodeMetadataService.cs | 159 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/SeasonMetadataService.cs | 150 | ||||
| -rw-r--r-- | MediaBrowser.Providers/TV/SeriesMetadataService.cs | 414 |
3 files changed, 362 insertions, 361 deletions
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index 9b4793ee6..b2b6cd9ab 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -1,6 +1,7 @@ using System; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -8,106 +9,102 @@ using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV +namespace MediaBrowser.Providers.TV; + +/// <summary> +/// Service to manage episode metadata. +/// </summary> +public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo> { /// <summary> - /// Service to manage episode metadata. + /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class. /// </summary> - public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param> + public EpisodeMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger<EpisodeMetadataService> logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IExternalDataManager externalDataManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager) + { + } + + /// <inheritdoc /> + protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType) { - /// <summary> - /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class. - /// </summary> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> - /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - public EpisodeMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger<EpisodeMetadataService> logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); + + var seriesName = item.FindSeriesName(); + if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) { + item.SeriesName = seriesName; + updatedType |= ItemUpdateType.MetadataImport; } - /// <inheritdoc /> - protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType) + var seasonName = item.FindSeasonName(); + if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal)) { - var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); - - var seriesName = item.FindSeriesName(); - if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) - { - item.SeriesName = seriesName; - updatedType |= ItemUpdateType.MetadataImport; - } - - var seasonName = item.FindSeasonName(); - if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal)) - { - item.SeasonName = seasonName; - updatedType |= ItemUpdateType.MetadataImport; - } - - var seriesId = item.FindSeriesId(); - if (!item.SeriesId.Equals(seriesId)) - { - item.SeriesId = seriesId; - updatedType |= ItemUpdateType.MetadataImport; - } - - var seasonId = item.FindSeasonId(); - if (!item.SeasonId.Equals(seasonId)) - { - item.SeasonId = seasonId; - updatedType |= ItemUpdateType.MetadataImport; - } + item.SeasonName = seasonName; + updatedType |= ItemUpdateType.MetadataImport; + } - var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); - if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) - { - item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - updatedType |= ItemUpdateType.MetadataImport; - } + var seriesId = item.FindSeriesId(); + if (!item.SeriesId.Equals(seriesId)) + { + item.SeriesId = seriesId; + updatedType |= ItemUpdateType.MetadataImport; + } - return updatedType; + var seasonId = item.FindSeasonId(); + if (!item.SeasonId.Equals(seasonId)) + { + item.SeasonId = seasonId; + updatedType |= ItemUpdateType.MetadataImport; } - /// <inheritdoc /> - protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); + if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; + updatedType |= ItemUpdateType.MetadataImport; + } + + return updatedType; + } - var sourceItem = source.Item; - var targetItem = target.Item; + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); - if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue) - { - targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber; - } + var sourceItem = source.Item; + var targetItem = target.Item; - if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue) - { - targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber; - } + if (replaceData || !targetItem.AirsBeforeSeasonNumber.HasValue) + { + targetItem.AirsBeforeSeasonNumber = sourceItem.AirsBeforeSeasonNumber; + } - if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue) - { - targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber; - } + if (replaceData || !targetItem.AirsAfterSeasonNumber.HasValue) + { + targetItem.AirsAfterSeasonNumber = sourceItem.AirsAfterSeasonNumber; + } - if (replaceData || !targetItem.IndexNumberEnd.HasValue) - { - targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd; - } + if (replaceData || !targetItem.AirsBeforeEpisodeNumber.HasValue) + { + targetItem.AirsBeforeEpisodeNumber = sourceItem.AirsBeforeEpisodeNumber; + } - if (replaceData || !targetItem.ParentIndexNumber.HasValue) - { - targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber; - } + if (replaceData || !targetItem.IndexNumberEnd.HasValue) + { + targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd; } } } diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index b27ccaa6a..ea228a658 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -4,6 +4,7 @@ using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -11,102 +12,103 @@ using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV +namespace MediaBrowser.Providers.TV; + +/// <summary> +/// Service to manage season metadata. +/// </summary> +public class SeasonMetadataService : MetadataService<Season, SeasonInfo> { /// <summary> - /// Service to manage season metadata. + /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class. /// </summary> - public class SeasonMetadataService : MetadataService<Season, SeasonInfo> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param> + public SeasonMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger<SeasonMetadataService> logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + IExternalDataManager externalDataManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager) { - /// <summary> - /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class. - /// </summary> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> - /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - public SeasonMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger<SeasonMetadataService> logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) - { - } + } - /// <inheritdoc /> - protected override bool EnableUpdatingPremiereDateFromChildren => true; + /// <inheritdoc /> + protected override bool EnableUpdatingPremiereDateFromChildren => true; - /// <inheritdoc /> - protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType) - { - var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); - - if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name)) - { - var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName; + /// <inheritdoc /> + protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType) + { + var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType); - if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase)) - { - item.Name = seasonZeroDisplayName; - updatedType |= ItemUpdateType.MetadataEdit; - } - } + if (item.IndexNumber == 0 && !item.IsLocked && !item.LockedFields.Contains(MetadataField.Name)) + { + var seasonZeroDisplayName = LibraryManager.GetLibraryOptions(item).SeasonZeroDisplayName; - var seriesName = item.FindSeriesName(); - if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) + if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase)) { - item.SeriesName = seriesName; - updatedType |= ItemUpdateType.MetadataImport; + item.Name = seasonZeroDisplayName; + updatedType |= ItemUpdateType.MetadataEdit; } + } - var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); - if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) - { - item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - updatedType |= ItemUpdateType.MetadataImport; - } + var seriesName = item.FindSeriesName(); + if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal)) + { + item.SeriesName = seriesName; + updatedType |= ItemUpdateType.MetadataImport; + } - var seriesId = item.FindSeriesId(); - if (!item.SeriesId.Equals(seriesId)) - { - item.SeriesId = seriesId; - updatedType |= ItemUpdateType.MetadataImport; - } + var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey(); + if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal)) + { + item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; + updatedType |= ItemUpdateType.MetadataImport; + } - return updatedType; + var seriesId = item.FindSeriesId(); + if (!item.SeriesId.Equals(seriesId)) + { + item.SeriesId = seriesId; + updatedType |= ItemUpdateType.MetadataImport; } - /// <inheritdoc /> - protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item) - => item.GetEpisodes(); + return updatedType; + } - /// <inheritdoc /> - protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) - { - var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); + /// <inheritdoc /> + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item) + => item.GetEpisodes(); - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) - { - updateType |= SaveIsVirtualItem(item, children); - } + /// <inheritdoc /> + protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) + { + var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); - return updateType; + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + { + updateType |= SaveIsVirtualItem(item, children); } - private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes) - { - var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual)); + return updateType; + } - if (item.IsVirtualItem != isVirtualItem) - { - item.IsVirtualItem = isVirtualItem; - return ItemUpdateType.MetadataEdit; - } + private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes) + { + var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual)); - return ItemUpdateType.None; + if (item.IsVirtualItem != isVirtualItem) + { + item.IsVirtualItem = isVirtualItem; + return ItemUpdateType.MetadataEdit; } + + return ItemUpdateType.None; } } diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 284415dce..0ccb7f80e 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -16,269 +17,270 @@ using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV +namespace MediaBrowser.Providers.TV; + +/// <summary> +/// Service to manage series metadata. +/// </summary> +public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { + private readonly ILocalizationManager _localizationManager; + /// <summary> - /// Service to manage series metadata. + /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class. /// </summary> - public class SeriesMetadataService : MetadataService<Series, SeriesInfo> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param> + public SeriesMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger<SeriesMetadataService> logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + ILocalizationManager localizationManager, + IExternalDataManager externalDataManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager) { - private readonly ILocalizationManager _localizationManager; + _localizationManager = localizationManager; + } - /// <summary> - /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class. - /// </summary> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> - /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public SeriesMetadataService( - IServerConfigurationManager serverConfigurationManager, - ILogger<SeriesMetadataService> logger, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager, - ILocalizationManager localizationManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + /// <inheritdoc /> + public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + { + if (item is Series series) { - _localizationManager = localizationManager; - } + var seasons = series.GetRecursiveChildren(i => i is Season).ToList(); - /// <inheritdoc /> - public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) - { - if (item is Series series) + foreach (var season in seasons) { - var seasons = series.GetRecursiveChildren(i => i is Season).ToList(); - - foreach (var season in seasons) + var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata); + if (hasUpdate) { - var hasUpdate = refreshOptions != null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata); - if (hasUpdate) - { - await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } + await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } - - return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false); } - /// <inheritdoc /> - protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) - { - await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); + return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false); + } - RemoveObsoleteEpisodes(item); - RemoveObsoleteSeasons(item); - await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + /// <inheritdoc /> + protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + { + await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); + + RemoveObsoleteEpisodes(item); + RemoveObsoleteSeasons(item); + await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + } + + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + var sourceItem = source.Item; + var targetItem = target.Item; + + if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) + { + targetItem.AirTime = sourceItem.AirTime; } - /// <inheritdoc /> - protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) + if (replaceData || !targetItem.Status.HasValue) { - base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); + targetItem.Status = sourceItem.Status; + } - var sourceItem = source.Item; - var targetItem = target.Item; + if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0) + { + targetItem.AirDays = sourceItem.AirDays; + } + } - if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) + private void RemoveObsoleteSeasons(Series series) + { + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync. + var physicalSeasonNumbers = new HashSet<int>(); + var virtualSeasons = new List<Season>(); + foreach (var existingSeason in series.Children.OfType<Season>()) + { + if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue) { - targetItem.AirTime = sourceItem.AirTime; + physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value); } - - if (replaceData || !targetItem.Status.HasValue) + else if (existingSeason.LocationType == LocationType.Virtual) { - targetItem.Status = sourceItem.Status; + virtualSeasons.Add(existingSeason); } + } - if (replaceData || targetItem.AirDays is null || targetItem.AirDays.Length == 0) + foreach (var virtualSeason in virtualSeasons) + { + var seasonNumber = virtualSeason.IndexNumber; + // If there's a physical season with the same number or no episodes in the season, delete it + if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) + || virtualSeason.GetEpisodes().Count == 0) { - targetItem.AirDays = sourceItem.AirDays; + Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); + + LibraryManager.DeleteItem( + virtualSeason, + new DeleteOptions + { + // Internal metadata paths are removed regardless of this. + DeleteFileLocation = false + }, + false); } } + } - private void RemoveObsoleteSeasons(Series series) + private void RemoveObsoleteEpisodes(Series series) + { + var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true) + .OfType<Episode>() + .GroupBy(e => e.ParentIndexNumber) + .ToList(); + + foreach (var seasonEpisodes in episodesBySeason) { - // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync. - var physicalSeasonNumbers = new HashSet<int>(); - var virtualSeasons = new List<Season>(); - foreach (var existingSeason in series.Children.OfType<Season>()) + List<Episode> nonPhysicalEpisodes = []; + List<Episode> physicalEpisodes = []; + foreach (var episode in seasonEpisodes) { - if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue) + if (episode.IsVirtualItem || episode.IsMissingEpisode) { - physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value); - } - else if (existingSeason.LocationType == LocationType.Virtual) - { - virtualSeasons.Add(existingSeason); + nonPhysicalEpisodes.Add(episode); + continue; } + + physicalEpisodes.Add(episode); } - foreach (var virtualSeason in virtualSeasons) + // Only consider non-physical episodes + foreach (var episode in nonPhysicalEpisodes) { - var seasonNumber = virtualSeason.IndexNumber; - // If there's a physical season with the same number or no episodes in the season, delete it - if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value)) - || virtualSeason.GetEpisodes().Count == 0) - { - Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); + // Episodes without an episode number are practically orphaned and should be deleted + // Episodes with a physical equivalent should be deleted (they are no longer missing) + var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value)); - LibraryManager.DeleteItem( - virtualSeason, - new DeleteOptions - { - // Internal metadata paths are removed regardless of this. - DeleteFileLocation = false - }, - false); + if (shouldKeep) + { + continue; } + + DeleteEpisode(episode); } } + } - private void RemoveObsoleteEpisodes(Series series) - { - var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true) - .OfType<Episode>() - .GroupBy(e => e.ParentIndexNumber) - .ToList(); + private void DeleteEpisode(Episode episode) + { + Logger.LogInformation( + "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}", + episode.ParentIndexNumber, + episode.IndexNumber, + episode.SeriesName); - foreach (var seasonEpisodes in episodesBySeason) + LibraryManager.DeleteItem( + episode, + new DeleteOptions { - List<Episode> nonPhysicalEpisodes = []; - List<Episode> physicalEpisodes = []; - foreach (var episode in seasonEpisodes) - { - if (episode.IsVirtualItem || episode.IsMissingEpisode) - { - nonPhysicalEpisodes.Add(episode); - continue; - } + // Internal metadata paths are removed regardless of this. + DeleteFileLocation = false + }, + false); + } - physicalEpisodes.Add(episode); - } + /// <summary> + /// Creates seasons for all episodes if they don't exist. + /// If no season number can be determined, a dummy season will be created. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken) + { + var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); + var seasons = seriesChildren.OfType<Season>().ToList(); + var uniqueSeasonNumbers = seriesChildren + .OfType<Episode>() + .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) + .Distinct(); - // Only consider non-physical episodes - foreach (var episode in nonPhysicalEpisodes) + // Loop through the unique season numbers + foreach (var seasonNumber in uniqueSeasonNumbers) + { + // Null season numbers will have a 'dummy' season created because seasons are always required. + var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); + if (existingSeason is null) + { + var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); + await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); + } + else if (existingSeason.IsVirtualItem) + { + var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode); + if (episodeCount > 0) { - // Episodes without an episode number are practically orphaned and should be deleted - // Episodes with a physical equivalent should be deleted (they are no longer missing) - var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value)); - - if (shouldKeep) - { - continue; - } - - DeleteEpisode(episode); + existingSeason.IsVirtualItem = false; + await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } } + } - private void DeleteEpisode(Episode episode) - { - Logger.LogInformation( - "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}", - episode.ParentIndexNumber, - episode.IndexNumber, - episode.SeriesName); - - LibraryManager.DeleteItem( - episode, - new DeleteOptions - { - // Internal metadata paths are removed regardless of this. - DeleteFileLocation = false - }, - false); - } + /// <summary> + /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="seasonName">The season name.</param> + /// <param name="seasonNumber">The season number.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The newly created season.</returns> + private async Task CreateSeasonAsync( + Series series, + string? seasonName, + int? seasonNumber, + CancellationToken cancellationToken) + { + Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); - /// <summary> - /// Creates seasons for all episodes if they don't exist. - /// If no season number can be determined, a dummy season will be created. - /// </summary> - /// <param name="series">The series.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>The async task.</returns> - private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken) + var season = new Season { - var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); - var seasons = seriesChildren.OfType<Season>().ToList(); - var uniqueSeasonNumbers = seriesChildren - .OfType<Episode>() - .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) - .Distinct(); + Name = seasonName, + IndexNumber = seasonNumber, + Id = LibraryManager.GetNewItemId( + series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, + typeof(Season)), + IsVirtualItem = false, + SeriesId = series.Id, + SeriesName = series.Name, + SeriesPresentationUniqueKey = series.GetPresentationUniqueKey() + }; - // Loop through the unique season numbers - foreach (var seasonNumber in uniqueSeasonNumbers) - { - // Null season numbers will have a 'dummy' season created because seasons are always required. - var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); - if (existingSeason is null) - { - var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); - await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); - } - else if (existingSeason.IsVirtualItem) - { - var episodeCount = seriesChildren.OfType<Episode>().Count(e => e.ParentIndexNumber == seasonNumber && !e.IsMissingEpisode); - if (episodeCount > 0) - { - existingSeason.IsVirtualItem = false; - await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - } - } - } + series.AddChild(season); + await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); + } - /// <summary> - /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. - /// </summary> - /// <param name="series">The series.</param> - /// <param name="seasonName">The season name.</param> - /// <param name="seasonNumber">The season number.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>The newly created season.</returns> - private async Task CreateSeasonAsync( - Series series, - string? seasonName, - int? seasonNumber, - CancellationToken cancellationToken) + private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) + { + if (string.IsNullOrEmpty(seasonName)) { - Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); - - var season = new Season + seasonName = seasonNumber switch { - Name = seasonName, - IndexNumber = seasonNumber, - Id = LibraryManager.GetNewItemId( - series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, - typeof(Season)), - IsVirtualItem = false, - SeriesId = series.Id, - SeriesName = series.Name, - SeriesPresentationUniqueKey = series.GetPresentationUniqueKey() + null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), + 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, + _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) }; - - series.AddChild(season); - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); } - private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) - { - if (string.IsNullOrEmpty(seasonName)) - { - seasonName = seasonNumber switch - { - null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), - 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, - _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) - }; - } - - return seasonName; - } + return seasonName; } } |
