MissingEpisodeProvider.cs 15 KB

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