SeriesPostScanTask.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. var seasonNumberString = parts[1];
  105. int seasonNumber;
  106. if (int.TryParse(seasonNumberString, NumberStyles.Integer, UsCulture, out seasonNumber))
  107. {
  108. var episodeNumberString = parts[2];
  109. int episodeNumber;
  110. if (int.TryParse(episodeNumberString, NumberStyles.Integer, UsCulture, out episodeNumber))
  111. {
  112. return new Tuple<int, int>(seasonNumber, episodeNumber);
  113. }
  114. }
  115. }
  116. return new Tuple<int, int>(-1, -1);
  117. })
  118. .Where(i => i.Item1 != -1 && i.Item2 != -1)
  119. .ToList();
  120. var existingEpisodes = series.RecursiveChildren
  121. .OfType<Episode>()
  122. .Where(i => i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue)
  123. .ToList();
  124. var hasChanges = false;
  125. if (_config.Configuration.CreateVirtualMissingEpisodes || _config.Configuration.CreateVirtualFutureEpisodes)
  126. {
  127. if (_config.Configuration.EnableInternetProviders)
  128. {
  129. hasChanges = await AddMissingEpisodes(series, seriesDataPath, existingEpisodes, episodeLookup, cancellationToken).ConfigureAwait(false);
  130. }
  131. }
  132. var anyRemoved = await RemoveObsoleteMissingEpsiodes(series, existingEpisodes, cancellationToken).ConfigureAwait(false);
  133. if (hasChanges || anyRemoved)
  134. {
  135. await series.RefreshMetadata(cancellationToken, true).ConfigureAwait(false);
  136. await series.ValidateChildren(new Progress<double>(), cancellationToken, true).ConfigureAwait(false);
  137. }
  138. }
  139. /// <summary>
  140. /// Adds the missing episodes.
  141. /// </summary>
  142. /// <param name="series">The series.</param>
  143. /// <param name="seriesDataPath">The series data path.</param>
  144. /// <param name="existingEpisodes">The existing episodes.</param>
  145. /// <param name="episodeLookup">The episode lookup.</param>
  146. /// <param name="cancellationToken">The cancellation token.</param>
  147. /// <returns>Task.</returns>
  148. private async Task<bool> AddMissingEpisodes(Series series, string seriesDataPath, List<Episode> existingEpisodes, IEnumerable<Tuple<int, int>> episodeLookup, CancellationToken cancellationToken)
  149. {
  150. var hasChanges = false;
  151. foreach (var tuple in episodeLookup)
  152. {
  153. if (tuple.Item1 <= 0)
  154. {
  155. // Ignore season zeros
  156. continue;
  157. }
  158. if (tuple.Item2 <= 0)
  159. {
  160. // Ignore episode zeros
  161. continue;
  162. }
  163. var existingEpisode = GetExistingEpisode(existingEpisodes, tuple);
  164. if (existingEpisode != null)
  165. {
  166. continue;
  167. }
  168. var airDate = GetAirDate(seriesDataPath, tuple.Item1, tuple.Item2);
  169. if (!airDate.HasValue)
  170. {
  171. continue;
  172. }
  173. if (airDate.Value < DateTime.UtcNow && _config.Configuration.CreateVirtualMissingEpisodes)
  174. {
  175. // tvdb has a lot of nearly blank episodes
  176. _logger.Info("Creating virtual missing episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2);
  177. await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
  178. hasChanges = true;
  179. }
  180. else if (airDate.Value > DateTime.UtcNow && _config.Configuration.CreateVirtualFutureEpisodes)
  181. {
  182. // tvdb has a lot of nearly blank episodes
  183. _logger.Info("Creating virtual future episode {0} {1}x{2}", series.Name, tuple.Item1, tuple.Item2);
  184. await AddEpisode(series, tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
  185. hasChanges = true;
  186. }
  187. }
  188. return hasChanges;
  189. }
  190. /// <summary>
  191. /// Removes the virtual entry after a corresponding physical version has been added
  192. /// </summary>
  193. private async Task<bool> RemoveObsoleteMissingEpsiodes(Series series, List<Episode> existingEpisodes, CancellationToken cancellationToken)
  194. {
  195. var physicalEpisodes = existingEpisodes
  196. .Where(i => i.LocationType != LocationType.Virtual)
  197. .ToList();
  198. var episodesToRemove = existingEpisodes
  199. .Where(i => i.LocationType == LocationType.Virtual)
  200. .Where(i =>
  201. {
  202. if (i.IndexNumber.HasValue && i.ParentIndexNumber.HasValue)
  203. {
  204. return physicalEpisodes.Any(p => p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == i.ParentIndexNumber.Value && p.ContainsEpisodeNumber(i.IndexNumber.Value));
  205. }
  206. return false;
  207. })
  208. .ToList();
  209. var hasChanges = false;
  210. foreach (var episodeToRemove in episodesToRemove)
  211. {
  212. _logger.Info("Removing {0} {1}x{2}", series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
  213. await episodeToRemove.Parent.RemoveChild(episodeToRemove, cancellationToken).ConfigureAwait(false);
  214. hasChanges = true;
  215. }
  216. return hasChanges;
  217. }
  218. /// <summary>
  219. /// Adds the episode.
  220. /// </summary>
  221. /// <param name="series">The series.</param>
  222. /// <param name="seasonNumber">The season number.</param>
  223. /// <param name="episodeNumber">The episode number.</param>
  224. /// <param name="cancellationToken">The cancellation token.</param>
  225. /// <returns>Task.</returns>
  226. private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
  227. {
  228. var season = series.Children.OfType<Season>().FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
  229. if (season == null)
  230. {
  231. season = await AddSeason(series, seasonNumber, cancellationToken).ConfigureAwait(false);
  232. }
  233. var name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture));
  234. var episode = new Episode
  235. {
  236. Name = string.Format("Episode {0}", episodeNumber.ToString(UsCulture)),
  237. IndexNumber = episodeNumber,
  238. ParentIndexNumber = seasonNumber,
  239. Parent = season,
  240. DisplayMediaType = typeof(Episode).Name,
  241. Id = (series.Id + name).GetMBId(typeof(Episode))
  242. };
  243. await season.AddChild(episode, cancellationToken).ConfigureAwait(false);
  244. await episode.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  245. }
  246. /// <summary>
  247. /// Adds the season.
  248. /// </summary>
  249. /// <param name="series">The series.</param>
  250. /// <param name="seasonNumber">The season number.</param>
  251. /// <param name="cancellationToken">The cancellation token.</param>
  252. /// <returns>Task{Season}.</returns>
  253. private async Task<Season> AddSeason(Series series, int seasonNumber, CancellationToken cancellationToken)
  254. {
  255. _logger.Info("Creating Season {0} entry for {1}", seasonNumber, series.Name);
  256. var name = string.Format("Season {0}", seasonNumber.ToString(UsCulture));
  257. var path = Path.Combine(series.Path, name);
  258. var season = new Season
  259. {
  260. Name = name,
  261. IndexNumber = seasonNumber,
  262. Path = path,
  263. Parent = series,
  264. DisplayMediaType = typeof(Season).Name
  265. };
  266. _directoryWatchers.TemporarilyIgnore(path);
  267. try
  268. {
  269. var info = Directory.CreateDirectory(path);
  270. season.DateCreated = info.CreationTimeUtc;
  271. season.DateModified = info.LastWriteTimeUtc;
  272. await series.AddChild(season, cancellationToken).ConfigureAwait(false);
  273. await season.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  274. }
  275. finally
  276. {
  277. _directoryWatchers.RemoveTempIgnore(path);
  278. }
  279. return season;
  280. }
  281. /// <summary>
  282. /// Gets the existing episode.
  283. /// </summary>
  284. /// <param name="existingEpisodes">The existing episodes.</param>
  285. /// <param name="tuple">The tuple.</param>
  286. /// <returns>Episode.</returns>
  287. private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, Tuple<int, int> tuple)
  288. {
  289. return existingEpisodes
  290. .FirstOrDefault(i => (i.ParentIndexNumber ?? -1) == tuple.Item1 && i.ContainsEpisodeNumber(tuple.Item2));
  291. }
  292. /// <summary>
  293. /// Gets the air date.
  294. /// </summary>
  295. /// <param name="seriesDataPath">The series data path.</param>
  296. /// <param name="seasonNumber">The season number.</param>
  297. /// <param name="episodeNumber">The episode number.</param>
  298. /// <returns>System.Nullable{DateTime}.</returns>
  299. private DateTime? GetAirDate(string seriesDataPath, int seasonNumber, int episodeNumber)
  300. {
  301. // First open up the tvdb xml file and make sure it has valid data
  302. var filename = string.Format("episode-{0}-{1}.xml", seasonNumber.ToString(UsCulture), episodeNumber.ToString(UsCulture));
  303. var xmlPath = Path.Combine(seriesDataPath, filename);
  304. // It appears the best way to filter out invalid entries is to only include those with valid air dates
  305. using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
  306. {
  307. // Use XmlReader for best performance
  308. using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
  309. {
  310. CheckCharacters = false,
  311. IgnoreProcessingInstructions = true,
  312. IgnoreComments = true,
  313. ValidationType = ValidationType.None
  314. }))
  315. {
  316. reader.MoveToContent();
  317. // Loop through each element
  318. while (reader.Read())
  319. {
  320. if (reader.NodeType == XmlNodeType.Element)
  321. {
  322. switch (reader.Name)
  323. {
  324. case "FirstAired":
  325. {
  326. var val = reader.ReadElementContentAsString();
  327. if (!string.IsNullOrWhiteSpace(val))
  328. {
  329. DateTime date;
  330. if (DateTime.TryParse(val, out date))
  331. {
  332. return date.ToUniversalTime();
  333. }
  334. }
  335. break;
  336. }
  337. default:
  338. reader.Skip();
  339. break;
  340. }
  341. }
  342. }
  343. }
  344. }
  345. return null;
  346. }
  347. }
  348. }