TvdbSeriesProvider.cs 47 KB

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