TvdbSeriesProvider.cs 65 KB

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