#nullable disable #pragma warning disable CS1591 using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { public class SeriesMetadataService : MetadataService { private readonly ILocalizationManager _localizationManager; public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILocalizationManager localizationManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { _localizationManager = localizationManager; } /// protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); RemoveObsoleteSeasons(item); await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } /// protected override bool IsFullLocalMetadata(Series item) { if (string.IsNullOrWhiteSpace(item.Overview)) { return false; } if (!item.ProductionYear.HasValue) { return false; } return base.IsFullLocalMetadata(item); } /// protected override void MergeData(MetadataResult source, MetadataResult target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); var sourceItem = source.Item; var targetItem = target.Item; if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) { targetItem.AirTime = sourceItem.AirTime; } if (replaceData || !targetItem.Status.HasValue) { targetItem.Status = sourceItem.Status; } if (replaceData || targetItem.AirDays == null || targetItem.AirDays.Length == 0) { targetItem.AirDays = sourceItem.AirDays; } } private void RemoveObsoleteSeasons(Series series) { // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync. var physicalSeasonNumbers = new HashSet(); var virtualSeasons = new List(); foreach (var existingSeason in series.Children.OfType()) { if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue) { physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value); } else if (existingSeason.LocationType == LocationType.Virtual) { virtualSeasons.Add(existingSeason); } } 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().Any()) { Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name); LibraryManager.DeleteItem( virtualSeason, new DeleteOptions { DeleteFileLocation = true }, false); } } } /// /// Creates seasons for all episodes that aren't in a season folder. /// If no season number can be determined, a dummy season will be created. /// /// The series. /// The cancellation token. /// The async task. private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) { var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var episodesInSeriesFolder = seriesChildren .OfType() .Where(i => !i.IsInSeasonFolder); List seasons = seriesChildren.OfType().ToList(); // Loop through the unique season numbers foreach (var episode in episodesInSeriesFolder) { // Null season numbers will have a 'dummy' season created because seasons are always required. var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null; var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); if (existingSeason == null) { var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false); seasons.Add(season); } else if (existingSeason.IsVirtualItem) { existingSeason.IsVirtualItem = false; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } } /// /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. /// /// The series. /// The season number. /// The cancellation token. /// The newly created season. private async Task CreateSeasonAsync( Series series, int? seasonNumber, CancellationToken cancellationToken) { string seasonName = seasonNumber switch { null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) }; Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var season = new Season { 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 }; series.AddChild(season); await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); return season; } } }