SeriesMetadataService.cs 8.1 KB

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