| 
					
				 | 
			
			
				@@ -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; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 |