MissingEpisodeProvider.cs 15 KB

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