SeriesPostScanTask.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.IO;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Model.Entities;
  7. using MediaBrowser.Model.Logging;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Globalization;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using System.Xml;
  17. namespace MediaBrowser.Providers.TV
  18. {
  19. class SeriesPostScanTask : ILibraryPostScanTask
  20. {
  21. /// <summary>
  22. /// The _library manager
  23. /// </summary>
  24. private readonly ILibraryManager _libraryManager;
  25. private readonly IServerConfigurationManager _config;
  26. private readonly ILogger _logger;
  27. private readonly IDirectoryWatchers _directoryWatchers;
  28. public SeriesPostScanTask(ILibraryManager libraryManager, ILogger logger, IDirectoryWatchers directoryWatchers, IServerConfigurationManager config)
  29. {
  30. _libraryManager = libraryManager;
  31. _logger = logger;
  32. _directoryWatchers = directoryWatchers;
  33. _config = config;
  34. }
  35. public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
  36. {
  37. return RunInternal(progress, cancellationToken);
  38. }
  39. private async Task RunInternal(IProgress<double> progress, CancellationToken cancellationToken)
  40. {
  41. var seriesList = _libraryManager.RootFolder
  42. .RecursiveChildren
  43. .OfType<Series>()
  44. .ToList();
  45. var numComplete = 0;
  46. foreach (var series in seriesList)
  47. {
  48. cancellationToken.ThrowIfCancellationRequested();
  49. await new MissingEpisodeProvider(_logger, _directoryWatchers, _config).Run(series, cancellationToken).ConfigureAwait(false);
  50. var episodes = series.RecursiveChildren
  51. .OfType<Episode>()
  52. .ToList();
  53. series.SpecialFeatureIds = episodes
  54. .Where(i => i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
  55. .Select(i => i.Id)
  56. .ToList();
  57. series.SeasonCount = episodes
  58. .Select(i => i.ParentIndexNumber ?? 0)
  59. .Where(i => i != 0)
  60. .Distinct()
  61. .Count();
  62. series.DateLastEpisodeAdded = episodes.Select(i => i.DateCreated)
  63. .OrderByDescending(i => i)
  64. .FirstOrDefault();
  65. numComplete++;
  66. double percent = numComplete;
  67. percent /= seriesList.Count;
  68. percent *= 100;
  69. progress.Report(percent);
  70. }
  71. }
  72. }
  73. class MissingEpisodeProvider
  74. {
  75. private readonly IServerConfigurationManager _config;
  76. private readonly ILogger _logger;
  77. private readonly IDirectoryWatchers _directoryWatchers;
  78. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  79. public MissingEpisodeProvider(ILogger logger, IDirectoryWatchers directoryWatchers, IServerConfigurationManager config)
  80. {
  81. _logger = logger;
  82. _directoryWatchers = directoryWatchers;
  83. _config = config;
  84. }
  85. public async Task Run(Series series, CancellationToken cancellationToken)
  86. {
  87. var tvdbId = series.GetProviderId(MetadataProviders.Tvdb);
  88. // Can't proceed without a tvdb id
  89. if (string.IsNullOrEmpty(tvdbId))
  90. {
  91. return;
  92. }
  93. var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
  94. var episodeFiles = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly)
  95. .Select(Path.GetFileNameWithoutExtension)
  96. .Where(i => i.StartsWith("episode-", StringComparison.OrdinalIgnoreCase))
  97. .ToList();
  98. var episodeLookup = episodeFiles
  99. .Select(i =>
  100. {
  101. var parts = i.Split('-');
  102. if (parts.Length == 3)
  103. {
  104. int seasonNumber;
  105. if (int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out seasonNumber))
  106. {
  107. int episodeNumber;
  108. if (int.TryParse(parts[2], NumberStyles.Integer, UsCulture, out episodeNumber))
  109. {
  110. return new Tuple<int, int>(seasonNumber, episodeNumber);
  111. }
  112. }
  113. }
  114. return new Tuple<int, int>(-1, -1);
  115. })
  116. .Where(i => i.Item1 != -1 && i.Item2 != -1)
  117. .ToList();
  118. var hasChanges = false;
  119. if (_config.Configuration.CreateVirtualMissingEpisodes || _config.Configuration.CreateVirtualFutureEpisodes)
  120. {
  121. if (_config.Configuration.EnableInternetProviders)
  122. {
  123. hasChanges = await AddMissingEpisodes(series, seriesDataPath, episodeLookup, cancellationToken).ConfigureAwait(false);
  124. }
  125. }
  126. var anyRemoved = await RemoveObsoleteMissingEpisodes(series, cancellationToken).ConfigureAwait(false);
  127. if (hasChanges || anyRemoved)
  128. {
  129. await series.RefreshMetadata(cancellationToken, true).ConfigureAwait(false);
  130. await series.ValidateChildren(new Progress<double>(), cancellationToken, true).ConfigureAwait(false);
  131. }
  132. }
  133. /// <summary>
  134. /// Adds the missing episodes.
  135. /// </summary>
  136. /// <param name="series">The series.</param>
  137. /// <param name="seriesDataPath">The series data path.</param>
  138. /// <param name="episodeLookup">The episode lookup.</param>
  139. /// <param name="cancellationToken">The cancellation token.</param>
  140. /// <returns>Task.</returns>
  141. private async Task<bool> AddMissingEpisodes(Series series, string seriesDataPath, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
  142. {
  143. var existingEpisodes = series.RecursiveChildren
  144. .OfType<Episode>()
  145. .ToList();
  146. var hasChanges = false;
  147. foreach (var tuple in episodeLookup)
  148. {
  149. if (tuple.Item1 <= 0)
  150. {
  151. // Ignore season zeros
  152. continue;
  153. }
  154. if (tuple.Item2 <= 0)
  155. {
  156. // Ignore episode zeros
  157. continue;
  158. }
  159. var existingEpisode = GetExistingEpisode(existingEpisodes, tuple);
  160. if (existingEpisode != null)
  161. {
  162. continue;
  163. }
  164. var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2);
  165. if (!airDate.HasValue)
  166. {
  167. continue;
  168. }
  169. var now = DateTime.UtcNow;
  170. if (airDate.Value < now && _config.Configuration.CreateVirtualMissingEpisodes)
  171. {
  172. // tvdb has a lot of nearly blank episodes
  173. _logger.Info("Creating virtual missing episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2);
  174. await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
  175. hasChanges = true;
  176. }
  177. else if (airDate.Value > now && _config.Configuration.CreateVirtualFutureEpisodes)
  178. {
  179. // tvdb has a lot of nearly blank episodes
  180. _logger.Info("Creating virtual future episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2);
  181. await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
  182. hasChanges = true;
  183. }
  184. }
  185. return hasChanges;
  186. }
  187. /// <summary>
  188. /// Removes the virtual entry after a corresponding physical version has been added
  189. /// </summary>
  190. private async Task<bool> RemoveObsoleteMissingEpisodes(Series series, CancellationToken cancellationToken)
  191. {
  192. var existingEpisodes = series.RecursiveChildren
  193. .OfType<Episode>()
  194. .ToList();
  195. var physicalEpisodes = existingEpisodes
  196. .Where(i => i.LocationType != LocationType.Virtual)
  197. .ToList();
  198. var virtualEpisodes = existingEpisodes
  199. .Where(i => i.LocationType == LocationType.Virtual)
  200. .ToList();
  201. var episodesToRemove = virtualEpisodes
  202. .Where(i =>
  203. {
  204. if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue)
  205. {
  206. return physicalEpisodes.Any(p => p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == i.ParentIndexNumber.Value && p.ContainsEpisodeNumber(i.IndexNumber.Value));
  207. }
  208. return false;
  209. })
  210. .ToList();
  211. var hasChanges = false;
  212. foreach (var episodeToRemove in episodesToRemove)
  213. {
  214. _logger.Info("Removing {0} {1}x{2}", series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
  215. await episodeToRemove.Parent.RemoveChild(episodeToRemove, cancellationToken).ConfigureAwait(false);
  216. hasChanges = true;
  217. }
  218. return hasChanges;
  219. }
  220. /// <summary>
  221. /// Adds the episode.
  222. /// </summary>
  223. /// <param name="series">The series.</param>
  224. /// <param name="seasonNumber">The season number.</param>
  225. /// <param name="episodeNumber">The episode number.</param>
  226. /// <param name="cancellationToken">The cancellation token.</param>
  227. /// <returns>Task.</returns>
  228. private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
  229. {
  230. var season = series.Children.OfType<Season>()
  231. .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
  232. if (season == null)
  233. {
  234. season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
  235. }
  236. var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture));
  237. var episode = new Episode
  238. {
  239. Name = name,
  240. IndexNumber = episodeNumber,
  241. ParentIndexNumber = seasonNumber,
  242. Parent = season,
  243. DisplayMediaType = typeof(Episode).Name,
  244. Id = (series.Id + seasonNumber.ToString(UsCulture) + name).GetMBId(typeof(Episode))
  245. };
  246. await season.AddChild(episode, cancellationToken).ConfigureAwait(false);
  247. await episode.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  248. }
  249. /// <summary>
  250. /// Adds the season.
  251. /// </summary>
  252. /// <param name="series">The series.</param>
  253. /// <param name="seasonNumber">The season number.</param>
  254. /// <param name="cancellationToken">The cancellation token.</param>
  255. /// <returns>Task{Season}.</returns>
  256. private async Task<Season> AddSeason(Series series, int seasonNumber, CancellationToken cancellationToken)
  257. {
  258. _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name);
  259. var name = string.Format("Season {0}", seasonNumber.ToString(UsCulture));
  260. var path = Path.Combine(series.Path, name);
  261. var season = new Season
  262. {
  263. Name = name,
  264. IndexNumber = seasonNumber,
  265. Path = path,
  266. Parent = series,
  267. DisplayMediaType = typeof(Season).Name
  268. };
  269. _directoryWatchers.TemporarilyIgnore(path);
  270. try
  271. {
  272. var info = Directory.CreateDirectory(path);
  273. season.DateCreated = info.CreationTimeUtc;
  274. season.DateModified = info.LastWriteTimeUtc;
  275. await series.AddChild(season, cancellationToken).ConfigureAwait(false);
  276. await season.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  277. }
  278. finally
  279. {
  280. _directoryWatchers.RemoveTempIgnore(path);
  281. }
  282. return season;
  283. }
  284. /// <summary>
  285. /// Gets the existing episode.
  286. /// </summary>
  287. /// <param name="existingEpisodes">The existing episodes.</param>
  288. /// <param name="tuple">The tuple.</param>
  289. /// <returns>Episode.</returns>
  290. private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, Tuple<int, int> tuple)
  291. {
  292. return existingEpisodes
  293. .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2));
  294. }
  295. /// <summary>
  296. /// Gets the air date.
  297. /// </summary>
  298. /// <param name="seriesDataPath">The series data path.</param>
  299. /// <param name="seasonNumber">The season number.</param>
  300. /// <param name="episodeNumber">The episode number.</param>
  301. /// <returns>System.Nullable{DateTime}.</returns>
  302. private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber)
  303. {
  304. // First open up the tvdb xml file and make sure it has valid data
  305. var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(UsCulture), episodeNumber.ToString(UsCulture));
  306. var xmlPath = Path.Combine(seriesDataPath, filename);
  307. DateTime? airDate = null;
  308. // It appears the best way to filter out invalid entries is to only include those with valid air dates
  309. using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
  310. {
  311. // Use XmlReader for best performance
  312. using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
  313. {
  314. CheckCharacters = false,
  315. IgnoreProcessingInstructions = true,
  316. IgnoreComments = true,
  317. ValidationType = ValidationType.None
  318. }))
  319. {
  320. reader.MoveToContent();
  321. // Loop through each element
  322. while (reader.Read())
  323. {
  324. if (reader.NodeType == XmlNodeType.Element)
  325. {
  326. switch (reader.Name)
  327. {
  328. case "EpisodeName":
  329. {
  330. var val = reader.ReadElementContentAsString();
  331. if (string.IsNullOrWhiteSpace(val))
  332. {
  333. // Not valid, ignore these
  334. return null;
  335. }
  336. break;
  337. }
  338. case "FirstAired":
  339. {
  340. var val = reader.ReadElementContentAsString();
  341. if (!string.IsNullOrWhiteSpace(val))
  342. {
  343. DateTime date;
  344. if (DateTime.TryParse(val, out date))
  345. {
  346. airDate = date.ToUniversalTime();
  347. }
  348. }
  349. break;
  350. }
  351. default:
  352. reader.Skip();
  353. break;
  354. }
  355. }
  356. }
  357. }
  358. }
  359. return airDate;
  360. }
  361. }
  362. }