TvdbSeriesProvider.cs 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.IO;
  3. using MediaBrowser.Common.Net;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Entities;
  6. using MediaBrowser.Controller.Entities.TV;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Providers;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.IO;
  11. using MediaBrowser.Model.Logging;
  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. namespace MediaBrowser.Providers.TV
  24. {
  25. public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
  26. {
  27. private const string TvdbSeriesOffset = "TvdbSeriesOffset";
  28. private const string TvdbSeriesOffsetFormat = "{0}-{1}";
  29. internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
  30. internal static TvdbSeriesProvider Current { get; private set; }
  31. private readonly IZipClient _zipClient;
  32. private readonly IHttpClient _httpClient;
  33. private readonly IFileSystem _fileSystem;
  34. private readonly IServerConfigurationManager _config;
  35. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  36. private readonly ILogger _logger;
  37. private readonly ISeriesOrderManager _seriesOrder;
  38. public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder)
  39. {
  40. _zipClient = zipClient;
  41. _httpClient = httpClient;
  42. _fileSystem = fileSystem;
  43. _config = config;
  44. _logger = logger;
  45. _seriesOrder = seriesOrder;
  46. Current = this;
  47. }
  48. private const string RootUrl = "http://www.thetvdb.com/api/";
  49. private const string SeriesQuery = "GetSeries.php?seriesname={0}";
  50. private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
  51. public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
  52. {
  53. var seriesId = searchInfo.GetProviderId(MetadataProviders.Tvdb);
  54. if (!string.IsNullOrEmpty(seriesId))
  55. {
  56. }
  57. return new List<RemoteSearchResult>();
  58. }
  59. public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
  60. {
  61. var result = new MetadataResult<Series>();
  62. var seriesId = itemId.GetProviderId(MetadataProviders.Tvdb);
  63. if (string.IsNullOrEmpty(seriesId))
  64. {
  65. seriesId = await FindSeries(itemId.Name, cancellationToken).ConfigureAwait(false);
  66. if (string.IsNullOrEmpty(seriesId))
  67. {
  68. int? yearInName = null;
  69. string nameWithoutYear;
  70. NameParser.ParseName(itemId.Name, out nameWithoutYear, out yearInName);
  71. if (!string.IsNullOrEmpty(nameWithoutYear) && !string.Equals(nameWithoutYear, itemId.Name, StringComparison.OrdinalIgnoreCase))
  72. {
  73. seriesId = await FindSeries(nameWithoutYear, cancellationToken).ConfigureAwait(false);
  74. }
  75. }
  76. }
  77. cancellationToken.ThrowIfCancellationRequested();
  78. if (!string.IsNullOrEmpty(seriesId))
  79. {
  80. await EnsureSeriesInfo(seriesId, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  81. result.Item = new Series();
  82. result.HasMetadata = true;
  83. FetchSeriesData(result.Item, seriesId, cancellationToken);
  84. await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false);
  85. }
  86. return result;
  87. }
  88. private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info)
  89. {
  90. var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name);
  91. if (index == null)
  92. return;
  93. var offset = info.AnimeSeriesIndex - index;
  94. var id = string.Format(TvdbSeriesOffsetFormat, series.GetProviderId(MetadataProviders.Tvdb), offset);
  95. series.SetProviderId(TvdbSeriesOffset, id);
  96. }
  97. internal static int? GetSeriesOffset(Dictionary<string, string> seriesProviderIds)
  98. {
  99. string idString;
  100. if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString))
  101. return null;
  102. var parts = idString.Split('-');
  103. if (parts.Length < 2)
  104. return null;
  105. int offset;
  106. if (int.TryParse(parts[1], out offset))
  107. return offset;
  108. return null;
  109. }
  110. /// <summary>
  111. /// Fetches the series data.
  112. /// </summary>
  113. /// <param name="series">The series.</param>
  114. /// <param name="seriesId">The series id.</param>
  115. /// <param name="cancellationToken">The cancellation token.</param>
  116. /// <returns>Task{System.Boolean}.</returns>
  117. private void FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken)
  118. {
  119. series.SetProviderId(MetadataProviders.Tvdb, seriesId);
  120. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
  121. var seriesXmlFilename = series.GetPreferredMetadataLanguage().ToLower() + ".xml";
  122. var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename);
  123. var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
  124. FetchSeriesInfo(series, seriesXmlPath, cancellationToken);
  125. cancellationToken.ThrowIfCancellationRequested();
  126. FetchActors(series, actorsXmlPath);
  127. }
  128. /// <summary>
  129. /// Downloads the series zip.
  130. /// </summary>
  131. /// <param name="seriesId">The series id.</param>
  132. /// <param name="seriesDataPath">The series data path.</param>
  133. /// <param name="lastTvDbUpdateTime">The last tv database update time.</param>
  134. /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
  135. /// <param name="cancellationToken">The cancellation token.</param>
  136. /// <returns>Task.</returns>
  137. internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken)
  138. {
  139. var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, preferredMetadataLanguage);
  140. using (var zipStream = await _httpClient.Get(new HttpRequestOptions
  141. {
  142. Url = url,
  143. ResourcePool = TvDbResourcePool,
  144. CancellationToken = cancellationToken
  145. }).ConfigureAwait(false))
  146. {
  147. // Delete existing files
  148. DeleteXmlFiles(seriesDataPath);
  149. // Copy to memory stream because we need a seekable stream
  150. using (var ms = new MemoryStream())
  151. {
  152. await zipStream.CopyToAsync(ms).ConfigureAwait(false);
  153. ms.Position = 0;
  154. _zipClient.ExtractAll(ms, seriesDataPath, true);
  155. }
  156. }
  157. // Sanitize all files, except for extracted episode files
  158. foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList()
  159. .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase)))
  160. {
  161. await SanitizeXmlFile(file).ConfigureAwait(false);
  162. }
  163. await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, preferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false);
  164. }
  165. private readonly Task _cachedTask = Task.FromResult(true);
  166. internal Task EnsureSeriesInfo(string seriesId, string preferredMetadataLanguage, CancellationToken cancellationToken)
  167. {
  168. var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
  169. Directory.CreateDirectory(seriesDataPath);
  170. var files = new DirectoryInfo(seriesDataPath).EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
  171. .ToList();
  172. var seriesXmlFilename = preferredMetadataLanguage + ".xml";
  173. var download = false;
  174. var automaticUpdatesEnabled = _config.Configuration.EnableTvDbUpdates;
  175. const int cacheDays = 3;
  176. var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
  177. // No need to check age if automatic updates are enabled
  178. if (seriesFile == null || !seriesFile.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays))
  179. {
  180. download = true;
  181. }
  182. var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  183. // No need to check age if automatic updates are enabled
  184. if (actorsXml == null || !actorsXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays))
  185. {
  186. download = true;
  187. }
  188. var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase));
  189. // No need to check age if automatic updates are enabled
  190. if (bannersXml == null || !bannersXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays))
  191. {
  192. download = true;
  193. }
  194. // Only download if not already there
  195. // The post-scan task will take care of updates so we don't need to re-download here
  196. if (download)
  197. {
  198. return DownloadSeriesZip(seriesId, seriesDataPath, null, preferredMetadataLanguage, cancellationToken);
  199. }
  200. return _cachedTask;
  201. }
  202. /// <summary>
  203. /// Finds the series.
  204. /// </summary>
  205. /// <param name="name">The name.</param>
  206. /// <param name="cancellationToken">The cancellation token.</param>
  207. /// <returns>Task{System.String}.</returns>
  208. private async Task<string> FindSeries(string name, CancellationToken cancellationToken)
  209. {
  210. var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name));
  211. var doc = new XmlDocument();
  212. using (var results = await _httpClient.Get(new HttpRequestOptions
  213. {
  214. Url = url,
  215. ResourcePool = TvDbResourcePool,
  216. CancellationToken = cancellationToken
  217. }).ConfigureAwait(false))
  218. {
  219. doc.Load(results);
  220. }
  221. if (doc.HasChildNodes)
  222. {
  223. var nodes = doc.SelectNodes("//Series");
  224. var comparableName = GetComparableName(name);
  225. if (nodes != null)
  226. {
  227. foreach (XmlNode node in nodes)
  228. {
  229. var titles = new List<string>();
  230. var nameNode = node.SelectSingleNode("./SeriesName");
  231. if (nameNode != null)
  232. {
  233. titles.Add(GetComparableName(nameNode.InnerText));
  234. }
  235. var aliasNode = node.SelectSingleNode("./AliasNames");
  236. if (aliasNode != null)
  237. {
  238. var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
  239. titles.AddRange(alias);
  240. }
  241. if (titles.Any(t => string.Equals(t, comparableName, StringComparison.OrdinalIgnoreCase)))
  242. {
  243. var id = node.SelectSingleNode("./seriesid");
  244. if (id != null)
  245. return id.InnerText;
  246. }
  247. foreach (var title in titles)
  248. {
  249. _logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
  250. }
  251. }
  252. }
  253. }
  254. _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
  255. return null;
  256. }
  257. /// <summary>
  258. /// The remove
  259. /// </summary>
  260. const string remove = "\"'!`?";
  261. /// <summary>
  262. /// The spacers
  263. /// </summary>
  264. const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
  265. /// <summary>
  266. /// Gets the name of the comparable.
  267. /// </summary>
  268. /// <param name="name">The name.</param>
  269. /// <returns>System.String.</returns>
  270. internal static string GetComparableName(string name)
  271. {
  272. name = name.ToLower();
  273. name = name.Normalize(NormalizationForm.FormKD);
  274. var sb = new StringBuilder();
  275. foreach (var c in name)
  276. {
  277. if ((int)c >= 0x2B0 && (int)c <= 0x0333)
  278. {
  279. // skip char modifier and diacritics
  280. }
  281. else if (remove.IndexOf(c) > -1)
  282. {
  283. // skip chars we are removing
  284. }
  285. else if (spacers.IndexOf(c) > -1)
  286. {
  287. sb.Append(" ");
  288. }
  289. else if (c == '&')
  290. {
  291. sb.Append(" and ");
  292. }
  293. else
  294. {
  295. sb.Append(c);
  296. }
  297. }
  298. name = sb.ToString();
  299. name = name.Replace(", the", "");
  300. name = name.Replace("the ", " ");
  301. name = name.Replace(" the ", " ");
  302. string prevName;
  303. do
  304. {
  305. prevName = name;
  306. name = name.Replace(" ", " ");
  307. } while (name.Length != prevName.Length);
  308. return name.Trim();
  309. }
  310. private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken)
  311. {
  312. var settings = new XmlReaderSettings
  313. {
  314. CheckCharacters = false,
  315. IgnoreProcessingInstructions = true,
  316. IgnoreComments = true,
  317. ValidationType = ValidationType.None
  318. };
  319. var episiodeAirDates = new List<DateTime>();
  320. using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
  321. {
  322. // Use XmlReader for best performance
  323. using (var reader = XmlReader.Create(streamReader, settings))
  324. {
  325. reader.MoveToContent();
  326. // Loop through each element
  327. while (reader.Read())
  328. {
  329. cancellationToken.ThrowIfCancellationRequested();
  330. if (reader.NodeType == XmlNodeType.Element)
  331. {
  332. switch (reader.Name)
  333. {
  334. case "Series":
  335. {
  336. using (var subtree = reader.ReadSubtree())
  337. {
  338. FetchDataFromSeriesNode(item, subtree, cancellationToken);
  339. }
  340. break;
  341. }
  342. case "Episode":
  343. {
  344. using (var subtree = reader.ReadSubtree())
  345. {
  346. var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
  347. if (date.HasValue)
  348. {
  349. episiodeAirDates.Add(date.Value);
  350. }
  351. }
  352. break;
  353. }
  354. default:
  355. reader.Skip();
  356. break;
  357. }
  358. }
  359. }
  360. }
  361. }
  362. if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
  363. {
  364. item.EndDate = episiodeAirDates.Max();
  365. }
  366. }
  367. private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
  368. {
  369. DateTime? airDate = null;
  370. int? seasonNumber = null;
  371. reader.MoveToContent();
  372. // Loop through each element
  373. while (reader.Read())
  374. {
  375. cancellationToken.ThrowIfCancellationRequested();
  376. if (reader.NodeType == XmlNodeType.Element)
  377. {
  378. switch (reader.Name)
  379. {
  380. case "FirstAired":
  381. {
  382. var val = reader.ReadElementContentAsString();
  383. if (!string.IsNullOrWhiteSpace(val))
  384. {
  385. DateTime date;
  386. if (DateTime.TryParse(val, out date))
  387. {
  388. airDate = date.ToUniversalTime();
  389. }
  390. }
  391. break;
  392. }
  393. case "SeasonNumber":
  394. {
  395. var val = reader.ReadElementContentAsString();
  396. if (!string.IsNullOrWhiteSpace(val))
  397. {
  398. int rval;
  399. // int.TryParse is local aware, so it can be probamatic, force us culture
  400. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  401. {
  402. seasonNumber = rval;
  403. }
  404. }
  405. break;
  406. }
  407. default:
  408. reader.Skip();
  409. break;
  410. }
  411. }
  412. }
  413. if (seasonNumber.HasValue && seasonNumber.Value != 0)
  414. {
  415. return airDate;
  416. }
  417. return null;
  418. }
  419. /// <summary>
  420. /// Fetches the actors.
  421. /// </summary>
  422. /// <param name="series">The series.</param>
  423. /// <param name="actorsXmlPath">The actors XML path.</param>
  424. private void FetchActors(Series series, string actorsXmlPath)
  425. {
  426. var settings = new XmlReaderSettings
  427. {
  428. CheckCharacters = false,
  429. IgnoreProcessingInstructions = true,
  430. IgnoreComments = true,
  431. ValidationType = ValidationType.None
  432. };
  433. using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
  434. {
  435. // Use XmlReader for best performance
  436. using (var reader = XmlReader.Create(streamReader, settings))
  437. {
  438. reader.MoveToContent();
  439. // Loop through each element
  440. while (reader.Read())
  441. {
  442. if (reader.NodeType == XmlNodeType.Element)
  443. {
  444. switch (reader.Name)
  445. {
  446. case "Actor":
  447. {
  448. using (var subtree = reader.ReadSubtree())
  449. {
  450. FetchDataFromActorNode(series, subtree);
  451. }
  452. break;
  453. }
  454. default:
  455. reader.Skip();
  456. break;
  457. }
  458. }
  459. }
  460. }
  461. }
  462. }
  463. /// <summary>
  464. /// Fetches the data from actor node.
  465. /// </summary>
  466. /// <param name="series">The series.</param>
  467. /// <param name="reader">The reader.</param>
  468. private void FetchDataFromActorNode(Series series, XmlReader reader)
  469. {
  470. reader.MoveToContent();
  471. var personInfo = new PersonInfo();
  472. while (reader.Read())
  473. {
  474. if (reader.NodeType == XmlNodeType.Element)
  475. {
  476. switch (reader.Name)
  477. {
  478. case "Name":
  479. {
  480. personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  481. break;
  482. }
  483. case "Role":
  484. {
  485. personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  486. break;
  487. }
  488. case "SortOrder":
  489. {
  490. var val = reader.ReadElementContentAsString();
  491. if (!string.IsNullOrWhiteSpace(val))
  492. {
  493. int rval;
  494. // int.TryParse is local aware, so it can be probamatic, force us culture
  495. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  496. {
  497. personInfo.SortOrder = rval;
  498. }
  499. }
  500. break;
  501. }
  502. default:
  503. reader.Skip();
  504. break;
  505. }
  506. }
  507. }
  508. personInfo.Type = PersonType.Actor;
  509. if (!string.IsNullOrEmpty(personInfo.Name))
  510. {
  511. series.AddPerson(personInfo);
  512. }
  513. }
  514. private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken)
  515. {
  516. reader.MoveToContent();
  517. // Loop through each element
  518. while (reader.Read())
  519. {
  520. cancellationToken.ThrowIfCancellationRequested();
  521. if (reader.NodeType == XmlNodeType.Element)
  522. {
  523. switch (reader.Name)
  524. {
  525. case "SeriesName":
  526. {
  527. item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  528. break;
  529. }
  530. case "Overview":
  531. {
  532. item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
  533. break;
  534. }
  535. case "Airs_DayOfWeek":
  536. {
  537. var val = reader.ReadElementContentAsString();
  538. if (!string.IsNullOrWhiteSpace(val))
  539. {
  540. item.AirDays = TVUtils.GetAirDays(val);
  541. }
  542. break;
  543. }
  544. case "Airs_Time":
  545. {
  546. var val = reader.ReadElementContentAsString();
  547. if (!string.IsNullOrWhiteSpace(val))
  548. {
  549. item.AirTime = val;
  550. }
  551. break;
  552. }
  553. case "ContentRating":
  554. {
  555. var val = reader.ReadElementContentAsString();
  556. if (!string.IsNullOrWhiteSpace(val))
  557. {
  558. item.OfficialRating = val;
  559. }
  560. break;
  561. }
  562. case "Rating":
  563. {
  564. var val = reader.ReadElementContentAsString();
  565. if (!string.IsNullOrWhiteSpace(val))
  566. {
  567. // Only fill this if it doesn't already have a value, since we get it from imdb which has better data
  568. if (!item.CommunityRating.HasValue || string.IsNullOrWhiteSpace(item.GetProviderId(MetadataProviders.Imdb)))
  569. {
  570. float rval;
  571. // float.TryParse is local aware, so it can be probamatic, force us culture
  572. if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval))
  573. {
  574. item.CommunityRating = rval;
  575. }
  576. }
  577. }
  578. break;
  579. }
  580. case "RatingCount":
  581. {
  582. var val = reader.ReadElementContentAsString();
  583. if (!string.IsNullOrWhiteSpace(val))
  584. {
  585. int rval;
  586. // int.TryParse is local aware, so it can be probamatic, force us culture
  587. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  588. {
  589. item.VoteCount = rval;
  590. }
  591. }
  592. break;
  593. }
  594. case "IMDB_ID":
  595. {
  596. var val = reader.ReadElementContentAsString();
  597. if (!string.IsNullOrWhiteSpace(val))
  598. {
  599. item.SetProviderId(MetadataProviders.Imdb, val);
  600. }
  601. break;
  602. }
  603. case "zap2it_id":
  604. {
  605. var val = reader.ReadElementContentAsString();
  606. if (!string.IsNullOrWhiteSpace(val))
  607. {
  608. item.SetProviderId(MetadataProviders.Zap2It, val);
  609. }
  610. break;
  611. }
  612. case "Status":
  613. {
  614. var val = reader.ReadElementContentAsString();
  615. if (!string.IsNullOrWhiteSpace(val))
  616. {
  617. SeriesStatus seriesStatus;
  618. if (Enum.TryParse(val, true, out seriesStatus))
  619. item.Status = seriesStatus;
  620. }
  621. break;
  622. }
  623. case "FirstAired":
  624. {
  625. var val = reader.ReadElementContentAsString();
  626. if (!string.IsNullOrWhiteSpace(val))
  627. {
  628. DateTime date;
  629. if (DateTime.TryParse(val, out date))
  630. {
  631. date = date.ToUniversalTime();
  632. item.PremiereDate = date;
  633. item.ProductionYear = date.Year;
  634. }
  635. }
  636. break;
  637. }
  638. case "Runtime":
  639. {
  640. var val = reader.ReadElementContentAsString();
  641. if (!string.IsNullOrWhiteSpace(val))
  642. {
  643. int rval;
  644. // int.TryParse is local aware, so it can be probamatic, force us culture
  645. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
  646. {
  647. item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks;
  648. }
  649. }
  650. break;
  651. }
  652. case "Genre":
  653. {
  654. var val = reader.ReadElementContentAsString();
  655. if (!string.IsNullOrWhiteSpace(val))
  656. {
  657. var vals = val
  658. .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
  659. .Select(i => i.Trim())
  660. .Where(i => !string.IsNullOrWhiteSpace(i))
  661. .ToList();
  662. if (vals.Count > 0)
  663. {
  664. item.Genres.Clear();
  665. foreach (var genre in vals)
  666. {
  667. item.AddGenre(genre);
  668. }
  669. }
  670. }
  671. break;
  672. }
  673. case "Network":
  674. {
  675. var val = reader.ReadElementContentAsString();
  676. if (!string.IsNullOrWhiteSpace(val))
  677. {
  678. var vals = val
  679. .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
  680. .Select(i => i.Trim())
  681. .Where(i => !string.IsNullOrWhiteSpace(i))
  682. .ToList();
  683. if (vals.Count > 0)
  684. {
  685. item.Studios.Clear();
  686. foreach (var genre in vals)
  687. {
  688. item.AddStudio(genre);
  689. }
  690. }
  691. }
  692. break;
  693. }
  694. default:
  695. reader.Skip();
  696. break;
  697. }
  698. }
  699. }
  700. }
  701. /// <summary>
  702. /// 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
  703. /// </summary>
  704. /// <param name="seriesDataPath">The series data path.</param>
  705. /// <param name="xmlFile">The XML file.</param>
  706. /// <param name="lastTvDbUpdateTime">The last tv db update time.</param>
  707. /// <returns>Task.</returns>
  708. private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime)
  709. {
  710. var settings = new XmlReaderSettings
  711. {
  712. CheckCharacters = false,
  713. IgnoreProcessingInstructions = true,
  714. IgnoreComments = true,
  715. ValidationType = ValidationType.None
  716. };
  717. using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
  718. {
  719. // Use XmlReader for best performance
  720. using (var reader = XmlReader.Create(streamReader, settings))
  721. {
  722. reader.MoveToContent();
  723. // Loop through each element
  724. while (reader.Read())
  725. {
  726. if (reader.NodeType == XmlNodeType.Element)
  727. {
  728. switch (reader.Name)
  729. {
  730. case "Episode":
  731. {
  732. var outerXml = reader.ReadOuterXml();
  733. await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false);
  734. break;
  735. }
  736. default:
  737. reader.Skip();
  738. break;
  739. }
  740. }
  741. }
  742. }
  743. }
  744. }
  745. private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime)
  746. {
  747. var settings = new XmlReaderSettings
  748. {
  749. CheckCharacters = false,
  750. IgnoreProcessingInstructions = true,
  751. IgnoreComments = true,
  752. ValidationType = ValidationType.None
  753. };
  754. var seasonNumber = -1;
  755. var episodeNumber = -1;
  756. var absoluteNumber = -1;
  757. var lastUpdateString = string.Empty;
  758. using (var streamReader = new StringReader(xml))
  759. {
  760. // Use XmlReader for best performance
  761. using (var reader = XmlReader.Create(streamReader, settings))
  762. {
  763. reader.MoveToContent();
  764. // Loop through each element
  765. while (reader.Read())
  766. {
  767. if (reader.NodeType == XmlNodeType.Element)
  768. {
  769. switch (reader.Name)
  770. {
  771. case "lastupdated":
  772. {
  773. lastUpdateString = reader.ReadElementContentAsString();
  774. break;
  775. }
  776. case "EpisodeNumber":
  777. {
  778. var val = reader.ReadElementContentAsString();
  779. if (!string.IsNullOrWhiteSpace(val))
  780. {
  781. int num;
  782. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  783. {
  784. episodeNumber = num;
  785. }
  786. }
  787. break;
  788. }
  789. case "absolute_number":
  790. {
  791. var val = reader.ReadElementContentAsString();
  792. if (!string.IsNullOrWhiteSpace(val))
  793. {
  794. int num;
  795. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  796. {
  797. absoluteNumber = num;
  798. }
  799. }
  800. break;
  801. }
  802. case "SeasonNumber":
  803. {
  804. var val = reader.ReadElementContentAsString();
  805. if (!string.IsNullOrWhiteSpace(val))
  806. {
  807. int num;
  808. if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
  809. {
  810. seasonNumber = num;
  811. }
  812. }
  813. break;
  814. }
  815. default:
  816. reader.Skip();
  817. break;
  818. }
  819. }
  820. }
  821. }
  822. }
  823. var hasEpisodeChanged = true;
  824. if (!string.IsNullOrEmpty(lastUpdateString) && lastTvDbUpdateTime.HasValue)
  825. {
  826. long num;
  827. if (long.TryParse(lastUpdateString, NumberStyles.Any, _usCulture, out num))
  828. {
  829. hasEpisodeChanged = num >= lastTvDbUpdateTime.Value;
  830. }
  831. }
  832. var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber));
  833. // Only save the file if not already there, or if the episode has changed
  834. if (hasEpisodeChanged || !File.Exists(file))
  835. {
  836. using (var writer = XmlWriter.Create(file, new XmlWriterSettings
  837. {
  838. Encoding = Encoding.UTF8,
  839. Async = true
  840. }))
  841. {
  842. await writer.WriteRawAsync(xml).ConfigureAwait(false);
  843. }
  844. }
  845. if (absoluteNumber != -1)
  846. {
  847. file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber));
  848. // Only save the file if not already there, or if the episode has changed
  849. if (hasEpisodeChanged || !File.Exists(file))
  850. {
  851. using (var writer = XmlWriter.Create(file, new XmlWriterSettings
  852. {
  853. Encoding = Encoding.UTF8,
  854. Async = true
  855. }))
  856. {
  857. await writer.WriteRawAsync(xml).ConfigureAwait(false);
  858. }
  859. }
  860. }
  861. }
  862. /// <summary>
  863. /// Gets the series data path.
  864. /// </summary>
  865. /// <param name="appPaths">The app paths.</param>
  866. /// <param name="seriesId">The series id.</param>
  867. /// <returns>System.String.</returns>
  868. internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
  869. {
  870. var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
  871. return seriesDataPath;
  872. }
  873. /// <summary>
  874. /// Gets the series data path.
  875. /// </summary>
  876. /// <param name="appPaths">The app paths.</param>
  877. /// <returns>System.String.</returns>
  878. internal static string GetSeriesDataPath(IApplicationPaths appPaths)
  879. {
  880. var dataPath = Path.Combine(appPaths.CachePath, "tvdb");
  881. return dataPath;
  882. }
  883. private void DeleteXmlFiles(string path)
  884. {
  885. try
  886. {
  887. foreach (var file in new DirectoryInfo(path)
  888. .EnumerateFiles("*.xml", SearchOption.AllDirectories)
  889. .ToList())
  890. {
  891. file.Delete();
  892. }
  893. }
  894. catch (DirectoryNotFoundException)
  895. {
  896. // No biggie
  897. }
  898. }
  899. /// <summary>
  900. /// Sanitizes the XML file.
  901. /// </summary>
  902. /// <param name="file">The file.</param>
  903. /// <returns>Task.</returns>
  904. private async Task SanitizeXmlFile(string file)
  905. {
  906. string validXml;
  907. using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  908. {
  909. using (var reader = new StreamReader(fileStream))
  910. {
  911. var xml = await reader.ReadToEndAsync().ConfigureAwait(false);
  912. validXml = StripInvalidXmlCharacters(xml);
  913. }
  914. }
  915. using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true))
  916. {
  917. using (var writer = new StreamWriter(fileStream))
  918. {
  919. await writer.WriteAsync(validXml).ConfigureAwait(false);
  920. }
  921. }
  922. }
  923. /// <summary>
  924. /// Strips the invalid XML characters.
  925. /// </summary>
  926. /// <param name="inString">The in string.</param>
  927. /// <returns>System.String.</returns>
  928. public static string StripInvalidXmlCharacters(string inString)
  929. {
  930. if (inString == null) return null;
  931. var sbOutput = new StringBuilder();
  932. char ch;
  933. for (int i = 0; i < inString.Length; i++)
  934. {
  935. ch = inString[i];
  936. if ((ch >= 0x0020 && ch <= 0xD7FF) ||
  937. (ch >= 0xE000 && ch <= 0xFFFD) ||
  938. ch == 0x0009 ||
  939. ch == 0x000A ||
  940. ch == 0x000D)
  941. {
  942. sbOutput.Append(ch);
  943. }
  944. }
  945. return sbOutput.ToString();
  946. }
  947. public string Name
  948. {
  949. get { return "TheTVDB"; }
  950. }
  951. public int Order
  952. {
  953. get
  954. {
  955. // After Omdb
  956. return 1;
  957. }
  958. }
  959. public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
  960. {
  961. return _httpClient.GetResponse(new HttpRequestOptions
  962. {
  963. CancellationToken = cancellationToken,
  964. Url = url,
  965. ResourcePool = TvDbResourcePool
  966. });
  967. }
  968. }
  969. }