SeriesMetadataService.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. #pragma warning disable CS1591
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using MediaBrowser.Controller.Configuration;
  8. using MediaBrowser.Controller.Entities.TV;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.Providers;
  11. using MediaBrowser.Model.Entities;
  12. using MediaBrowser.Model.Globalization;
  13. using MediaBrowser.Model.IO;
  14. using MediaBrowser.Providers.Manager;
  15. using Microsoft.Extensions.Logging;
  16. namespace MediaBrowser.Providers.TV
  17. {
  18. public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
  19. {
  20. private readonly ILocalizationManager _localizationManager;
  21. public SeriesMetadataService(
  22. IServerConfigurationManager serverConfigurationManager,
  23. ILogger<SeriesMetadataService> logger,
  24. IProviderManager providerManager,
  25. IFileSystem fileSystem,
  26. ILibraryManager libraryManager,
  27. ILocalizationManager localizationManager)
  28. : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
  29. {
  30. _localizationManager = localizationManager;
  31. }
  32. /// <inheritdoc />
  33. protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
  34. {
  35. await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
  36. RemoveObsoleteSeasons(item);
  37. await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
  38. }
  39. /// <inheritdoc />
  40. protected override bool IsFullLocalMetadata(Series item)
  41. {
  42. if (string.IsNullOrWhiteSpace(item.Overview))
  43. {
  44. return false;
  45. }
  46. if (!item.ProductionYear.HasValue)
  47. {
  48. return false;
  49. }
  50. return base.IsFullLocalMetadata(item);
  51. }
  52. /// <inheritdoc />
  53. protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
  54. {
  55. ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
  56. var sourceItem = source.Item;
  57. var targetItem = target.Item;
  58. if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
  59. {
  60. targetItem.AirTime = sourceItem.AirTime;
  61. }
  62. if (replaceData || !targetItem.Status.HasValue)
  63. {
  64. targetItem.Status = sourceItem.Status;
  65. }
  66. if (replaceData || targetItem.AirDays == null || targetItem.AirDays.Length == 0)
  67. {
  68. targetItem.AirDays = sourceItem.AirDays;
  69. }
  70. }
  71. private void RemoveObsoleteSeasons(Series series)
  72. {
  73. // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in FillInMissingSeasonsAsync.
  74. var physicalSeasonNumbers = new HashSet<int>();
  75. var virtualSeasons = new List<Season>();
  76. foreach (var existingSeason in series.Children.OfType<Season>())
  77. {
  78. if (existingSeason.LocationType != LocationType.Virtual && existingSeason.IndexNumber.HasValue)
  79. {
  80. physicalSeasonNumbers.Add(existingSeason.IndexNumber.Value);
  81. }
  82. else if (existingSeason.LocationType == LocationType.Virtual)
  83. {
  84. virtualSeasons.Add(existingSeason);
  85. }
  86. }
  87. foreach (var virtualSeason in virtualSeasons)
  88. {
  89. var seasonNumber = virtualSeason.IndexNumber;
  90. // If there's a physical season with the same number or no episodes in the season, delete it
  91. if ((seasonNumber.HasValue && physicalSeasonNumbers.Contains(seasonNumber.Value))
  92. || !virtualSeason.GetEpisodes().Any())
  93. {
  94. Logger.LogInformation("Removing virtual season {SeasonNumber} in series {SeriesName}", virtualSeason.IndexNumber, series.Name);
  95. LibraryManager.DeleteItem(
  96. virtualSeason,
  97. new DeleteOptions
  98. {
  99. DeleteFileLocation = true
  100. },
  101. false);
  102. }
  103. }
  104. }
  105. /// <summary>
  106. /// Creates seasons for all episodes that aren't in a season folder.
  107. /// If no season number can be determined, a dummy season will be created.
  108. /// </summary>
  109. /// <param name="series">The series.</param>
  110. /// <param name="cancellationToken">The cancellation token.</param>
  111. /// <returns>The async task.</returns>
  112. private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
  113. {
  114. var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
  115. .Cast<Episode>()
  116. .Where(i => !i.IsInSeasonFolder);
  117. List<Season> seasons = series.Children.OfType<Season>().ToList();
  118. // Loop through the unique season numbers
  119. foreach (var episode in episodesInSeriesFolder)
  120. {
  121. // Null season numbers will have a 'dummy' season created because seasons are always required.
  122. var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null;
  123. var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
  124. if (existingSeason == null)
  125. {
  126. var season = await CreateSeasonAsync(series, seasonNumber, cancellationToken).ConfigureAwait(false);
  127. seasons.Add(season);
  128. }
  129. else if (existingSeason.IsVirtualItem)
  130. {
  131. existingSeason.IsVirtualItem = false;
  132. await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
  133. }
  134. }
  135. }
  136. /// <summary>
  137. /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata.
  138. /// </summary>
  139. /// <param name="series">The series.</param>
  140. /// <param name="seasonNumber">The season number.</param>
  141. /// <param name="cancellationToken">The cancellation token.</param>
  142. /// <returns>The newly created season.</returns>
  143. private async Task<Season> CreateSeasonAsync(
  144. Series series,
  145. int? seasonNumber,
  146. CancellationToken cancellationToken)
  147. {
  148. string seasonName = seasonNumber switch
  149. {
  150. null => _localizationManager.GetLocalizedString("NameSeasonUnknown"),
  151. 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName,
  152. _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value)
  153. };
  154. Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
  155. var season = new Season
  156. {
  157. Name = seasonName,
  158. IndexNumber = seasonNumber,
  159. Id = LibraryManager.GetNewItemId(
  160. series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
  161. typeof(Season)),
  162. IsVirtualItem = false,
  163. SeriesId = series.Id,
  164. SeriesName = series.Name
  165. };
  166. series.AddChild(season, cancellationToken);
  167. await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
  168. return season;
  169. }
  170. }
  171. }