TvdbPrescanTask.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. using MediaBrowser.Common.Net;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Model.Entities;
  6. using MediaBrowser.Model.Logging;
  7. using MediaBrowser.Model.Net;
  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. using CommonIO;
  18. using MediaBrowser.Controller.Entities;
  19. namespace MediaBrowser.Providers.TV
  20. {
  21. /// <summary>
  22. /// Class TvdbPrescanTask
  23. /// </summary>
  24. public class TvdbPrescanTask : ILibraryPostScanTask
  25. {
  26. /// <summary>
  27. /// The server time URL
  28. /// </summary>
  29. private const string ServerTimeUrl = "https://thetvdb.com/api/Updates.php?type=none";
  30. /// <summary>
  31. /// The updates URL
  32. /// </summary>
  33. private const string UpdatesUrl = "https://thetvdb.com/api/Updates.php?type=all&time={0}";
  34. /// <summary>
  35. /// The _HTTP client
  36. /// </summary>
  37. private readonly IHttpClient _httpClient;
  38. /// <summary>
  39. /// The _logger
  40. /// </summary>
  41. private readonly ILogger _logger;
  42. /// <summary>
  43. /// The _config
  44. /// </summary>
  45. private readonly IServerConfigurationManager _config;
  46. private readonly IFileSystem _fileSystem;
  47. private readonly ILibraryManager _libraryManager;
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="TvdbPrescanTask"/> class.
  50. /// </summary>
  51. /// <param name="logger">The logger.</param>
  52. /// <param name="httpClient">The HTTP client.</param>
  53. /// <param name="config">The config.</param>
  54. public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager)
  55. {
  56. _logger = logger;
  57. _httpClient = httpClient;
  58. _config = config;
  59. _fileSystem = fileSystem;
  60. _libraryManager = libraryManager;
  61. }
  62. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  63. /// <summary>
  64. /// Runs the specified progress.
  65. /// </summary>
  66. /// <param name="progress">The progress.</param>
  67. /// <param name="cancellationToken">The cancellation token.</param>
  68. /// <returns>Task.</returns>
  69. public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
  70. {
  71. if (!_config.Configuration.EnableInternetProviders)
  72. {
  73. progress.Report(100);
  74. return;
  75. }
  76. var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase));
  77. if (seriesConfig != null && seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase))
  78. {
  79. progress.Report(100);
  80. return;
  81. }
  82. var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths);
  83. _fileSystem.CreateDirectory(path);
  84. var timestampFile = Path.Combine(path, "time.txt");
  85. var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile);
  86. // Don't check for tvdb updates anymore frequently than 24 hours
  87. if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1)
  88. {
  89. return;
  90. }
  91. // Find out the last time we queried tvdb for updates
  92. var lastUpdateTime = timestampFileInfo.Exists ? _fileSystem.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty;
  93. string newUpdateTime;
  94. var existingDirectories = Directory.EnumerateDirectories(path)
  95. .Select(Path.GetFileName)
  96. .ToList();
  97. var seriesList = _libraryManager.GetItemList(new InternalItemsQuery()
  98. {
  99. IncludeItemTypes = new[] { typeof(Series).Name },
  100. Recursive = true,
  101. GroupByPresentationUniqueKey = false
  102. }).Cast<Series>();
  103. var seriesIdsInLibrary = seriesList
  104. .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb)))
  105. .Select(i => i.GetProviderId(MetadataProviders.Tvdb))
  106. .ToList();
  107. var missingSeries = seriesIdsInLibrary.Except(existingDirectories, StringComparer.OrdinalIgnoreCase)
  108. .ToList();
  109. // If this is our first time, update all series
  110. if (string.IsNullOrEmpty(lastUpdateTime))
  111. {
  112. // First get tvdb server time
  113. using (var stream = await _httpClient.Get(new HttpRequestOptions
  114. {
  115. Url = ServerTimeUrl,
  116. CancellationToken = cancellationToken,
  117. EnableHttpCompression = true,
  118. ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
  119. }).ConfigureAwait(false))
  120. {
  121. newUpdateTime = GetUpdateTime(stream);
  122. }
  123. existingDirectories.AddRange(missingSeries);
  124. await UpdateSeries(existingDirectories, path, null, progress, cancellationToken).ConfigureAwait(false);
  125. }
  126. else
  127. {
  128. var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false);
  129. newUpdateTime = seriesToUpdate.Item2;
  130. long lastUpdateValue;
  131. long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out lastUpdateValue);
  132. var nullableUpdateValue = lastUpdateValue == 0 ? (long?)null : lastUpdateValue;
  133. var listToUpdate = seriesToUpdate.Item1.ToList();
  134. listToUpdate.AddRange(missingSeries);
  135. await UpdateSeries(listToUpdate, path, nullableUpdateValue, progress, cancellationToken).ConfigureAwait(false);
  136. }
  137. _fileSystem.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8);
  138. progress.Report(100);
  139. }
  140. /// <summary>
  141. /// Gets the update time.
  142. /// </summary>
  143. /// <param name="response">The response.</param>
  144. /// <returns>System.String.</returns>
  145. private string GetUpdateTime(Stream response)
  146. {
  147. var settings = new XmlReaderSettings
  148. {
  149. CheckCharacters = false,
  150. IgnoreProcessingInstructions = true,
  151. IgnoreComments = true,
  152. ValidationType = ValidationType.None
  153. };
  154. using (var streamReader = new StreamReader(response, Encoding.UTF8))
  155. {
  156. // Use XmlReader for best performance
  157. using (var reader = XmlReader.Create(streamReader, settings))
  158. {
  159. reader.MoveToContent();
  160. // Loop through each element
  161. while (reader.Read())
  162. {
  163. if (reader.NodeType == XmlNodeType.Element)
  164. {
  165. switch (reader.Name)
  166. {
  167. case "Time":
  168. {
  169. return (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  170. }
  171. default:
  172. reader.Skip();
  173. break;
  174. }
  175. }
  176. }
  177. }
  178. }
  179. return null;
  180. }
  181. /// <summary>
  182. /// Gets the series ids to update.
  183. /// </summary>
  184. /// <param name="existingSeriesIds">The existing series ids.</param>
  185. /// <param name="lastUpdateTime">The last update time.</param>
  186. /// <param name="cancellationToken">The cancellation token.</param>
  187. /// <returns>Task{IEnumerable{System.String}}.</returns>
  188. private async Task<Tuple<IEnumerable<string>, string>> GetSeriesIdsToUpdate(IEnumerable<string> existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken)
  189. {
  190. // First get last time
  191. using (var stream = await _httpClient.Get(new HttpRequestOptions
  192. {
  193. Url = string.Format(UpdatesUrl, lastUpdateTime),
  194. CancellationToken = cancellationToken,
  195. EnableHttpCompression = true,
  196. ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
  197. }).ConfigureAwait(false))
  198. {
  199. var data = GetUpdatedSeriesIdList(stream);
  200. var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
  201. var seriesList = data.Item1
  202. .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i));
  203. return new Tuple<IEnumerable<string>, string>(seriesList, data.Item2);
  204. }
  205. }
  206. private Tuple<List<string>, string> GetUpdatedSeriesIdList(Stream stream)
  207. {
  208. string updateTime = null;
  209. var idList = new List<string>();
  210. var settings = new XmlReaderSettings
  211. {
  212. CheckCharacters = false,
  213. IgnoreProcessingInstructions = true,
  214. IgnoreComments = true,
  215. ValidationType = ValidationType.None
  216. };
  217. using (var streamReader = new StreamReader(stream, Encoding.UTF8))
  218. {
  219. // Use XmlReader for best performance
  220. using (var reader = XmlReader.Create(streamReader, settings))
  221. {
  222. reader.MoveToContent();
  223. // Loop through each element
  224. while (reader.Read())
  225. {
  226. if (reader.NodeType == XmlNodeType.Element)
  227. {
  228. switch (reader.Name)
  229. {
  230. case "Time":
  231. {
  232. updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  233. break;
  234. }
  235. case "Series":
  236. {
  237. var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  238. idList.Add(id);
  239. break;
  240. }
  241. default:
  242. reader.Skip();
  243. break;
  244. }
  245. }
  246. }
  247. }
  248. }
  249. return new Tuple<List<string>, string>(idList, updateTime);
  250. }
  251. /// <summary>
  252. /// Updates the series.
  253. /// </summary>
  254. /// <param name="seriesIds">The series ids.</param>
  255. /// <param name="seriesDataPath">The series data path.</param>
  256. /// <param name="lastTvDbUpdateTime">The last tv db update time.</param>
  257. /// <param name="progress">The progress.</param>
  258. /// <param name="cancellationToken">The cancellation token.</param>
  259. /// <returns>Task.</returns>
  260. private async Task UpdateSeries(IEnumerable<string> seriesIds, string seriesDataPath, long? lastTvDbUpdateTime, IProgress<double> progress, CancellationToken cancellationToken)
  261. {
  262. var list = seriesIds.ToList();
  263. var numComplete = 0;
  264. var seriesList = _libraryManager.GetItemList(new InternalItemsQuery()
  265. {
  266. IncludeItemTypes = new[] { typeof(Series).Name },
  267. Recursive = true,
  268. GroupByPresentationUniqueKey = false
  269. }).Cast<Series>();
  270. // Gather all series into a lookup by tvdb id
  271. var allSeries = seriesList
  272. .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb)))
  273. .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb));
  274. foreach (var seriesId in list)
  275. {
  276. // Find the preferred language(s) for the movie in the library
  277. var languages = allSeries[seriesId]
  278. .Select(i => i.GetPreferredMetadataLanguage())
  279. .Distinct(StringComparer.OrdinalIgnoreCase)
  280. .ToList();
  281. foreach (var language in languages)
  282. {
  283. try
  284. {
  285. await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false);
  286. }
  287. catch (HttpException ex)
  288. {
  289. _logger.ErrorException("Error updating tvdb series id {0}, language {1}", ex, seriesId, language);
  290. // Already logged at lower levels, but don't fail the whole operation, unless timed out
  291. // We have to fail this to make it run again otherwise new episode data could potentially be missing
  292. if (ex.IsTimedOut)
  293. {
  294. throw;
  295. }
  296. }
  297. }
  298. numComplete++;
  299. double percent = numComplete;
  300. percent /= list.Count;
  301. percent *= 100;
  302. progress.Report(percent);
  303. }
  304. }
  305. /// <summary>
  306. /// Updates the series.
  307. /// </summary>
  308. /// <param name="id">The id.</param>
  309. /// <param name="seriesDataPath">The series data path.</param>
  310. /// <param name="lastTvDbUpdateTime">The last tv db update time.</param>
  311. /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
  312. /// <param name="cancellationToken">The cancellation token.</param>
  313. /// <returns>Task.</returns>
  314. private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken)
  315. {
  316. _logger.Info("Updating series from tvdb " + id + ", language " + preferredMetadataLanguage);
  317. seriesDataPath = Path.Combine(seriesDataPath, id);
  318. _fileSystem.CreateDirectory(seriesDataPath);
  319. return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken);
  320. }
  321. }
  322. }