TvdbPrescanTask.cs 16 KB

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