TvdbSeriesProvider.cs 56 KB

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