TvdbSeriesProvider.cs 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.IO;
  3. using MediaBrowser.Common.Net;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Entities;
  6. using MediaBrowser.Controller.Entities.TV;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Providers;
  9. using MediaBrowser.Model.Configuration;
  10. using MediaBrowser.Model.Entities;
  11. using MediaBrowser.Model.IO;
  12. using MediaBrowser.Model.Logging;
  13. using MediaBrowser.Model.Net;
  14. using MediaBrowser.Model.Providers;
  15. using System;
  16. using System.Collections.Generic;
  17. using System.Globalization;
  18. using System.IO;
  19. using System.Linq;
  20. using System.Net;
  21. using System.Text;
  22. using System.Threading;
  23. using System.Threading.Tasks;
  24. using System.Xml;
  25. using CommonIO;
  26. namespace MediaBrowser.Providers.TV
  27. {
  28. public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IItemIdentityProvider<SeriesInfo>, IHasOrder
  29. {
  30. private const string TvdbSeriesOffset = "TvdbSeriesOffset";
  31. private const string TvdbSeriesOffsetFormat = "{0}-{1}";
  32. internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
  33. internal static TvdbSeriesProvider Current { get; private set; }
  34. private readonly IZipClient _zipClient;
  35. private readonly IHttpClient _httpClient;
  36. private readonly IFileSystem _fileSystem;
  37. private readonly IServerConfigurationManager _config;
  38. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  39. private readonly ILogger _logger;
  40. private readonly ISeriesOrderManager _seriesOrder;
  41. private readonly ILibraryManager _libraryManager;
  42. public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder, ILibraryManager libraryManager)
  43. {
  44. _zipClient = zipClient;
  45. _httpClient = httpClient;
  46. _fileSystem = fileSystem;
  47. _config = config;
  48. _logger = logger;
  49. _seriesOrder = seriesOrder;
  50. _libraryManager = libraryManager;
  51. Current = this;
  52. }
  53. private const string SeriesSearchUrl = "http://www.thetvdb.com/api/GetSeries.php?seriesname={0}&language={1}";
  54. private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
  55. private const string GetSeriesByImdbId = "http://www.thetvdb.com/api/GetSeriesByRemoteID.php?imdbid={0}&language={1}";
  56. public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
  57. {
  58. if (IsValidSeries(searchInfo.ProviderIds))
  59. {
  60. var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
  61. if (metadata.HasMetadata)
  62. {
  63. return new List<RemoteSearchResult>
  64. {
  65. new RemoteSearchResult
  66. {
  67. Name = metadata.Item.Name,
  68. PremiereDate = metadata.Item.PremiereDate,
  69. ProductionYear = metadata.Item.ProductionYear,
  70. ProviderIds = metadata.Item.ProviderIds,
  71. SearchProviderName = Name
  72. }
  73. };
  74. }
  75. }
  76. return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  77. }
  78. public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
  79. {
  80. var result = new MetadataResult<Series>();
  81. if (!IsValidSeries(itemId.ProviderIds))
  82. {
  83. await Identify(itemId).ConfigureAwait(false);
  84. }
  85. cancellationToken.ThrowIfCancellationRequested();
  86. if (IsValidSeries(itemId.ProviderIds))
  87. {
  88. await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  89. result.Item = new Series();
  90. result.HasMetadata = true;
  91. FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken);
  92. await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false);
  93. }
  94. return result;
  95. }
  96. private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info)
  97. {
  98. var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name);
  99. if (index == null)
  100. return;
  101. var offset = info.AnimeSeriesIndex - index;
  102. var id = string.Format(TvdbSeriesOffsetFormat, series.GetProviderId(MetadataProviders.Tvdb), offset);
  103. series.SetProviderId(TvdbSeriesOffset, id);
  104. }
  105. internal static int? GetSeriesOffset(Dictionary<string, string> seriesProviderIds)
  106. {
  107. string idString;
  108. if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString))
  109. return null;
  110. var parts = idString.Split('-');
  111. if (parts.Length < 2)
  112. return null;
  113. int offset;
  114. if (int.TryParse(parts[1], out offset))
  115. return offset;
  116. return null;
  117. }
  118. /// <summary>
  119. /// Fetches the series data.
  120. /// </summary>
  121. /// <param name="result">The result.</param>
  122. /// <param name="metadataLanguage">The metadata language.</param>
  123. /// <param name="seriesProviderIds">The series provider ids.</param>
  124. /// <param name="cancellationToken">The cancellation token.</param>
  125. /// <returns>Task{System.Boolean}.</returns>
  126. private void FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
  127. {
  128. var series = result.Item;
  129. string id;
  130. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id))
  131. {
  132. series.SetProviderId(MetadataProviders.Tvdb, id);
  133. }
  134. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
  135. {
  136. series.SetProviderId(MetadataProviders.Imdb, id);
  137. }
  138. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  139. var seriesXmlPath = GetSeriesXmlPath (seriesProviderIds, metadataLanguage);
  140. var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
  141. FetchSeriesInfo(series, seriesXmlPath, cancellationToken);
  142. cancellationToken.ThrowIfCancellationRequested();
  143. result.ResetPeople();
  144. FetchActors(result, actorsXmlPath);
  145. }
  146. /// <summary>
  147. /// Downloads the series zip.
  148. /// </summary>
  149. /// <param name="seriesId">The series id.</param>
  150. /// <param name="idType">Type of the identifier.</param>
  151. /// <param name="seriesDataPath">The series data path.</param>
  152. /// <param name="lastTvDbUpdateTime">The last tv database update time.</param>
  153. /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
  154. /// <param name="cancellationToken">The cancellation token.</param>
  155. /// <returns>Task.</returns>
  156. /// <exception cref="System.ArgumentNullException">seriesId</exception>
  157. internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken)
  158. {
  159. if (string.IsNullOrWhiteSpace(seriesId))
  160. {
  161. throw new ArgumentNullException("seriesId");
  162. }
  163. try
  164. {
  165. await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  166. return;
  167. }
  168. catch (HttpException ex)
  169. {
  170. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  171. {
  172. throw;
  173. }
  174. }
  175. if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))
  176. {
  177. await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  178. }
  179. }
  180. private async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken)
  181. {
  182. if (string.IsNullOrWhiteSpace(seriesId))
  183. {
  184. throw new ArgumentNullException("seriesId");
  185. }
  186. if (!string.Equals(idType, "tvdb", StringComparison.OrdinalIgnoreCase))
  187. {
  188. seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  189. }
  190. if (string.IsNullOrWhiteSpace(seriesId))
  191. {
  192. throw new ArgumentNullException("seriesId");
  193. }
  194. var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, preferredMetadataLanguage);
  195. using (var zipStream = await _httpClient.Get(new HttpRequestOptions
  196. {
  197. Url = url,
  198. ResourcePool = TvDbResourcePool,
  199. CancellationToken = cancellationToken
  200. }).ConfigureAwait(false))
  201. {
  202. // Delete existing files
  203. DeleteXmlFiles(seriesDataPath);
  204. // Copy to memory stream because we need a seekable stream
  205. using (var ms = new MemoryStream())
  206. {
  207. await zipStream.CopyToAsync(ms).ConfigureAwait(false);
  208. ms.Position = 0;
  209. _zipClient.ExtractAllFromZip(ms, seriesDataPath, true);
  210. }
  211. }
  212. // Sanitize all files, except for extracted episode files
  213. foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList()
  214. .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase)))
  215. {
  216. await SanitizeXmlFile(file).ConfigureAwait(false);
  217. }
  218. var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, preferredMetadataLanguage + ".xml");
  219. var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml");
  220. if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase))
  221. {
  222. _fileSystem.CopyFile(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true);
  223. }
  224. await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false);
  225. }
  226. private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken)
  227. {
  228. var url = string.Format(GetSeriesByImdbId, id, language);
  229. using (var result = await _httpClient.Get(new HttpRequestOptions
  230. {
  231. Url = url,
  232. ResourcePool = TvDbResourcePool,
  233. CancellationToken = cancellationToken
  234. }).ConfigureAwait(false))
  235. {
  236. var doc = new XmlDocument();
  237. doc.Load(result);
  238. if (doc.HasChildNodes)
  239. {
  240. var node = doc.SelectSingleNode("//Series/seriesid");
  241. if (node != null)
  242. {
  243. var idResult = node.InnerText;
  244. _logger.Info("Tvdb GetSeriesByRemoteId produced id of {0}", idResult ?? string.Empty);
  245. return idResult;
  246. }
  247. }
  248. }
  249. return null;
  250. }
  251. public TvdbOptions GetTvDbOptions()
  252. {
  253. return _config.GetConfiguration<TvdbOptions>("tvdb");
  254. }
  255. internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
  256. {
  257. string id;
  258. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id))
  259. {
  260. // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
  261. if (!string.IsNullOrWhiteSpace(id))
  262. {
  263. return true;
  264. }
  265. }
  266. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
  267. {
  268. // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
  269. if (!string.IsNullOrWhiteSpace(id))
  270. {
  271. return true;
  272. }
  273. }
  274. return false;
  275. }
  276. private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1,1);
  277. internal async Task<string> EnsureSeriesInfo(Dictionary<string, string> seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken)
  278. {
  279. await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  280. try
  281. {
  282. string seriesId;
  283. if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId))
  284. {
  285. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  286. // Only download if not already there
  287. // The post-scan task will take care of updates so we don't need to re-download here
  288. if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage))
  289. {
  290. await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  291. }
  292. return seriesDataPath;
  293. }
  294. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId))
  295. {
  296. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds);
  297. // Only download if not already there
  298. // The post-scan task will take care of updates so we don't need to re-download here
  299. if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage))
  300. {
  301. await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  302. }
  303. return seriesDataPath;
  304. }
  305. return null;
  306. }
  307. finally
  308. {
  309. _ensureSemaphore.Release();
  310. }
  311. }
  312. private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage)
  313. {
  314. try
  315. {
  316. var files = _fileSystem.GetFiles(seriesDataPath)
  317. .ToList();
  318. var seriesXmlFilename = preferredMetadataLanguage + ".xml";
  319. var automaticUpdatesEnabled = GetTvDbOptions().EnableAutomaticUpdates;
  320. const int cacheDays = 1;
  321. var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
  322. // No need to check age if automatic updates are enabled
  323. if (seriesFile == null || !seriesFile.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays))
  324. {
  325. return false;
  326. }
  327. var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  328. // No need to check age if automatic updates are enabled
  329. if (actorsXml == null || !actorsXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays))
  330. {
  331. return false;
  332. }
  333. var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  334. // No need to check age if automatic updates are enabled
  335. if (bannersXml == null || !bannersXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays))
  336. {
  337. return false;
  338. }
  339. return true;
  340. }
  341. catch (DirectoryNotFoundException)
  342. {
  343. return false;
  344. }
  345. catch (FileNotFoundException)
  346. {
  347. return false;
  348. }
  349. }
  350. /// <summary>
  351. /// Finds the series.
  352. /// </summary>
  353. /// <param name="name">The name.</param>
  354. /// <param name="year">The year.</param>
  355. /// <param name="language">The language.</param>
  356. /// <param name="cancellationToken">The cancellation token.</param>
  357. /// <returns>Task{System.String}.</returns>
  358. private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
  359. {
  360. var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList();
  361. if (results.Count == 0)
  362. {
  363. var parsedName = _libraryManager.ParseName(name);
  364. var nameWithoutYear = parsedName.Name;
  365. if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
  366. {
  367. results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList();
  368. }
  369. }
  370. return results.Where(i =>
  371. {
  372. if (year.HasValue && i.ProductionYear.HasValue)
  373. {
  374. // Allow one year tolerance
  375. return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
  376. }
  377. return true;
  378. });
  379. }
  380. private async Task<IEnumerable<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
  381. {
  382. var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), language.ToLower());
  383. var doc = new XmlDocument();
  384. using (var results = await _httpClient.Get(new HttpRequestOptions
  385. {
  386. Url = url,
  387. ResourcePool = TvDbResourcePool,
  388. CancellationToken = cancellationToken
  389. }).ConfigureAwait(false))
  390. {
  391. doc.Load(results);
  392. }
  393. var searchResults = new List<RemoteSearchResult>();
  394. if (doc.HasChildNodes)
  395. {
  396. var nodes = doc.SelectNodes("//Series");
  397. var comparableName = GetComparableName(name);
  398. if (nodes != null)
  399. {
  400. foreach (XmlNode node in nodes)
  401. {
  402. var searchResult = new RemoteSearchResult
  403. {
  404. SearchProviderName = Name
  405. };
  406. var titles = new List<string>();
  407. var nameNode = node.SelectSingleNode("./SeriesName");
  408. if (nameNode != null)
  409. {
  410. titles.Add(GetComparableName(nameNode.InnerText));
  411. }
  412. var aliasNode = node.SelectSingleNode("./AliasNames");
  413. if (aliasNode != null)
  414. {
  415. var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
  416. titles.AddRange(alias);
  417. }
  418. var imdbIdNode = node.SelectSingleNode("./IMDB_ID");
  419. if (imdbIdNode != null)
  420. {
  421. var val = imdbIdNode.InnerText;
  422. if (!string.IsNullOrWhiteSpace(val))
  423. {
  424. searchResult.SetProviderId(MetadataProviders.Imdb, val);
  425. }
  426. }
  427. var bannerNode = node.SelectSingleNode("./banner");
  428. if (bannerNode != null)
  429. {
  430. var val = bannerNode.InnerText;
  431. if (!string.IsNullOrWhiteSpace(val))
  432. {
  433. searchResult.ImageUrl = TVUtils.BannerUrl + val;
  434. }
  435. }
  436. var airDateNode = node.SelectSingleNode("./FirstAired");
  437. if (airDateNode != null)
  438. {
  439. var val = airDateNode.InnerText;
  440. if (!string.IsNullOrWhiteSpace(val))
  441. {
  442. DateTime date;
  443. if (DateTime.TryParse(val, out date))
  444. {
  445. searchResult.ProductionYear = date.Year;
  446. }
  447. }
  448. }
  449. foreach (var title in titles)
  450. {
  451. if (string.Equals(title, comparableName, StringComparison.OrdinalIgnoreCase))
  452. {
  453. var id = node.SelectSingleNode("./seriesid") ??
  454. node.SelectSingleNode("./id");
  455. if (id != null)
  456. {
  457. searchResult.Name = title;
  458. searchResult.SetProviderId(MetadataProviders.Tvdb, id.InnerText);
  459. searchResults.Add(searchResult);
  460. }
  461. break;
  462. }
  463. _logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
  464. }
  465. }
  466. }
  467. }
  468. if (searchResults.Count == 0)
  469. {
  470. _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
  471. }
  472. return searchResults;
  473. }
  474. /// <summary>
  475. /// The remove
  476. /// </summary>
  477. const string remove = "\"'!`?";
  478. /// <summary>
  479. /// The spacers
  480. /// </summary>
  481. const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
  482. /// <summary>
  483. /// Gets the name of the comparable.
  484. /// </summary>
  485. /// <param name="name">The name.</param>
  486. /// <returns>System.String.</returns>
  487. internal static string GetComparableName(string name)
  488. {
  489. name = name.ToLower();
  490. name = name.Normalize(NormalizationForm.FormKD);
  491. var sb = new StringBuilder();
  492. foreach (var c in name)
  493. {
  494. if ((int)c >= 0x2B0 && (int)c <= 0x0333)
  495. {
  496. // skip char modifier and diacritics
  497. }
  498. else if (remove.IndexOf(c) > -1)
  499. {
  500. // skip chars we are removing
  501. }
  502. else if (spacers.IndexOf(c) > -1)
  503. {
  504. sb.Append(" ");
  505. }
  506. else if (c == '&')
  507. {
  508. sb.Append(" and ");
  509. }
  510. else
  511. {
  512. sb.Append(c);
  513. }
  514. }
  515. name = sb.ToString();
  516. name = name.Replace(", the", "");
  517. name = name.Replace("the ", " ");
  518. name = name.Replace(" the ", " ");
  519. string prevName;
  520. do
  521. {
  522. prevName = name;
  523. name = name.Replace(" ", " ");
  524. } while (name.Length != prevName.Length);
  525. return name.Trim();
  526. }
  527. private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken)
  528. {
  529. var settings = new XmlReaderSettings
  530. {
  531. CheckCharacters = false,
  532. IgnoreProcessingInstructions = true,
  533. IgnoreComments = true,
  534. ValidationType = ValidationType.None
  535. };
  536. var episiodeAirDates = new List<DateTime>();
  537. using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
  538. {
  539. // Use XmlReader for best performance
  540. using (var reader = XmlReader.Create(streamReader, settings))
  541. {
  542. reader.MoveToContent();
  543. // Loop through each element
  544. while (reader.Read())
  545. {
  546. cancellationToken.ThrowIfCancellationRequested();
  547. if (reader.NodeType == XmlNodeType.Element)
  548. {
  549. switch (reader.Name)
  550. {
  551. case "Series":
  552. {
  553. using (var subtree = reader.ReadSubtree())
  554. {
  555. FetchDataFromSeriesNode(item, subtree, cancellationToken);
  556. }
  557. break;
  558. }
  559. case "Episode":
  560. {
  561. using (var subtree = reader.ReadSubtree())
  562. {
  563. var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
  564. if (date.HasValue)
  565. {
  566. episiodeAirDates.Add(date.Value);
  567. }
  568. }
  569. break;
  570. }
  571. default:
  572. reader.Skip();
  573. break;
  574. }
  575. }
  576. }
  577. }
  578. }
  579. if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
  580. {
  581. item.EndDate = episiodeAirDates.Max();
  582. }
  583. }
  584. private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
  585. {
  586. DateTime? airDate = null;
  587. int? seasonNumber = null;
  588. reader.MoveToContent();
  589. // Loop through each element
  590. while (reader.Read())
  591. {
  592. cancellationToken.ThrowIfCancellationRequested();
  593. if (reader.NodeType == XmlNodeType.Element)
  594. {
  595. switch (reader.Name)
  596. {
  597. case "FirstAired":
  598. {
  599. var val = reader.ReadElementContentAsString();
  600. if (!string.IsNullOrWhiteSpace(val))
  601. {
  602. DateTime date;
  603. if (DateTime.TryParse(val, out date))
  604. {
  605. airDate = date.ToUniversalTime();
  606. }
  607. }
  608. break;
  609. }
  610. case "SeasonNumber":
  611. {
  612. var val = reader.ReadElementContentAsString();
  613. if (!string.IsNullOrWhiteSpace(val))
  614. {
  615. int rval;
  616. // int.TryParse is local aware, so it can be probamatic, force us culture
  617. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  618. {
  619. seasonNumber = rval;
  620. }
  621. }
  622. break;
  623. }
  624. default:
  625. reader.Skip();
  626. break;
  627. }
  628. }
  629. }
  630. if (seasonNumber.HasValue && seasonNumber.Value != 0)
  631. {
  632. return airDate;
  633. }
  634. return null;
  635. }
  636. /// <summary>
  637. /// Fetches the actors.
  638. /// </summary>
  639. /// <param name="result">The result.</param>
  640. /// <param name="actorsXmlPath">The actors XML path.</param>
  641. private void FetchActors(MetadataResult<Series> result, string actorsXmlPath)
  642. {
  643. var settings = new XmlReaderSettings
  644. {
  645. CheckCharacters = false,
  646. IgnoreProcessingInstructions = true,
  647. IgnoreComments = true,
  648. ValidationType = ValidationType.None
  649. };
  650. using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
  651. {
  652. // Use XmlReader for best performance
  653. using (var reader = XmlReader.Create(streamReader, settings))
  654. {
  655. reader.MoveToContent();
  656. // Loop through each element
  657. while (reader.Read())
  658. {
  659. if (reader.NodeType == XmlNodeType.Element)
  660. {
  661. switch (reader.Name)
  662. {
  663. case "Actor":
  664. {
  665. using (var subtree = reader.ReadSubtree())
  666. {
  667. FetchDataFromActorNode(result, subtree);
  668. }
  669. break;
  670. }
  671. default:
  672. reader.Skip();
  673. break;
  674. }
  675. }
  676. }
  677. }
  678. }
  679. }
  680. /// <summary>
  681. /// Fetches the data from actor node.
  682. /// </summary>
  683. /// <param name="result">The result.</param>
  684. /// <param name="reader">The reader.</param>
  685. private void FetchDataFromActorNode(MetadataResult<Series> result, XmlReader reader)
  686. {
  687. reader.MoveToContent();
  688. var personInfo = new PersonInfo();
  689. while (reader.Read())
  690. {
  691. if (reader.NodeType == XmlNodeType.Element)
  692. {
  693. switch (reader.Name)
  694. {
  695. case "Name":
  696. {
  697. personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  698. break;
  699. }
  700. case "Role":
  701. {
  702. personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  703. break;
  704. }
  705. case "id":
  706. {
  707. break;
  708. }
  709. case "Image":
  710. {
  711. var url = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  712. if (!string.IsNullOrWhiteSpace(url))
  713. {
  714. personInfo.ImageUrl = TVUtils.BannerUrl + url;
  715. }
  716. break;
  717. }
  718. case "SortOrder":
  719. {
  720. var val = reader.ReadElementContentAsString();
  721. if (!string.IsNullOrWhiteSpace(val))
  722. {
  723. int rval;
  724. // int.TryParse is local aware, so it can be probamatic, force us culture
  725. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  726. {
  727. personInfo.SortOrder = rval;
  728. }
  729. }
  730. break;
  731. }
  732. default:
  733. reader.Skip();
  734. break;
  735. }
  736. }
  737. }
  738. personInfo.Type = PersonType.Actor;
  739. if (!string.IsNullOrWhiteSpace(personInfo.Name))
  740. {
  741. result.AddPerson(personInfo);
  742. }
  743. }
  744. private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken)
  745. {
  746. reader.MoveToContent();
  747. // Loop through each element
  748. while (reader.Read())
  749. {
  750. cancellationToken.ThrowIfCancellationRequested();
  751. if (reader.NodeType == XmlNodeType.Element)
  752. {
  753. switch (reader.Name)
  754. {
  755. case "SeriesName":
  756. {
  757. item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  758. break;
  759. }
  760. case "Overview":
  761. {
  762. item.Overview = (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))
  1098. {
  1099. var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
  1100. return seriesDataPath;
  1101. }
  1102. if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out 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. public class TvdbConfigStore : IConfigurationFactory
  1225. {
  1226. public IEnumerable<ConfigurationStore> GetConfigurations()
  1227. {
  1228. return new List<ConfigurationStore>
  1229. {
  1230. new ConfigurationStore
  1231. {
  1232. Key = "tvdb",
  1233. ConfigurationType = typeof(TvdbOptions)
  1234. }
  1235. };
  1236. }
  1237. }
  1238. }