TvdbSeriesProvider.cs 48 KB

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