Ver Fonte

Merge pull request #4792 from cvium/fix_missing_seasons

Add missing seasons during AfterMetadataRefresh

(cherry picked from commit 8eeed8252374ed5713a67b947aaad0aed6a681e5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
Bond-009 há 4 anos atrás
pai
commit
c08ce82a04
1 ficheiros alterados com 132 adições e 1 exclusões
  1. 132 1
      MediaBrowser.Providers/TV/SeriesMetadataService.cs

+ 132 - 1
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -1,10 +1,16 @@
 #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;
@@ -13,14 +19,27 @@ namespace MediaBrowser.Providers.TV
 {
     public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
     {
+        private readonly ILocalizationManager _localizationManager;
+
         public SeriesMetadataService(
             IServerConfigurationManager serverConfigurationManager,
             ILogger<SeriesMetadataService> logger,
             IProviderManager providerManager,
             IFileSystem fileSystem,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            ILocalizationManager localizationManager)
             : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
+            _localizationManager = localizationManager;
+        }
+
+        /// <inheritdoc />
+        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);
         }
 
         /// <inheritdoc />
@@ -62,5 +81,117 @@ namespace MediaBrowser.Providers.TV
                 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<int>();
+            var virtualSeasons = new List<Season>();
+            foreach (var existingSeason in series.Children.OfType<Season>())
+            {
+                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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>The async task.</returns>
+        private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
+        {
+            var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
+                .Cast<Episode>()
+                .Where(i => !i.IsInSeasonFolder);
+
+            List<Season> seasons = series.Children.OfType<Season>().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);
+                }
+            }
+        }
+
+        /// <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="seasonNumber">The season number.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>The newly created season.</returns>
+        private async Task<Season> 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, cancellationToken);
+
+            await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
+
+            return season;
+        }
     }
 }