MissingEpisodeProvider.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. using System;
  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;
  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.Plugins.TheTvdb;
  16. using Microsoft.Extensions.Logging;
  17. namespace MediaBrowser.Providers.TV
  18. {
  19. public class MissingEpisodeProvider
  20. {
  21. private const double UnairedEpisodeThresholdDays = 2;
  22. private readonly IServerConfigurationManager _config;
  23. private readonly ILogger _logger;
  24. private readonly ILibraryManager _libraryManager;
  25. private readonly ILocalizationManager _localization;
  26. private readonly IFileSystem _fileSystem;
  27. private readonly TvdbClientManager _tvdbClientManager;
  28. public MissingEpisodeProvider(
  29. ILogger logger,
  30. IServerConfigurationManager config,
  31. ILibraryManager libraryManager,
  32. ILocalizationManager localization,
  33. IFileSystem fileSystem,
  34. TvdbClientManager tvdbClientManager)
  35. {
  36. _logger = logger;
  37. _config = config;
  38. _libraryManager = libraryManager;
  39. _localization = localization;
  40. _fileSystem = fileSystem;
  41. _tvdbClientManager = tvdbClientManager;
  42. }
  43. public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
  44. {
  45. var tvdbId = series.GetProviderId(MetadataProvider.Tvdb);
  46. if (string.IsNullOrEmpty(tvdbId))
  47. {
  48. return false;
  49. }
  50. var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken);
  51. var episodeLookup = episodes
  52. .Select(i =>
  53. {
  54. DateTime.TryParse(i.FirstAired, out var firstAired);
  55. var seasonNumber = i.AiredSeason.GetValueOrDefault(-1);
  56. var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1);
  57. return (seasonNumber, episodeNumber, firstAired);
  58. })
  59. .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1)
  60. .OrderBy(i => i.seasonNumber)
  61. .ThenBy(i => i.episodeNumber)
  62. .ToList();
  63. var allRecursiveChildren = series.GetRecursiveChildren();
  64. var hasBadData = HasInvalidContent(allRecursiveChildren);
  65. // Be conservative here to avoid creating missing episodes for ones they already have
  66. var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes;
  67. var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup);
  68. if (anySeasonsRemoved)
  69. {
  70. // refresh this
  71. allRecursiveChildren = series.GetRecursiveChildren();
  72. }
  73. var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes);
  74. if (anyEpisodesRemoved)
  75. {
  76. // refresh this
  77. allRecursiveChildren = series.GetRecursiveChildren();
  78. }
  79. var hasNewEpisodes = false;
  80. if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name))
  81. {
  82. hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken)
  83. .ConfigureAwait(false);
  84. }
  85. if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
  86. {
  87. return true;
  88. }
  89. return false;
  90. }
  91. /// <summary>
  92. /// Returns true if a series has any seasons or episodes without season or episode numbers
  93. /// If this data is missing no virtual items will be added in order to prevent possible duplicates
  94. /// </summary>
  95. private bool HasInvalidContent(IList<BaseItem> allItems)
  96. {
  97. return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) ||
  98. allItems.OfType<Episode>().Any(i =>
  99. {
  100. if (!i.ParentIndexNumber.HasValue)
  101. {
  102. return true;
  103. }
  104. // You could have episodes under season 0 with no number
  105. return false;
  106. });
  107. }
  108. private async Task<bool> AddMissingEpisodes(
  109. Series series,
  110. IEnumerable<BaseItem> allItems,
  111. bool addMissingEpisodes,
  112. IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup,
  113. CancellationToken cancellationToken)
  114. {
  115. var existingEpisodes = allItems.OfType<Episode>().ToList();
  116. var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count());
  117. var hasChanges = false;
  118. foreach (var tuple in episodeLookup)
  119. {
  120. if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0)
  121. {
  122. // Ignore episode/season zeros
  123. continue;
  124. }
  125. var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple);
  126. if (existingEpisode != null)
  127. {
  128. continue;
  129. }
  130. var airDate = tuple.firstAired;
  131. var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays);
  132. if (airDate < now && addMissingEpisodes || airDate > now)
  133. {
  134. // tvdb has a lot of nearly blank episodes
  135. _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber);
  136. await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false);
  137. hasChanges = true;
  138. }
  139. }
  140. return hasChanges;
  141. }
  142. /// <summary>
  143. /// Removes the virtual entry after a corresponding physical version has been added
  144. /// </summary>
  145. private bool RemoveObsoleteOrMissingEpisodes(
  146. IEnumerable<BaseItem> allRecursiveChildren,
  147. IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup,
  148. bool allowMissingEpisodes)
  149. {
  150. var existingEpisodes = allRecursiveChildren.OfType<Episode>();
  151. var physicalEpisodes = new List<Episode>();
  152. var virtualEpisodes = new List<Episode>();
  153. foreach (var episode in existingEpisodes)
  154. {
  155. if (episode.LocationType == LocationType.Virtual)
  156. {
  157. virtualEpisodes.Add(episode);
  158. }
  159. else
  160. {
  161. physicalEpisodes.Add(episode);
  162. }
  163. }
  164. var episodesToRemove = virtualEpisodes
  165. .Where(i =>
  166. {
  167. if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue)
  168. {
  169. return true;
  170. }
  171. var seasonNumber = i.ParentIndexNumber.Value;
  172. var episodeNumber = i.IndexNumber.Value;
  173. // If there's a physical episode with the same season and episode number, delete it
  174. if (physicalEpisodes.Any(p =>
  175. p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber &&
  176. p.ContainsEpisodeNumber(episodeNumber)))
  177. {
  178. return true;
  179. }
  180. // If the episode no longer exists in the remote lookup, delete it
  181. if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber))
  182. {
  183. return true;
  184. }
  185. // If it's missing, but not unaired, remove it
  186. return !allowMissingEpisodes && i.IsMissingEpisode &&
  187. (!i.PremiereDate.HasValue ||
  188. i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) <
  189. DateTime.Now.Date);
  190. });
  191. var hasChanges = false;
  192. foreach (var episodeToRemove in episodesToRemove)
  193. {
  194. _libraryManager.DeleteItem(episodeToRemove, new DeleteOptions
  195. {
  196. DeleteFileLocation = true
  197. }, false);
  198. hasChanges = true;
  199. }
  200. return hasChanges;
  201. }
  202. /// <summary>
  203. /// Removes the obsolete or missing seasons.
  204. /// </summary>
  205. /// <param name="allRecursiveChildren"></param>
  206. /// <param name="episodeLookup">The episode lookup.</param>
  207. /// <returns><see cref="bool" />.</returns>
  208. private bool RemoveObsoleteOrMissingSeasons(
  209. IList<BaseItem> allRecursiveChildren,
  210. IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup)
  211. {
  212. var existingSeasons = allRecursiveChildren.OfType<Season>().ToList();
  213. var physicalSeasons = new List<Season>();
  214. var virtualSeasons = new List<Season>();
  215. foreach (var season in existingSeasons)
  216. {
  217. if (season.LocationType == LocationType.Virtual)
  218. {
  219. virtualSeasons.Add(season);
  220. }
  221. else
  222. {
  223. physicalSeasons.Add(season);
  224. }
  225. }
  226. var allEpisodes = allRecursiveChildren.OfType<Episode>().ToList();
  227. var seasonsToRemove = virtualSeasons
  228. .Where(i =>
  229. {
  230. if (i.IndexNumber.HasValue)
  231. {
  232. var seasonNumber = i.IndexNumber.Value;
  233. // If there's a physical season with the same number, delete it
  234. if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal)))
  235. {
  236. return true;
  237. }
  238. // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it
  239. return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder);
  240. }
  241. // Season does not have a number
  242. // Remove if there are no episodes directly in series without a season number
  243. return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder);
  244. });
  245. var hasChanges = false;
  246. foreach (var seasonToRemove in seasonsToRemove)
  247. {
  248. _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions
  249. {
  250. DeleteFileLocation = true
  251. }, false);
  252. hasChanges = true;
  253. }
  254. return hasChanges;
  255. }
  256. /// <summary>
  257. /// Adds the episode.
  258. /// </summary>
  259. /// <param name="series">The series.</param>
  260. /// <param name="seasonNumber">The season number.</param>
  261. /// <param name="episodeNumber">The episode number.</param>
  262. /// <param name="cancellationToken">The cancellation token.</param>
  263. /// <returns>Task.</returns>
  264. private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
  265. {
  266. var season = series.Children.OfType<Season>()
  267. .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
  268. if (season == null)
  269. {
  270. var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem);
  271. season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false);
  272. }
  273. var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture);
  274. var episode = new Episode
  275. {
  276. Name = name,
  277. IndexNumber = episodeNumber,
  278. ParentIndexNumber = seasonNumber,
  279. Id = _libraryManager.GetNewItemId(
  280. series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name,
  281. typeof(Episode)),
  282. IsVirtualItem = true,
  283. SeasonId = season?.Id ?? Guid.Empty,
  284. SeriesId = series.Id
  285. };
  286. season.AddChild(episode, cancellationToken);
  287. await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
  288. }
  289. /// <summary>
  290. /// Gets the existing episode.
  291. /// </summary>
  292. /// <param name="existingEpisodes">The existing episodes.</param>
  293. /// <param name="seasonCounts"></param>
  294. /// <param name="episodeTuple"></param>
  295. /// <returns>Episode.</returns>
  296. private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple)
  297. {
  298. var seasonNumber = episodeTuple.seasonNumber;
  299. var episodeNumber = episodeTuple.episodeNumber;
  300. while (true)
  301. {
  302. var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber);
  303. if (episode != null)
  304. {
  305. return episode;
  306. }
  307. seasonNumber--;
  308. if (seasonCounts.ContainsKey(seasonNumber))
  309. {
  310. episodeNumber += seasonCounts[seasonNumber];
  311. }
  312. else
  313. {
  314. break;
  315. }
  316. }
  317. return null;
  318. }
  319. private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, int season, int episode)
  320. => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode));
  321. }
  322. }