TvdbSeriesProvider.cs 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Entities.TV;
  6. using MediaBrowser.Controller.Library;
  7. using MediaBrowser.Controller.Providers;
  8. using MediaBrowser.Model.Entities;
  9. using MediaBrowser.Model.IO;
  10. using MediaBrowser.Model.Logging;
  11. using MediaBrowser.Model.Net;
  12. using MediaBrowser.Model.Providers;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.Globalization;
  16. using System.IO;
  17. using System.Linq;
  18. using System.Net;
  19. using System.Text;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. using System.Xml;
  23. using CommonIO;
  24. using MediaBrowser.Common.IO;
  25. namespace MediaBrowser.Providers.TV
  26. {
  27. public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
  28. {
  29. private const string TvdbSeriesOffset = "TvdbSeriesOffset";
  30. private const string TvdbSeriesOffsetFormat = "{0}-{1}";
  31. internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
  32. internal static TvdbSeriesProvider Current { get; private set; }
  33. private readonly IZipClient _zipClient;
  34. private readonly IHttpClient _httpClient;
  35. private readonly IFileSystem _fileSystem;
  36. private readonly IServerConfigurationManager _config;
  37. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  38. private readonly ILogger _logger;
  39. private readonly ILibraryManager _libraryManager;
  40. private readonly IMemoryStreamProvider _memoryStreamProvider;
  41. public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager)
  42. {
  43. _zipClient = zipClient;
  44. _httpClient = httpClient;
  45. _fileSystem = fileSystem;
  46. _config = config;
  47. _logger = logger;
  48. _libraryManager = libraryManager;
  49. Current = this;
  50. }
  51. private const string SeriesSearchUrl = "https://www.thetvdb.com/api/GetSeries.php?seriesname={0}&language={1}";
  52. private const string SeriesGetZip = "https://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
  53. private const string GetSeriesByImdbId = "https://www.thetvdb.com/api/GetSeriesByRemoteID.php?imdbid={0}&language={1}";
  54. private string NormalizeLanguage(string language)
  55. {
  56. if (string.IsNullOrWhiteSpace(language))
  57. {
  58. return language;
  59. }
  60. // pt-br is just pt to tvdb
  61. return language.Split('-')[0].ToLower();
  62. }
  63. public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
  64. {
  65. if (IsValidSeries(searchInfo.ProviderIds))
  66. {
  67. var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
  68. if (metadata.HasMetadata)
  69. {
  70. return new List<RemoteSearchResult>
  71. {
  72. new RemoteSearchResult
  73. {
  74. Name = metadata.Item.Name,
  75. PremiereDate = metadata.Item.PremiereDate,
  76. ProductionYear = metadata.Item.ProductionYear,
  77. ProviderIds = metadata.Item.ProviderIds,
  78. SearchProviderName = Name
  79. }
  80. };
  81. }
  82. }
  83. return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  84. }
  85. public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
  86. {
  87. var result = new MetadataResult<Series>();
  88. result.QueriedById = true;
  89. if (!IsValidSeries(itemId.ProviderIds))
  90. {
  91. result.QueriedById = false;
  92. await Identify(itemId).ConfigureAwait(false);
  93. }
  94. cancellationToken.ThrowIfCancellationRequested();
  95. if (IsValidSeries(itemId.ProviderIds))
  96. {
  97. await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  98. result.Item = new Series();
  99. result.HasMetadata = true;
  100. FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken);
  101. }
  102. return result;
  103. }
  104. internal static int? GetSeriesOffset(Dictionary<string, string> seriesProviderIds)
  105. {
  106. string idString;
  107. if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString))
  108. return null;
  109. var parts = idString.Split('-');
  110. if (parts.Length < 2)
  111. return null;
  112. int offset;
  113. if (int.TryParse(parts[1], out offset))
  114. return offset;
  115. return null;
  116. }
  117. /// <summary>
  118. /// Fetches the series data.
  119. /// </summary>
  120. /// <param name="result">The result.</param>
  121. /// <param name="metadataLanguage">The metadata language.</param>
  122. /// <param name="seriesProviderIds">The series provider ids.</param>
  123. /// <param name="cancellationToken">The cancellation token.</param>
  124. /// <returns>Task{System.Boolean}.</returns>
  125. private void FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
  126. {
  127. var series = result.Item;
  128. string id;
  129. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id))
  130. {
  131. series.SetProviderId(MetadataProviders.Tvdb, id);
  132. }
  133. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id))
  134. {
  135. series.SetProviderId(MetadataProviders.Imdb, id);
  136. }
  137. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  138. var seriesXmlPath = GetSeriesXmlPath(seriesProviderIds, metadataLanguage);
  139. var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
  140. FetchSeriesInfo(result, seriesXmlPath, cancellationToken);
  141. cancellationToken.ThrowIfCancellationRequested();
  142. result.ResetPeople();
  143. FetchActors(result, actorsXmlPath);
  144. }
  145. /// <summary>
  146. /// Downloads the series zip.
  147. /// </summary>
  148. /// <param name="seriesId">The series id.</param>
  149. /// <param name="idType">Type of the identifier.</param>
  150. /// <param name="seriesDataPath">The series data path.</param>
  151. /// <param name="lastTvDbUpdateTime">The last tv database update time.</param>
  152. /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
  153. /// <param name="cancellationToken">The cancellation token.</param>
  154. /// <returns>Task.</returns>
  155. /// <exception cref="System.ArgumentNullException">seriesId</exception>
  156. internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken)
  157. {
  158. if (string.IsNullOrWhiteSpace(seriesId))
  159. {
  160. throw new ArgumentNullException("seriesId");
  161. }
  162. try
  163. {
  164. await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  165. return;
  166. }
  167. catch (HttpException ex)
  168. {
  169. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  170. {
  171. throw;
  172. }
  173. }
  174. if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))
  175. {
  176. await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  177. }
  178. }
  179. private async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken)
  180. {
  181. if (string.IsNullOrWhiteSpace(seriesId))
  182. {
  183. throw new ArgumentNullException("seriesId");
  184. }
  185. if (!string.Equals(idType, "tvdb", StringComparison.OrdinalIgnoreCase))
  186. {
  187. seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  188. }
  189. if (string.IsNullOrWhiteSpace(seriesId))
  190. {
  191. throw new ArgumentNullException("seriesId");
  192. }
  193. var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, NormalizeLanguage(preferredMetadataLanguage));
  194. using (var zipStream = await _httpClient.Get(new HttpRequestOptions
  195. {
  196. Url = url,
  197. ResourcePool = TvDbResourcePool,
  198. CancellationToken = cancellationToken
  199. }).ConfigureAwait(false))
  200. {
  201. // Delete existing files
  202. DeleteXmlFiles(seriesDataPath);
  203. // Copy to memory stream because we need a seekable stream
  204. using (var ms = _memoryStreamProvider.CreateNew())
  205. {
  206. await zipStream.CopyToAsync(ms).ConfigureAwait(false);
  207. ms.Position = 0;
  208. _zipClient.ExtractAllFromZip(ms, seriesDataPath, true);
  209. }
  210. }
  211. // Sanitize all files, except for extracted episode files
  212. foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList()
  213. .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase)))
  214. {
  215. await SanitizeXmlFile(file).ConfigureAwait(false);
  216. }
  217. var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, NormalizeLanguage(preferredMetadataLanguage) + ".xml");
  218. var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml");
  219. if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase))
  220. {
  221. _fileSystem.CopyFile(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true);
  222. }
  223. await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false);
  224. }
  225. private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken)
  226. {
  227. var url = string.Format(GetSeriesByImdbId, id, NormalizeLanguage(language));
  228. using (var result = await _httpClient.Get(new HttpRequestOptions
  229. {
  230. Url = url,
  231. ResourcePool = TvDbResourcePool,
  232. CancellationToken = cancellationToken
  233. }).ConfigureAwait(false))
  234. {
  235. var doc = new XmlDocument();
  236. doc.Load(result);
  237. if (doc.HasChildNodes)
  238. {
  239. var node = doc.SelectSingleNode("//Series/seriesid");
  240. if (node != null)
  241. {
  242. var idResult = node.InnerText;
  243. _logger.Info("Tvdb GetSeriesByRemoteId produced id of {0}", idResult ?? string.Empty);
  244. return idResult;
  245. }
  246. }
  247. }
  248. return null;
  249. }
  250. internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
  251. {
  252. string id;
  253. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id))
  254. {
  255. // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
  256. if (!string.IsNullOrWhiteSpace(id))
  257. {
  258. return true;
  259. }
  260. }
  261. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id))
  262. {
  263. // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
  264. if (!string.IsNullOrWhiteSpace(id))
  265. {
  266. return true;
  267. }
  268. }
  269. return false;
  270. }
  271. private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1);
  272. internal async Task<string> EnsureSeriesInfo(Dictionary<string, string> seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken)
  273. {
  274. await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  275. try
  276. {
  277. string seriesId;
  278. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId))
  279. {
  280. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  281. // Only download if not already there
  282. // The post-scan task will take care of updates so we don't need to re-download here
  283. if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage))
  284. {
  285. await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  286. }
  287. return seriesDataPath;
  288. }
  289. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId))
  290. {
  291. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  292. // Only download if not already there
  293. // The post-scan task will take care of updates so we don't need to re-download here
  294. if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage))
  295. {
  296. await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  297. }
  298. return seriesDataPath;
  299. }
  300. return null;
  301. }
  302. finally
  303. {
  304. _ensureSemaphore.Release();
  305. }
  306. }
  307. private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage)
  308. {
  309. try
  310. {
  311. var files = _fileSystem.GetFiles(seriesDataPath)
  312. .ToList();
  313. var seriesXmlFilename = preferredMetadataLanguage + ".xml";
  314. const int cacheDays = 1;
  315. var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
  316. // No need to check age if automatic updates are enabled
  317. if (seriesFile == null || !seriesFile.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays)
  318. {
  319. return false;
  320. }
  321. var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  322. // No need to check age if automatic updates are enabled
  323. if (actorsXml == null || !actorsXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays)
  324. {
  325. return false;
  326. }
  327. var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  328. // No need to check age if automatic updates are enabled
  329. if (bannersXml == null || !bannersXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays)
  330. {
  331. return false;
  332. }
  333. return true;
  334. }
  335. catch (DirectoryNotFoundException)
  336. {
  337. return false;
  338. }
  339. catch (FileNotFoundException)
  340. {
  341. return false;
  342. }
  343. }
  344. /// <summary>
  345. /// Finds the series.
  346. /// </summary>
  347. /// <param name="name">The name.</param>
  348. /// <param name="year">The year.</param>
  349. /// <param name="language">The language.</param>
  350. /// <param name="cancellationToken">The cancellation token.</param>
  351. /// <returns>Task{System.String}.</returns>
  352. private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
  353. {
  354. var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList();
  355. if (results.Count == 0)
  356. {
  357. var parsedName = _libraryManager.ParseName(name);
  358. var nameWithoutYear = parsedName.Name;
  359. if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
  360. {
  361. results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList();
  362. }
  363. }
  364. return results.Where(i =>
  365. {
  366. if (year.HasValue && i.ProductionYear.HasValue)
  367. {
  368. // Allow one year tolerance
  369. return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
  370. }
  371. return true;
  372. });
  373. }
  374. private async Task<IEnumerable<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
  375. {
  376. var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), NormalizeLanguage(language));
  377. var doc = new XmlDocument();
  378. using (var results = await _httpClient.Get(new HttpRequestOptions
  379. {
  380. Url = url,
  381. ResourcePool = TvDbResourcePool,
  382. CancellationToken = cancellationToken
  383. }).ConfigureAwait(false))
  384. {
  385. doc.Load(results);
  386. }
  387. var searchResults = new List<RemoteSearchResult>();
  388. if (doc.HasChildNodes)
  389. {
  390. var nodes = doc.SelectNodes("//Series");
  391. var comparableName = GetComparableName(name);
  392. if (nodes != null)
  393. {
  394. foreach (XmlNode node in nodes)
  395. {
  396. var searchResult = new RemoteSearchResult
  397. {
  398. SearchProviderName = Name
  399. };
  400. var titles = new List<string>();
  401. var nameNode = node.SelectSingleNode("./SeriesName");
  402. if (nameNode != null)
  403. {
  404. titles.Add(GetComparableName(nameNode.InnerText));
  405. }
  406. var aliasNode = node.SelectSingleNode("./AliasNames");
  407. if (aliasNode != null)
  408. {
  409. var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
  410. titles.AddRange(alias);
  411. }
  412. var imdbIdNode = node.SelectSingleNode("./IMDB_ID");
  413. if (imdbIdNode != null)
  414. {
  415. var val = imdbIdNode.InnerText;
  416. if (!string.IsNullOrWhiteSpace(val))
  417. {
  418. searchResult.SetProviderId(MetadataProviders.Imdb, val);
  419. }
  420. }
  421. var bannerNode = node.SelectSingleNode("./banner");
  422. if (bannerNode != null)
  423. {
  424. var val = bannerNode.InnerText;
  425. if (!string.IsNullOrWhiteSpace(val))
  426. {
  427. searchResult.ImageUrl = TVUtils.BannerUrl + val;
  428. }
  429. }
  430. var airDateNode = node.SelectSingleNode("./FirstAired");
  431. if (airDateNode != null)
  432. {
  433. var val = airDateNode.InnerText;
  434. if (!string.IsNullOrWhiteSpace(val))
  435. {
  436. DateTime date;
  437. if (DateTime.TryParse(val, out date))
  438. {
  439. searchResult.ProductionYear = date.Year;
  440. }
  441. }
  442. }
  443. foreach (var title in titles)
  444. {
  445. if (string.Equals(title, comparableName, StringComparison.OrdinalIgnoreCase))
  446. {
  447. var id = node.SelectSingleNode("./seriesid") ??
  448. node.SelectSingleNode("./id");
  449. if (id != null)
  450. {
  451. searchResult.Name = title;
  452. searchResult.SetProviderId(MetadataProviders.Tvdb, id.InnerText);
  453. searchResults.Add(searchResult);
  454. }
  455. break;
  456. }
  457. _logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
  458. }
  459. }
  460. }
  461. }
  462. if (searchResults.Count == 0)
  463. {
  464. _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
  465. }
  466. return searchResults;
  467. }
  468. /// <summary>
  469. /// The remove
  470. /// </summary>
  471. const string remove = "\"'!`?";
  472. /// <summary>
  473. /// The spacers
  474. /// </summary>
  475. const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
  476. /// <summary>
  477. /// Gets the name of the comparable.
  478. /// </summary>
  479. /// <param name="name">The name.</param>
  480. /// <returns>System.String.</returns>
  481. internal static string GetComparableName(string name)
  482. {
  483. name = name.ToLower();
  484. name = name.Normalize(NormalizationForm.FormKD);
  485. var sb = new StringBuilder();
  486. foreach (var c in name)
  487. {
  488. if ((int)c >= 0x2B0 && (int)c <= 0x0333)
  489. {
  490. // skip char modifier and diacritics
  491. }
  492. else if (remove.IndexOf(c) > -1)
  493. {
  494. // skip chars we are removing
  495. }
  496. else if (spacers.IndexOf(c) > -1)
  497. {
  498. sb.Append(" ");
  499. }
  500. else if (c == '&')
  501. {
  502. sb.Append(" and ");
  503. }
  504. else
  505. {
  506. sb.Append(c);
  507. }
  508. }
  509. name = sb.ToString();
  510. name = name.Replace(", the", "");
  511. name = name.Replace("the ", " ");
  512. name = name.Replace(" the ", " ");
  513. string prevName;
  514. do
  515. {
  516. prevName = name;
  517. name = name.Replace(" ", " ");
  518. } while (name.Length != prevName.Length);
  519. return name.Trim();
  520. }
  521. private void FetchSeriesInfo(MetadataResult<Series> result, string seriesXmlPath, CancellationToken cancellationToken)
  522. {
  523. var settings = new XmlReaderSettings
  524. {
  525. CheckCharacters = false,
  526. IgnoreProcessingInstructions = true,
  527. IgnoreComments = true,
  528. ValidationType = ValidationType.None
  529. };
  530. var episiodeAirDates = new List<DateTime>();
  531. using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
  532. {
  533. // Use XmlReader for best performance
  534. using (var reader = XmlReader.Create(streamReader, settings))
  535. {
  536. reader.MoveToContent();
  537. // Loop through each element
  538. while (reader.Read())
  539. {
  540. cancellationToken.ThrowIfCancellationRequested();
  541. if (reader.NodeType == XmlNodeType.Element)
  542. {
  543. switch (reader.Name)
  544. {
  545. case "Series":
  546. {
  547. using (var subtree = reader.ReadSubtree())
  548. {
  549. FetchDataFromSeriesNode(result, subtree, cancellationToken);
  550. }
  551. break;
  552. }
  553. case "Episode":
  554. {
  555. using (var subtree = reader.ReadSubtree())
  556. {
  557. var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
  558. if (date.HasValue)
  559. {
  560. episiodeAirDates.Add(date.Value);
  561. }
  562. }
  563. break;
  564. }
  565. default:
  566. reader.Skip();
  567. break;
  568. }
  569. }
  570. }
  571. }
  572. }
  573. if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
  574. {
  575. result.Item.EndDate = episiodeAirDates.Max();
  576. }
  577. }
  578. private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
  579. {
  580. DateTime? airDate = null;
  581. int? seasonNumber = null;
  582. reader.MoveToContent();
  583. // Loop through each element
  584. while (reader.Read())
  585. {
  586. cancellationToken.ThrowIfCancellationRequested();
  587. if (reader.NodeType == XmlNodeType.Element)
  588. {
  589. switch (reader.Name)
  590. {
  591. case "FirstAired":
  592. {
  593. var val = reader.ReadElementContentAsString();
  594. if (!string.IsNullOrWhiteSpace(val))
  595. {
  596. DateTime date;
  597. if (DateTime.TryParse(val, out date))
  598. {
  599. airDate = date.ToUniversalTime();
  600. }
  601. }
  602. break;
  603. }
  604. case "SeasonNumber":
  605. {
  606. var val = reader.ReadElementContentAsString();
  607. if (!string.IsNullOrWhiteSpace(val))
  608. {
  609. int rval;
  610. // int.TryParse is local aware, so it can be probamatic, force us culture
  611. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  612. {
  613. seasonNumber = rval;
  614. }
  615. }
  616. break;
  617. }
  618. default:
  619. reader.Skip();
  620. break;
  621. }
  622. }
  623. }
  624. if (seasonNumber.HasValue && seasonNumber.Value != 0)
  625. {
  626. return airDate;
  627. }
  628. return null;
  629. }
  630. /// <summary>
  631. /// Fetches the actors.
  632. /// </summary>
  633. /// <param name="result">The result.</param>
  634. /// <param name="actorsXmlPath">The actors XML path.</param>
  635. private void FetchActors(MetadataResult<Series> result, string actorsXmlPath)
  636. {
  637. var settings = new XmlReaderSettings
  638. {
  639. CheckCharacters = false,
  640. IgnoreProcessingInstructions = true,
  641. IgnoreComments = true,
  642. ValidationType = ValidationType.None
  643. };
  644. using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
  645. {
  646. // Use XmlReader for best performance
  647. using (var reader = XmlReader.Create(streamReader, settings))
  648. {
  649. reader.MoveToContent();
  650. // Loop through each element
  651. while (reader.Read())
  652. {
  653. if (reader.NodeType == XmlNodeType.Element)
  654. {
  655. switch (reader.Name)
  656. {
  657. case "Actor":
  658. {
  659. using (var subtree = reader.ReadSubtree())
  660. {
  661. FetchDataFromActorNode(result, subtree);
  662. }
  663. break;
  664. }
  665. default:
  666. reader.Skip();
  667. break;
  668. }
  669. }
  670. }
  671. }
  672. }
  673. }
  674. /// <summary>
  675. /// Fetches the data from actor node.
  676. /// </summary>
  677. /// <param name="result">The result.</param>
  678. /// <param name="reader">The reader.</param>
  679. private void FetchDataFromActorNode(MetadataResult<Series> result, XmlReader reader)
  680. {
  681. reader.MoveToContent();
  682. var personInfo = new PersonInfo();
  683. while (reader.Read())
  684. {
  685. if (reader.NodeType == XmlNodeType.Element)
  686. {
  687. switch (reader.Name)
  688. {
  689. case "Name":
  690. {
  691. personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  692. break;
  693. }
  694. case "Role":
  695. {
  696. personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  697. break;
  698. }
  699. case "id":
  700. {
  701. break;
  702. }
  703. case "Image":
  704. {
  705. var url = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  706. if (!string.IsNullOrWhiteSpace(url))
  707. {
  708. personInfo.ImageUrl = TVUtils.BannerUrl + url;
  709. }
  710. break;
  711. }
  712. case "SortOrder":
  713. {
  714. var val = reader.ReadElementContentAsString();
  715. if (!string.IsNullOrWhiteSpace(val))
  716. {
  717. int rval;
  718. // int.TryParse is local aware, so it can be probamatic, force us culture
  719. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  720. {
  721. personInfo.SortOrder = rval;
  722. }
  723. }
  724. break;
  725. }
  726. default:
  727. reader.Skip();
  728. break;
  729. }
  730. }
  731. }
  732. personInfo.Type = PersonType.Actor;
  733. if (!string.IsNullOrWhiteSpace(personInfo.Name))
  734. {
  735. result.AddPerson(personInfo);
  736. }
  737. }
  738. private void FetchDataFromSeriesNode(MetadataResult<Series> result, XmlReader reader, CancellationToken cancellationToken)
  739. {
  740. Series item = result.Item;
  741. reader.MoveToContent();
  742. // Loop through each element
  743. while (reader.Read())
  744. {
  745. cancellationToken.ThrowIfCancellationRequested();
  746. if (reader.NodeType == XmlNodeType.Element)
  747. {
  748. switch (reader.Name)
  749. {
  750. case "SeriesName":
  751. {
  752. item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  753. break;
  754. }
  755. case "Overview":
  756. {
  757. item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  758. break;
  759. }
  760. case "Language":
  761. {
  762. result.ResultLanguage = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  763. break;
  764. }
  765. case "Airs_DayOfWeek":
  766. {
  767. var val = reader.ReadElementContentAsString();
  768. if (!string.IsNullOrWhiteSpace(val))
  769. {
  770. item.AirDays = TVUtils.GetAirDays(val);
  771. }
  772. break;
  773. }
  774. case "Airs_Time":
  775. {
  776. var val = reader.ReadElementContentAsString();
  777. if (!string.IsNullOrWhiteSpace(val))
  778. {
  779. item.AirTime = val;
  780. }
  781. break;
  782. }
  783. case "ContentRating":
  784. {
  785. var val = reader.ReadElementContentAsString();
  786. if (!string.IsNullOrWhiteSpace(val))
  787. {
  788. item.OfficialRating = val;
  789. }
  790. break;
  791. }
  792. case "Rating":
  793. {
  794. var val = reader.ReadElementContentAsString();
  795. if (!string.IsNullOrWhiteSpace(val))
  796. {
  797. float rval;
  798. // float.TryParse is local aware, so it can be probamatic, force us culture
  799. if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval))
  800. {
  801. item.CommunityRating = rval;
  802. }
  803. }
  804. break;
  805. }
  806. case "RatingCount":
  807. {
  808. var val = reader.ReadElementContentAsString();
  809. if (!string.IsNullOrWhiteSpace(val))
  810. {
  811. int rval;
  812. // int.TryParse is local aware, so it can be probamatic, force us culture
  813. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  814. {
  815. item.VoteCount = rval;
  816. }
  817. }
  818. break;
  819. }
  820. case "IMDB_ID":
  821. {
  822. var val = reader.ReadElementContentAsString();
  823. if (!string.IsNullOrWhiteSpace(val))
  824. {
  825. item.SetProviderId(MetadataProviders.Imdb, val);
  826. }
  827. break;
  828. }
  829. case "zap2it_id":
  830. {
  831. var val = reader.ReadElementContentAsString();
  832. if (!string.IsNullOrWhiteSpace(val))
  833. {
  834. item.SetProviderId(MetadataProviders.Zap2It, val);
  835. }
  836. break;
  837. }
  838. case "Status":
  839. {
  840. var val = reader.ReadElementContentAsString();
  841. if (!string.IsNullOrWhiteSpace(val))
  842. {
  843. SeriesStatus seriesStatus;
  844. if (Enum.TryParse(val, true, out seriesStatus))
  845. item.Status = seriesStatus;
  846. }
  847. break;
  848. }
  849. case "FirstAired":
  850. {
  851. var val = reader.ReadElementContentAsString();
  852. if (!string.IsNullOrWhiteSpace(val))
  853. {
  854. DateTime date;
  855. if (DateTime.TryParse(val, out date))
  856. {
  857. date = date.ToUniversalTime();
  858. item.PremiereDate = date;
  859. item.ProductionYear = date.Year;
  860. }
  861. }
  862. break;
  863. }
  864. case "Runtime":
  865. {
  866. var val = reader.ReadElementContentAsString();
  867. if (!string.IsNullOrWhiteSpace(val))
  868. {
  869. int rval;
  870. // int.TryParse is local aware, so it can be probamatic, force us culture
  871. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  872. {
  873. item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks;
  874. }
  875. }
  876. break;
  877. }
  878. case "Genre":
  879. {
  880. var val = reader.ReadElementContentAsString();
  881. if (!string.IsNullOrWhiteSpace(val))
  882. {
  883. var vals = val
  884. .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
  885. .Select(i => i.Trim())
  886. .Where(i => !string.IsNullOrWhiteSpace(i))
  887. .ToList();
  888. if (vals.Count > 0)
  889. {
  890. item.Genres.Clear();
  891. foreach (var genre in vals)
  892. {
  893. item.AddGenre(genre);
  894. }
  895. }
  896. }
  897. break;
  898. }
  899. case "Network":
  900. {
  901. var val = reader.ReadElementContentAsString();
  902. if (!string.IsNullOrWhiteSpace(val))
  903. {
  904. var vals = val
  905. .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
  906. .Select(i => i.Trim())
  907. .Where(i => !string.IsNullOrWhiteSpace(i))
  908. .ToList();
  909. if (vals.Count > 0)
  910. {
  911. item.Studios.Clear();
  912. foreach (var genre in vals)
  913. {
  914. item.AddStudio(genre);
  915. }
  916. }
  917. }
  918. break;
  919. }
  920. default:
  921. reader.Skip();
  922. break;
  923. }
  924. }
  925. }
  926. }
  927. /// <summary>
  928. /// Extracts info for each episode into invididual xml files so that they can be easily accessed without having to step through the entire series xml
  929. /// </summary>
  930. /// <param name="seriesDataPath">The series data path.</param>
  931. /// <param name="xmlFile">The XML file.</param>
  932. /// <param name="lastTvDbUpdateTime">The last tv db update time.</param>
  933. /// <returns>Task.</returns>
  934. private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime)
  935. {
  936. var settings = new XmlReaderSettings
  937. {
  938. CheckCharacters = false,
  939. IgnoreProcessingInstructions = true,
  940. IgnoreComments = true,
  941. ValidationType = ValidationType.None
  942. };
  943. using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
  944. {
  945. // Use XmlReader for best performance
  946. using (var reader = XmlReader.Create(streamReader, settings))
  947. {
  948. reader.MoveToContent();
  949. // Loop through each element
  950. while (reader.Read())
  951. {
  952. if (reader.NodeType == XmlNodeType.Element)
  953. {
  954. switch (reader.Name)
  955. {
  956. case "Episode":
  957. {
  958. var outerXml = reader.ReadOuterXml();
  959. await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false);
  960. break;
  961. }
  962. default:
  963. reader.Skip();
  964. break;
  965. }
  966. }
  967. }
  968. }
  969. }
  970. }
  971. private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime)
  972. {
  973. var settings = new XmlReaderSettings
  974. {
  975. CheckCharacters = false,
  976. IgnoreProcessingInstructions = true,
  977. IgnoreComments = true,
  978. ValidationType = ValidationType.None
  979. };
  980. var seasonNumber = -1;
  981. var episodeNumber = -1;
  982. var absoluteNumber = -1;
  983. var lastUpdateString = string.Empty;
  984. using (var streamReader = new StringReader(xml))
  985. {
  986. // Use XmlReader for best performance
  987. using (var reader = XmlReader.Create(streamReader, settings))
  988. {
  989. reader.MoveToContent();
  990. // Loop through each element
  991. while (reader.Read())
  992. {
  993. if (reader.NodeType == XmlNodeType.Element)
  994. {
  995. switch (reader.Name)
  996. {
  997. case "lastupdated":
  998. {
  999. lastUpdateString = reader.ReadElementContentAsString();
  1000. break;
  1001. }
  1002. case "EpisodeNumber":
  1003. {
  1004. var val = reader.ReadElementContentAsString();
  1005. if (!string.IsNullOrWhiteSpace(val))
  1006. {
  1007. int num;
  1008. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  1009. {
  1010. episodeNumber = num;
  1011. }
  1012. }
  1013. break;
  1014. }
  1015. case "absolute_number":
  1016. {
  1017. var val = reader.ReadElementContentAsString();
  1018. if (!string.IsNullOrWhiteSpace(val))
  1019. {
  1020. int num;
  1021. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  1022. {
  1023. absoluteNumber = num;
  1024. }
  1025. }
  1026. break;
  1027. }
  1028. case "SeasonNumber":
  1029. {
  1030. var val = reader.ReadElementContentAsString();
  1031. if (!string.IsNullOrWhiteSpace(val))
  1032. {
  1033. int num;
  1034. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  1035. {
  1036. seasonNumber = num;
  1037. }
  1038. }
  1039. break;
  1040. }
  1041. default:
  1042. reader.Skip();
  1043. break;
  1044. }
  1045. }
  1046. }
  1047. }
  1048. }
  1049. var hasEpisodeChanged = true;
  1050. if (!string.IsNullOrWhiteSpace(lastUpdateString) && lastTvDbUpdateTime.HasValue)
  1051. {
  1052. long num;
  1053. if (long.TryParse(lastUpdateString, NumberStyles.Any, _usCulture, out num))
  1054. {
  1055. hasEpisodeChanged = num >= lastTvDbUpdateTime.Value;
  1056. }
  1057. }
  1058. var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber));
  1059. // Only save the file if not already there, or if the episode has changed
  1060. if (hasEpisodeChanged || !_fileSystem.FileExists(file))
  1061. {
  1062. using (var writer = XmlWriter.Create(file, new XmlWriterSettings
  1063. {
  1064. Encoding = Encoding.UTF8,
  1065. Async = true
  1066. }))
  1067. {
  1068. await writer.WriteRawAsync(xml).ConfigureAwait(false);
  1069. }
  1070. }
  1071. if (absoluteNumber != -1)
  1072. {
  1073. file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber));
  1074. // Only save the file if not already there, or if the episode has changed
  1075. if (hasEpisodeChanged || !_fileSystem.FileExists(file))
  1076. {
  1077. using (var writer = XmlWriter.Create(file, new XmlWriterSettings
  1078. {
  1079. Encoding = Encoding.UTF8,
  1080. Async = true
  1081. }))
  1082. {
  1083. await writer.WriteRawAsync(xml).ConfigureAwait(false);
  1084. }
  1085. }
  1086. }
  1087. }
  1088. /// <summary>
  1089. /// Gets the series data path.
  1090. /// </summary>
  1091. /// <param name="appPaths">The app paths.</param>
  1092. /// <param name="seriesProviderIds">The series provider ids.</param>
  1093. /// <returns>System.String.</returns>
  1094. internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary<string, string> seriesProviderIds)
  1095. {
  1096. string seriesId;
  1097. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId))
  1098. {
  1099. var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
  1100. return seriesDataPath;
  1101. }
  1102. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId))
  1103. {
  1104. var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
  1105. return seriesDataPath;
  1106. }
  1107. return null;
  1108. }
  1109. public string GetSeriesXmlPath(Dictionary<string, string> seriesProviderIds, string language)
  1110. {
  1111. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  1112. var seriesXmlFilename = language.ToLower() + ".xml";
  1113. return Path.Combine(seriesDataPath, seriesXmlFilename);
  1114. }
  1115. /// <summary>
  1116. /// Gets the series data path.
  1117. /// </summary>
  1118. /// <param name="appPaths">The app paths.</param>
  1119. /// <returns>System.String.</returns>
  1120. internal static string GetSeriesDataPath(IApplicationPaths appPaths)
  1121. {
  1122. var dataPath = Path.Combine(appPaths.CachePath, "tvdb");
  1123. return dataPath;
  1124. }
  1125. private void DeleteXmlFiles(string path)
  1126. {
  1127. try
  1128. {
  1129. foreach (var file in _fileSystem.GetFilePaths(path, true)
  1130. .ToList())
  1131. {
  1132. _fileSystem.DeleteFile(file);
  1133. }
  1134. }
  1135. catch (DirectoryNotFoundException)
  1136. {
  1137. // No biggie
  1138. }
  1139. }
  1140. /// <summary>
  1141. /// Sanitizes the XML file.
  1142. /// </summary>
  1143. /// <param name="file">The file.</param>
  1144. /// <returns>Task.</returns>
  1145. private async Task SanitizeXmlFile(string file)
  1146. {
  1147. string validXml;
  1148. using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  1149. {
  1150. using (var reader = new StreamReader(fileStream))
  1151. {
  1152. var xml = await reader.ReadToEndAsync().ConfigureAwait(false);
  1153. validXml = StripInvalidXmlCharacters(xml);
  1154. }
  1155. }
  1156. using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true))
  1157. {
  1158. using (var writer = new StreamWriter(fileStream))
  1159. {
  1160. await writer.WriteAsync(validXml).ConfigureAwait(false);
  1161. }
  1162. }
  1163. }
  1164. /// <summary>
  1165. /// Strips the invalid XML characters.
  1166. /// </summary>
  1167. /// <param name="inString">The in string.</param>
  1168. /// <returns>System.String.</returns>
  1169. public static string StripInvalidXmlCharacters(string inString)
  1170. {
  1171. if (inString == null) return null;
  1172. var sbOutput = new StringBuilder();
  1173. char ch;
  1174. for (int i = 0; i < inString.Length; i++)
  1175. {
  1176. ch = inString[i];
  1177. if ((ch >= 0x0020 && ch <= 0xD7FF) ||
  1178. (ch >= 0xE000 && ch <= 0xFFFD) ||
  1179. ch == 0x0009 ||
  1180. ch == 0x000A ||
  1181. ch == 0x000D)
  1182. {
  1183. sbOutput.Append(ch);
  1184. }
  1185. }
  1186. return sbOutput.ToString();
  1187. }
  1188. public string Name
  1189. {
  1190. get { return "TheTVDB"; }
  1191. }
  1192. public async Task Identify(SeriesInfo info)
  1193. {
  1194. if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb)))
  1195. {
  1196. return;
  1197. }
  1198. var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false);
  1199. var entry = srch.FirstOrDefault();
  1200. if (entry != null)
  1201. {
  1202. var id = entry.GetProviderId(MetadataProviders.Tvdb);
  1203. info.SetProviderId(MetadataProviders.Tvdb, id);
  1204. }
  1205. }
  1206. public int Order
  1207. {
  1208. get
  1209. {
  1210. // After Omdb
  1211. return 1;
  1212. }
  1213. }
  1214. public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
  1215. {
  1216. return _httpClient.GetResponse(new HttpRequestOptions
  1217. {
  1218. CancellationToken = cancellationToken,
  1219. Url = url,
  1220. ResourcePool = TvDbResourcePool
  1221. });
  1222. }
  1223. }
  1224. }