MovieDbProvider.cs 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Entities.Movies;
  6. using MediaBrowser.Controller.Providers;
  7. using MediaBrowser.Model.Entities;
  8. using MediaBrowser.Model.Logging;
  9. using MediaBrowser.Model.Serialization;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Globalization;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Net;
  16. using System.Text;
  17. using System.Text.RegularExpressions;
  18. using System.Threading;
  19. using System.Threading.Tasks;
  20. namespace MediaBrowser.Providers.Movies
  21. {
  22. /// <summary>
  23. /// Class MovieDbProvider
  24. /// </summary>
  25. public class MovieDbProvider : BaseMetadataProvider, IDisposable
  26. {
  27. protected static CultureInfo EnUs = new CultureInfo("en-US");
  28. protected readonly IProviderManager ProviderManager;
  29. /// <summary>
  30. /// The movie db
  31. /// </summary>
  32. private readonly SemaphoreSlim _movieDbResourcePool = new SemaphoreSlim(1,1);
  33. internal static MovieDbProvider Current { get; private set; }
  34. /// <summary>
  35. /// Gets the json serializer.
  36. /// </summary>
  37. /// <value>The json serializer.</value>
  38. protected IJsonSerializer JsonSerializer { get; private set; }
  39. /// <summary>
  40. /// Gets the HTTP client.
  41. /// </summary>
  42. /// <value>The HTTP client.</value>
  43. protected IHttpClient HttpClient { get; private set; }
  44. /// <summary>
  45. /// Initializes a new instance of the <see cref="MovieDbProvider" /> class.
  46. /// </summary>
  47. /// <param name="logManager">The log manager.</param>
  48. /// <param name="configurationManager">The configuration manager.</param>
  49. /// <param name="jsonSerializer">The json serializer.</param>
  50. /// <param name="httpClient">The HTTP client.</param>
  51. /// <param name="providerManager">The provider manager.</param>
  52. public MovieDbProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, IProviderManager providerManager)
  53. : base(logManager, configurationManager)
  54. {
  55. JsonSerializer = jsonSerializer;
  56. HttpClient = httpClient;
  57. ProviderManager = providerManager;
  58. Current = this;
  59. }
  60. /// <summary>
  61. /// Releases unmanaged and - optionally - managed resources.
  62. /// </summary>
  63. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  64. protected virtual void Dispose(bool dispose)
  65. {
  66. if (dispose)
  67. {
  68. _movieDbResourcePool.Dispose();
  69. }
  70. }
  71. /// <summary>
  72. /// Gets the priority.
  73. /// </summary>
  74. /// <value>The priority.</value>
  75. public override MetadataProviderPriority Priority
  76. {
  77. get { return MetadataProviderPriority.Third; }
  78. }
  79. /// <summary>
  80. /// Supportses the specified item.
  81. /// </summary>
  82. /// <param name="item">The item.</param>
  83. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  84. public override bool Supports(BaseItem item)
  85. {
  86. var trailer = item as Trailer;
  87. if (trailer != null)
  88. {
  89. return !trailer.IsLocalTrailer;
  90. }
  91. // Don't support local trailers
  92. return item is Movie || item is BoxSet || item is MusicVideo;
  93. }
  94. /// <summary>
  95. /// Gets a value indicating whether [requires internet].
  96. /// </summary>
  97. /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
  98. public override bool RequiresInternet
  99. {
  100. get
  101. {
  102. return true;
  103. }
  104. }
  105. protected override bool RefreshOnVersionChange
  106. {
  107. get
  108. {
  109. return true;
  110. }
  111. }
  112. protected override string ProviderVersion
  113. {
  114. get
  115. {
  116. return "2";
  117. }
  118. }
  119. /// <summary>
  120. /// The _TMDB settings task
  121. /// </summary>
  122. private TmdbSettingsResult _tmdbSettings;
  123. private readonly SemaphoreSlim _tmdbSettingsSemaphore = new SemaphoreSlim(1, 1);
  124. /// <summary>
  125. /// Gets the TMDB settings.
  126. /// </summary>
  127. /// <returns>Task{TmdbSettingsResult}.</returns>
  128. internal async Task<TmdbSettingsResult> GetTmdbSettings(CancellationToken cancellationToken)
  129. {
  130. if (_tmdbSettings != null)
  131. {
  132. return _tmdbSettings;
  133. }
  134. await _tmdbSettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  135. // Check again in case it got populated while we were waiting.
  136. if (_tmdbSettings != null)
  137. {
  138. _tmdbSettingsSemaphore.Release();
  139. return _tmdbSettings;
  140. }
  141. try
  142. {
  143. using (var json = await GetMovieDbResponse(new HttpRequestOptions
  144. {
  145. Url = string.Format(TmdbConfigUrl, ApiKey),
  146. CancellationToken = cancellationToken,
  147. AcceptHeader = AcceptHeader
  148. }).ConfigureAwait(false))
  149. {
  150. _tmdbSettings = JsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
  151. return _tmdbSettings;
  152. }
  153. }
  154. finally
  155. {
  156. _tmdbSettingsSemaphore.Release();
  157. }
  158. }
  159. private const string TmdbConfigUrl = "http://api.themoviedb.org/3/configuration?api_key={0}";
  160. private const string Search3 = @"http://api.themoviedb.org/3/search/movie?api_key={1}&query={0}&language={2}";
  161. private const string AltTitleSearch = @"http://api.themoviedb.org/3/movie/{0}/alternative_titles?api_key={1}&country={2}";
  162. private const string GetMovieInfo3 = @"http://api.themoviedb.org/3/movie/{0}?api_key={1}&language={2}&append_to_response=casts,releases,images,keywords,trailers";
  163. private const string GetBoxSetInfo3 = @"http://api.themoviedb.org/3/collection/{0}?api_key={1}&language={2}&append_to_response=images";
  164. internal static string ApiKey = "f6bd687ffa63cd282b6ff2c6877f2669";
  165. internal static string AcceptHeader = "application/json,image/*";
  166. static readonly Regex[] NameMatches = new[] {
  167. new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year
  168. new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
  169. };
  170. public const string AltMetaFileName = "movie.xml";
  171. protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
  172. {
  173. if (HasAltMeta(item))
  174. return false;
  175. return base.NeedsRefreshInternal(item, providerInfo);
  176. }
  177. /// <summary>
  178. /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
  179. /// </summary>
  180. /// <param name="item">The item.</param>
  181. /// <param name="force">if set to <c>true</c> [force].</param>
  182. /// <param name="cancellationToken">The cancellation token</param>
  183. /// <returns>Task{System.Boolean}.</returns>
  184. public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
  185. {
  186. cancellationToken.ThrowIfCancellationRequested();
  187. await FetchMovieData(item, cancellationToken).ConfigureAwait(false);
  188. SetLastRefreshed(item, DateTime.UtcNow);
  189. return true;
  190. }
  191. /// <summary>
  192. /// Determines whether [has alt meta] [the specified item].
  193. /// </summary>
  194. /// <param name="item">The item.</param>
  195. /// <returns><c>true</c> if [has alt meta] [the specified item]; otherwise, <c>false</c>.</returns>
  196. internal static bool HasAltMeta(BaseItem item)
  197. {
  198. if (item is BoxSet)
  199. {
  200. return item.LocationType == LocationType.FileSystem && item.ResolveArgs.ContainsMetaFileByName("collection.xml");
  201. }
  202. return item.LocationType == LocationType.FileSystem && item.ResolveArgs.ContainsMetaFileByName(AltMetaFileName);
  203. }
  204. /// <summary>
  205. /// Fetches the movie data.
  206. /// </summary>
  207. /// <param name="item">The item.</param>
  208. /// <param name="cancellationToken"></param>
  209. /// <returns>Task.</returns>
  210. private async Task FetchMovieData(BaseItem item, CancellationToken cancellationToken)
  211. {
  212. string id = item.GetProviderId(MetadataProviders.Tmdb) ?? await FindId(item, item.ProductionYear, cancellationToken).ConfigureAwait(false);
  213. if (id != null)
  214. {
  215. Logger.Debug("MovieDbProvider - getting movie info with id: " + id);
  216. cancellationToken.ThrowIfCancellationRequested();
  217. await FetchMovieData(item, id, cancellationToken).ConfigureAwait(false);
  218. }
  219. else
  220. {
  221. Logger.Info("MovieDBProvider could not find " + item.Name + ". Check name on themoviedb.org.");
  222. }
  223. }
  224. /// <summary>
  225. /// Parses the name.
  226. /// </summary>
  227. /// <param name="name">The name.</param>
  228. /// <param name="justName">Name of the just.</param>
  229. /// <param name="year">The year.</param>
  230. protected void ParseName(string name, out string justName, out int? year)
  231. {
  232. justName = null;
  233. year = null;
  234. foreach (var re in NameMatches)
  235. {
  236. Match m = re.Match(name);
  237. if (m.Success)
  238. {
  239. justName = m.Groups["name"].Value.Trim();
  240. string y = m.Groups["year"] != null ? m.Groups["year"].Value : null;
  241. int temp;
  242. year = Int32.TryParse(y, out temp) ? temp : (int?)null;
  243. break;
  244. }
  245. }
  246. }
  247. /// <summary>
  248. /// Finds the id.
  249. /// </summary>
  250. /// <param name="item">The item.</param>
  251. /// <param name="productionYear">The production year.</param>
  252. /// <param name="cancellationToken">The cancellation token</param>
  253. /// <returns>Task{System.String}.</returns>
  254. public async Task<string> FindId(BaseItem item, int? productionYear, CancellationToken cancellationToken)
  255. {
  256. string id = null;
  257. if (item.LocationType == LocationType.FileSystem)
  258. {
  259. string justName = item.Path != null ? item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar)) : string.Empty;
  260. id = justName.GetAttributeValue("tmdbid");
  261. if (id != null)
  262. {
  263. Logger.Debug("Using tmdb id specified in path.");
  264. return id;
  265. }
  266. }
  267. int? year;
  268. string name = item.Name;
  269. ParseName(name, out name, out year);
  270. if (year == null && productionYear != null)
  271. {
  272. year = productionYear;
  273. }
  274. Logger.Info("MovieDbProvider: Finding id for movie: " + name);
  275. string language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower();
  276. //if we are a boxset - look at our first child
  277. var boxset = item as BoxSet;
  278. if (boxset != null)
  279. {
  280. var firstChild = boxset.Children.FirstOrDefault();
  281. if (firstChild != null)
  282. {
  283. Logger.Debug("MovieDbProvider - Attempting to find boxset ID from: " + firstChild.Name);
  284. string childName;
  285. int? childYear;
  286. ParseName(firstChild.Name, out childName, out childYear);
  287. id = await GetBoxsetIdFromMovie(childName, childYear, language, cancellationToken).ConfigureAwait(false);
  288. if (id != null)
  289. {
  290. Logger.Info("MovieDbProvider - Found Boxset ID: " + id);
  291. }
  292. }
  293. return id;
  294. }
  295. //nope - search for it
  296. id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
  297. if (id == null)
  298. {
  299. //try in english if wasn't before
  300. if (language != "en")
  301. {
  302. id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
  303. }
  304. else
  305. {
  306. // try with dot and _ turned to space
  307. var originalName = name;
  308. name = name.Replace(",", " ");
  309. name = name.Replace(".", " ");
  310. name = name.Replace("_", " ");
  311. name = name.Replace("-", "");
  312. // Search again if the new name is different
  313. if (!string.Equals(name, originalName))
  314. {
  315. id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
  316. if (id == null && language != "en")
  317. {
  318. //one more time, in english
  319. id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
  320. }
  321. }
  322. if (id == null && item.LocationType == LocationType.FileSystem)
  323. {
  324. //last resort - try using the actual folder name
  325. var pathName = Path.GetFileName(item.ResolveArgs.Path);
  326. // Only search if it's a name we haven't already tried.
  327. if (!string.Equals(pathName, name, StringComparison.OrdinalIgnoreCase)
  328. && !string.Equals(pathName, originalName, StringComparison.OrdinalIgnoreCase))
  329. {
  330. id = await AttemptFindId(pathName, year, "en", cancellationToken).ConfigureAwait(false);
  331. }
  332. }
  333. }
  334. }
  335. return id;
  336. }
  337. /// <summary>
  338. /// Attempts the find id.
  339. /// </summary>
  340. /// <param name="name">The name.</param>
  341. /// <param name="year">The year.</param>
  342. /// <param name="language">The language.</param>
  343. /// <param name="cancellationToken">The cancellation token</param>
  344. /// <returns>Task{System.String}.</returns>
  345. public virtual async Task<string> AttemptFindId(string name, int? year, string language, CancellationToken cancellationToken)
  346. {
  347. string url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
  348. TmdbMovieSearchResults searchResult = null;
  349. using (Stream json = await GetMovieDbResponse(new HttpRequestOptions
  350. {
  351. Url = url3,
  352. CancellationToken = cancellationToken,
  353. AcceptHeader = AcceptHeader
  354. }).ConfigureAwait(false))
  355. {
  356. searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
  357. }
  358. if (searchResult == null || searchResult.results.Count == 0)
  359. {
  360. //try replacing numbers
  361. foreach (var pair in ReplaceStartNumbers)
  362. {
  363. if (name.StartsWith(pair.Key))
  364. {
  365. name = name.Remove(0, pair.Key.Length);
  366. name = pair.Value + name;
  367. }
  368. }
  369. foreach (var pair in ReplaceEndNumbers)
  370. {
  371. if (name.EndsWith(pair.Key))
  372. {
  373. name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
  374. name = name + pair.Value;
  375. }
  376. }
  377. Logger.Info("MovieDBProvider - No results. Trying replacement numbers: " + name);
  378. url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
  379. using (var json = await GetMovieDbResponse(new HttpRequestOptions
  380. {
  381. Url = url3,
  382. CancellationToken = cancellationToken,
  383. AcceptHeader = AcceptHeader
  384. }).ConfigureAwait(false))
  385. {
  386. searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
  387. }
  388. }
  389. if (searchResult != null)
  390. {
  391. string compName = GetComparableName(name, Logger);
  392. foreach (var possible in searchResult.results)
  393. {
  394. string matchedName = null;
  395. string id = possible.id.ToString(CultureInfo.InvariantCulture);
  396. string n = possible.title;
  397. if (GetComparableName(n, Logger) == compName)
  398. {
  399. matchedName = n;
  400. }
  401. else
  402. {
  403. n = possible.original_title;
  404. if (GetComparableName(n, Logger) == compName)
  405. {
  406. matchedName = n;
  407. }
  408. }
  409. Logger.Debug("MovieDbProvider - " + compName + " didn't match " + n);
  410. //if main title matches we don't have to look for alternatives
  411. if (matchedName == null)
  412. {
  413. //that title didn't match - look for alternatives
  414. url3 = string.Format(AltTitleSearch, id, ApiKey, ConfigurationManager.Configuration.MetadataCountryCode);
  415. using (var json = await GetMovieDbResponse(new HttpRequestOptions
  416. {
  417. Url = url3,
  418. CancellationToken = cancellationToken,
  419. AcceptHeader = AcceptHeader
  420. }).ConfigureAwait(false))
  421. {
  422. var response = JsonSerializer.DeserializeFromStream<TmdbAltTitleResults>(json);
  423. if (response != null && response.titles != null)
  424. {
  425. foreach (var title in response.titles)
  426. {
  427. var t = GetComparableName(title.title, Logger);
  428. if (t == compName)
  429. {
  430. Logger.Debug("MovieDbProvider - " + compName +
  431. " matched " + t);
  432. matchedName = t;
  433. break;
  434. }
  435. Logger.Debug("MovieDbProvider - " + compName +
  436. " did not match " + t);
  437. }
  438. }
  439. }
  440. }
  441. if (matchedName != null)
  442. {
  443. Logger.Debug("Match " + matchedName + " for " + name);
  444. if (year != null)
  445. {
  446. DateTime r;
  447. //These dates are always in this exact format
  448. if (DateTime.TryParseExact(possible.release_date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out r))
  449. {
  450. if (Math.Abs(r.Year - year.Value) > 1) // allow a 1 year tolerance on release date
  451. {
  452. Logger.Debug("Result " + matchedName + " released on " + r + " did not match year " + year);
  453. continue;
  454. }
  455. }
  456. }
  457. //matched name and year
  458. return id;
  459. }
  460. }
  461. }
  462. return null;
  463. }
  464. /// <summary>
  465. /// URLs the encode.
  466. /// </summary>
  467. /// <param name="name">The name.</param>
  468. /// <returns>System.String.</returns>
  469. private static string UrlEncode(string name)
  470. {
  471. return WebUtility.UrlEncode(name);
  472. }
  473. /// <summary>
  474. /// Gets the boxset id from movie.
  475. /// </summary>
  476. /// <param name="name">The name.</param>
  477. /// <param name="year">The year.</param>
  478. /// <param name="language">The language.</param>
  479. /// <param name="cancellationToken">The cancellation token</param>
  480. /// <returns>Task{System.String}.</returns>
  481. protected async Task<string> GetBoxsetIdFromMovie(string name, int? year, string language, CancellationToken cancellationToken)
  482. {
  483. string id = null;
  484. string childId = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
  485. if (childId != null)
  486. {
  487. string url = string.Format(GetMovieInfo3, childId, ApiKey, language);
  488. using (Stream json = await GetMovieDbResponse(new HttpRequestOptions
  489. {
  490. Url = url,
  491. CancellationToken = cancellationToken,
  492. AcceptHeader = AcceptHeader
  493. }).ConfigureAwait(false))
  494. {
  495. var movieResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
  496. if (movieResult != null && movieResult.belongs_to_collection != null)
  497. {
  498. id = movieResult.belongs_to_collection.id.ToString(CultureInfo.InvariantCulture);
  499. }
  500. else
  501. {
  502. Logger.Error("Unable to obtain boxset id.");
  503. }
  504. }
  505. }
  506. return id;
  507. }
  508. /// <summary>
  509. /// Fetches the movie data.
  510. /// </summary>
  511. /// <param name="item">The item.</param>
  512. /// <param name="id">The id.</param>
  513. /// <param name="cancellationToken">The cancellation token</param>
  514. /// <returns>Task.</returns>
  515. protected async Task FetchMovieData(BaseItem item, string id, CancellationToken cancellationToken)
  516. {
  517. cancellationToken.ThrowIfCancellationRequested();
  518. if (String.IsNullOrEmpty(id))
  519. {
  520. Logger.Info("MoviedbProvider: Ignoring " + item.Name + " because ID forced blank.");
  521. return;
  522. }
  523. if (item.GetProviderId(MetadataProviders.Tmdb) == null) item.SetProviderId(MetadataProviders.Tmdb, id);
  524. var mainResult = await FetchMainResult(item, id, cancellationToken).ConfigureAwait(false);
  525. if (mainResult == null) return;
  526. ProcessMainInfo(item, mainResult);
  527. }
  528. /// <summary>
  529. /// Fetches the main result.
  530. /// </summary>
  531. /// <param name="item">The item.</param>
  532. /// <param name="id">The id.</param>
  533. /// <param name="cancellationToken">The cancellation token</param>
  534. /// <returns>Task{CompleteMovieData}.</returns>
  535. protected async Task<CompleteMovieData> FetchMainResult(BaseItem item, string id, CancellationToken cancellationToken)
  536. {
  537. var baseUrl = item is BoxSet ? GetBoxSetInfo3 : GetMovieInfo3;
  538. string url = string.Format(baseUrl, id, ApiKey, ConfigurationManager.Configuration.PreferredMetadataLanguage);
  539. CompleteMovieData mainResult;
  540. cancellationToken.ThrowIfCancellationRequested();
  541. using (var json = await GetMovieDbResponse(new HttpRequestOptions
  542. {
  543. Url = url,
  544. CancellationToken = cancellationToken,
  545. AcceptHeader = AcceptHeader
  546. }).ConfigureAwait(false))
  547. {
  548. mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
  549. }
  550. cancellationToken.ThrowIfCancellationRequested();
  551. if (mainResult != null && string.IsNullOrEmpty(mainResult.overview))
  552. {
  553. if (ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() != "en")
  554. {
  555. Logger.Info("MovieDbProvider couldn't find meta for language " + ConfigurationManager.Configuration.PreferredMetadataLanguage + ". Trying English...");
  556. url = string.Format(baseUrl, id, ApiKey, "en");
  557. using (Stream json = await GetMovieDbResponse(new HttpRequestOptions
  558. {
  559. Url = url,
  560. CancellationToken = cancellationToken,
  561. AcceptHeader = AcceptHeader
  562. }).ConfigureAwait(false))
  563. {
  564. mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
  565. }
  566. if (String.IsNullOrEmpty(mainResult.overview))
  567. {
  568. Logger.Error("MovieDbProvider - Unable to find information for " + item.Name + " (id:" + id + ")");
  569. return null;
  570. }
  571. }
  572. }
  573. return mainResult;
  574. }
  575. /// <summary>
  576. /// Processes the main info.
  577. /// </summary>
  578. /// <param name="movie">The movie.</param>
  579. /// <param name="movieData">The movie data.</param>
  580. protected virtual void ProcessMainInfo(BaseItem movie, CompleteMovieData movieData)
  581. {
  582. if (movie != null && movieData != null)
  583. {
  584. if (!movie.LockedFields.Contains(MetadataFields.Name))
  585. {
  586. movie.Name = movieData.title ?? movieData.original_title ?? movie.Name;
  587. }
  588. if (!movie.LockedFields.Contains(MetadataFields.Overview))
  589. {
  590. movie.Overview = WebUtility.HtmlDecode(movieData.overview);
  591. }
  592. movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
  593. movie.HomePageUrl = movieData.homepage;
  594. movie.Budget = movieData.budget;
  595. movie.Revenue = movieData.revenue;
  596. if (!string.IsNullOrEmpty(movieData.tagline))
  597. {
  598. movie.Taglines.Clear();
  599. movie.AddTagline(movieData.tagline);
  600. }
  601. movie.SetProviderId(MetadataProviders.Imdb, movieData.imdb_id);
  602. if (movieData.belongs_to_collection != null)
  603. {
  604. movie.SetProviderId(MetadataProviders.TmdbCollection, movieData.belongs_to_collection.id.ToString(CultureInfo.InvariantCulture));
  605. }
  606. float rating;
  607. string voteAvg = movieData.vote_average.ToString(CultureInfo.InvariantCulture);
  608. //tmdb appears to have unified their numbers to always report "7.3" regardless of country
  609. // so I removed the culture-specific processing here because it was not working for other countries -ebr
  610. if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating))
  611. movie.CommunityRating = rating;
  612. //release date and certification are retrieved based on configured country and we fall back on US if not there and to minimun release date if still no match
  613. if (movieData.releases != null && movieData.releases.countries != null)
  614. {
  615. var ourRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals(ConfigurationManager.Configuration.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country();
  616. var usRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)) ?? new Country();
  617. var minimunRelease = movieData.releases.countries.OrderBy(c => c.release_date).FirstOrDefault() ?? new Country();
  618. var ratingPrefix = ConfigurationManager.Configuration.MetadataCountryCode.Equals("us", StringComparison.OrdinalIgnoreCase) ? "" : ConfigurationManager.Configuration.MetadataCountryCode +"-";
  619. movie.OfficialRating = !string.IsNullOrEmpty(ourRelease.certification)
  620. ? ratingPrefix + ourRelease.certification
  621. : !string.IsNullOrEmpty(usRelease.certification)
  622. ? usRelease.certification
  623. : !string.IsNullOrEmpty(minimunRelease.certification)
  624. ? minimunRelease.iso_3166_1 + "-" + minimunRelease.certification
  625. : null;
  626. if (ourRelease.release_date != default(DateTime))
  627. {
  628. if (ourRelease.release_date.Year != 1)
  629. {
  630. movie.PremiereDate = ourRelease.release_date.ToUniversalTime();
  631. movie.ProductionYear = ourRelease.release_date.Year;
  632. }
  633. }
  634. else if(usRelease.release_date != default (DateTime))
  635. {
  636. if (usRelease.release_date.Year != 1)
  637. {
  638. movie.PremiereDate = usRelease.release_date.ToUniversalTime();
  639. movie.ProductionYear = usRelease.release_date.Year;
  640. }
  641. }
  642. else if (minimunRelease.release_date != default (DateTime))
  643. {
  644. if (minimunRelease.release_date.Year != 1)
  645. {
  646. movie.PremiereDate = minimunRelease.release_date.ToUniversalTime();
  647. movie.ProductionYear = minimunRelease.release_date.Year;
  648. }
  649. }
  650. }
  651. else
  652. {
  653. if (movieData.release_date.Year != 1)
  654. {
  655. //no specific country release info at all
  656. movie.PremiereDate = movieData.release_date.ToUniversalTime();
  657. movie.ProductionYear = movieData.release_date.Year;
  658. }
  659. }
  660. //if that didn't find a rating and we are a boxset, use the one from our first child
  661. if (movie.OfficialRating == null && movie is BoxSet)
  662. {
  663. var boxset = movie as BoxSet;
  664. Logger.Info("MovieDbProvider - Using rating of first child of boxset...");
  665. var firstChild = boxset.Children.FirstOrDefault();
  666. boxset.OfficialRating = firstChild != null ? firstChild.OfficialRating : null;
  667. }
  668. if (movieData.runtime > 0)
  669. movie.OriginalRunTimeTicks = TimeSpan.FromMinutes(movieData.runtime).Ticks;
  670. //studios
  671. if (movieData.production_companies != null && !movie.LockedFields.Contains(MetadataFields.Studios))
  672. {
  673. movie.Studios.Clear();
  674. foreach (var studio in movieData.production_companies.Select(c => c.name))
  675. {
  676. movie.AddStudio(studio);
  677. }
  678. }
  679. //genres
  680. if (movieData.genres != null && !movie.LockedFields.Contains(MetadataFields.Genres))
  681. {
  682. movie.Genres.Clear();
  683. foreach (var genre in movieData.genres.Select(g => g.name))
  684. {
  685. movie.AddGenre(genre);
  686. }
  687. }
  688. movie.People.Clear();
  689. movie.Tags.Clear();
  690. //Actors, Directors, Writers - all in People
  691. //actors come from cast
  692. if (movieData.casts != null && movieData.casts.cast != null && !movie.LockedFields.Contains(MetadataFields.Cast))
  693. {
  694. foreach (var actor in movieData.casts.cast.OrderBy(a => a.order)) movie.AddPerson(new PersonInfo { Name = actor.name, Role = actor.character, Type = PersonType.Actor });
  695. }
  696. //and the rest from crew
  697. if (movieData.casts != null && movieData.casts.crew != null)
  698. {
  699. foreach (var person in movieData.casts.crew) movie.AddPerson(new PersonInfo { Name = person.name, Role = person.job, Type = person.department });
  700. }
  701. if (movieData.keywords != null && movieData.keywords.keywords != null && !movie.LockedFields.Contains(MetadataFields.Tags))
  702. {
  703. movie.Tags = movieData.keywords.keywords.Select(i => i.name).ToList();
  704. }
  705. if (movieData.trailers != null && movieData.trailers.youtube != null &&
  706. movieData.trailers.youtube.Count > 0)
  707. {
  708. movie.RemoteTrailers = movieData.trailers.youtube.Select(i => new MediaUrl
  709. {
  710. Url = string.Format("http://www.youtube.com/watch?{0}", i.source),
  711. IsDirectLink = false,
  712. Name = i.name,
  713. VideoSize = string.Equals("hd", i.size, StringComparison.OrdinalIgnoreCase) ? VideoSize.HighDefinition : VideoSize.StandardDefinition
  714. }).ToList();
  715. }
  716. }
  717. }
  718. private DateTime _lastRequestDate = DateTime.MinValue;
  719. /// <summary>
  720. /// Gets the movie db response.
  721. /// </summary>
  722. internal async Task<Stream> GetMovieDbResponse(HttpRequestOptions options)
  723. {
  724. var cancellationToken = options.CancellationToken;
  725. await _movieDbResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  726. try
  727. {
  728. // Limit to three requests per second
  729. var diff = 340 - (DateTime.Now - _lastRequestDate).TotalMilliseconds;
  730. if (diff > 0)
  731. {
  732. await Task.Delay(Convert.ToInt32(diff), cancellationToken).ConfigureAwait(false);
  733. }
  734. _lastRequestDate = DateTime.Now;
  735. return await HttpClient.Get(options).ConfigureAwait(false);
  736. }
  737. finally
  738. {
  739. _lastRequestDate = DateTime.Now;
  740. _movieDbResourcePool.Release();
  741. }
  742. }
  743. /// <summary>
  744. /// The remove
  745. /// </summary>
  746. const string Remove = "\"'!`?";
  747. // "Face/Off" support.
  748. /// <summary>
  749. /// The spacers
  750. /// </summary>
  751. const string Spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
  752. /// <summary>
  753. /// The replace start numbers
  754. /// </summary>
  755. static readonly Dictionary<string, string> ReplaceStartNumbers = new Dictionary<string, string> {
  756. {"1 ","one "},
  757. {"2 ","two "},
  758. {"3 ","three "},
  759. {"4 ","four "},
  760. {"5 ","five "},
  761. {"6 ","six "},
  762. {"7 ","seven "},
  763. {"8 ","eight "},
  764. {"9 ","nine "},
  765. {"10 ","ten "},
  766. {"11 ","eleven "},
  767. {"12 ","twelve "},
  768. {"13 ","thirteen "},
  769. {"100 ","one hundred "},
  770. {"101 ","one hundred one "}
  771. };
  772. /// <summary>
  773. /// The replace end numbers
  774. /// </summary>
  775. static readonly Dictionary<string, string> ReplaceEndNumbers = new Dictionary<string, string> {
  776. {" 1"," i"},
  777. {" 2"," ii"},
  778. {" 3"," iii"},
  779. {" 4"," iv"},
  780. {" 5"," v"},
  781. {" 6"," vi"},
  782. {" 7"," vii"},
  783. {" 8"," viii"},
  784. {" 9"," ix"},
  785. {" 10"," x"}
  786. };
  787. /// <summary>
  788. /// Gets the name of the comparable.
  789. /// </summary>
  790. /// <param name="name">The name.</param>
  791. /// <param name="logger">The logger.</param>
  792. /// <returns>System.String.</returns>
  793. internal static string GetComparableName(string name, ILogger logger)
  794. {
  795. name = name.ToLower();
  796. name = name.Replace("á", "a");
  797. name = name.Replace("é", "e");
  798. name = name.Replace("í", "i");
  799. name = name.Replace("ó", "o");
  800. name = name.Replace("ú", "u");
  801. name = name.Replace("ü", "u");
  802. name = name.Replace("ñ", "n");
  803. foreach (var pair in ReplaceStartNumbers)
  804. {
  805. if (name.StartsWith(pair.Key))
  806. {
  807. name = name.Remove(0, pair.Key.Length);
  808. name = pair.Value + name;
  809. logger.Info("MovieDbProvider - Replaced Start Numbers: " + name);
  810. }
  811. }
  812. foreach (var pair in ReplaceEndNumbers)
  813. {
  814. if (name.EndsWith(pair.Key))
  815. {
  816. name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
  817. name = name + pair.Value;
  818. logger.Info("MovieDbProvider - Replaced End Numbers: " + name);
  819. }
  820. }
  821. name = name.Normalize(NormalizationForm.FormKD);
  822. var sb = new StringBuilder();
  823. foreach (var c in name)
  824. {
  825. if (c >= 0x2B0 && c <= 0x0333)
  826. {
  827. // skip char modifier and diacritics
  828. }
  829. else if (Remove.IndexOf(c) > -1)
  830. {
  831. // skip chars we are removing
  832. }
  833. else if (Spacers.IndexOf(c) > -1)
  834. {
  835. sb.Append(" ");
  836. }
  837. else if (c == '&')
  838. {
  839. sb.Append(" and ");
  840. }
  841. else
  842. {
  843. sb.Append(c);
  844. }
  845. }
  846. name = sb.ToString();
  847. name = name.Replace(", the", "");
  848. name = name.Replace(" the ", " ");
  849. name = name.Replace("the ", "");
  850. string prev_name;
  851. do
  852. {
  853. prev_name = name;
  854. name = name.Replace(" ", " ");
  855. } while (name.Length != prev_name.Length);
  856. return name.Trim();
  857. }
  858. #region Result Objects
  859. /// <summary>
  860. /// Class TmdbTitle
  861. /// </summary>
  862. protected class TmdbTitle
  863. {
  864. /// <summary>
  865. /// Gets or sets the iso_3166_1.
  866. /// </summary>
  867. /// <value>The iso_3166_1.</value>
  868. public string iso_3166_1 { get; set; }
  869. /// <summary>
  870. /// Gets or sets the title.
  871. /// </summary>
  872. /// <value>The title.</value>
  873. public string title { get; set; }
  874. }
  875. /// <summary>
  876. /// Class TmdbAltTitleResults
  877. /// </summary>
  878. protected class TmdbAltTitleResults
  879. {
  880. /// <summary>
  881. /// Gets or sets the id.
  882. /// </summary>
  883. /// <value>The id.</value>
  884. public int id { get; set; }
  885. /// <summary>
  886. /// Gets or sets the titles.
  887. /// </summary>
  888. /// <value>The titles.</value>
  889. public List<TmdbTitle> titles { get; set; }
  890. }
  891. /// <summary>
  892. /// Class TmdbMovieSearchResult
  893. /// </summary>
  894. protected class TmdbMovieSearchResult
  895. {
  896. /// <summary>
  897. /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult.
  898. /// </summary>
  899. /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
  900. public bool adult { get; set; }
  901. /// <summary>
  902. /// Gets or sets the backdrop_path.
  903. /// </summary>
  904. /// <value>The backdrop_path.</value>
  905. public string backdrop_path { get; set; }
  906. /// <summary>
  907. /// Gets or sets the id.
  908. /// </summary>
  909. /// <value>The id.</value>
  910. public int id { get; set; }
  911. /// <summary>
  912. /// Gets or sets the original_title.
  913. /// </summary>
  914. /// <value>The original_title.</value>
  915. public string original_title { get; set; }
  916. /// <summary>
  917. /// Gets or sets the release_date.
  918. /// </summary>
  919. /// <value>The release_date.</value>
  920. public string release_date { get; set; }
  921. /// <summary>
  922. /// Gets or sets the poster_path.
  923. /// </summary>
  924. /// <value>The poster_path.</value>
  925. public string poster_path { get; set; }
  926. /// <summary>
  927. /// Gets or sets the popularity.
  928. /// </summary>
  929. /// <value>The popularity.</value>
  930. public double popularity { get; set; }
  931. /// <summary>
  932. /// Gets or sets the title.
  933. /// </summary>
  934. /// <value>The title.</value>
  935. public string title { get; set; }
  936. /// <summary>
  937. /// Gets or sets the vote_average.
  938. /// </summary>
  939. /// <value>The vote_average.</value>
  940. public double vote_average { get; set; }
  941. /// <summary>
  942. /// Gets or sets the vote_count.
  943. /// </summary>
  944. /// <value>The vote_count.</value>
  945. public int vote_count { get; set; }
  946. }
  947. /// <summary>
  948. /// Class TmdbMovieSearchResults
  949. /// </summary>
  950. protected class TmdbMovieSearchResults
  951. {
  952. /// <summary>
  953. /// Gets or sets the page.
  954. /// </summary>
  955. /// <value>The page.</value>
  956. public int page { get; set; }
  957. /// <summary>
  958. /// Gets or sets the results.
  959. /// </summary>
  960. /// <value>The results.</value>
  961. public List<TmdbMovieSearchResult> results { get; set; }
  962. /// <summary>
  963. /// Gets or sets the total_pages.
  964. /// </summary>
  965. /// <value>The total_pages.</value>
  966. public int total_pages { get; set; }
  967. /// <summary>
  968. /// Gets or sets the total_results.
  969. /// </summary>
  970. /// <value>The total_results.</value>
  971. public int total_results { get; set; }
  972. }
  973. protected class BelongsToCollection
  974. {
  975. public int id { get; set; }
  976. public string name { get; set; }
  977. public string poster_path { get; set; }
  978. public string backdrop_path { get; set; }
  979. }
  980. protected class GenreItem
  981. {
  982. public int id { get; set; }
  983. public string name { get; set; }
  984. }
  985. protected class ProductionCompany
  986. {
  987. public string name { get; set; }
  988. public int id { get; set; }
  989. }
  990. protected class ProductionCountry
  991. {
  992. public string iso_3166_1 { get; set; }
  993. public string name { get; set; }
  994. }
  995. protected class SpokenLanguage
  996. {
  997. public string iso_639_1 { get; set; }
  998. public string name { get; set; }
  999. }
  1000. protected class Cast
  1001. {
  1002. public int id { get; set; }
  1003. public string name { get; set; }
  1004. public string character { get; set; }
  1005. public int order { get; set; }
  1006. public int cast_id { get; set; }
  1007. public string profile_path { get; set; }
  1008. }
  1009. protected class Crew
  1010. {
  1011. public int id { get; set; }
  1012. public string name { get; set; }
  1013. public string department { get; set; }
  1014. public string job { get; set; }
  1015. public string profile_path { get; set; }
  1016. }
  1017. protected class Casts
  1018. {
  1019. public List<Cast> cast { get; set; }
  1020. public List<Crew> crew { get; set; }
  1021. }
  1022. protected class Country
  1023. {
  1024. public string iso_3166_1 { get; set; }
  1025. public string certification { get; set; }
  1026. public DateTime release_date { get; set; }
  1027. }
  1028. protected class Releases
  1029. {
  1030. public List<Country> countries { get; set; }
  1031. }
  1032. protected class Keyword
  1033. {
  1034. public int id { get; set; }
  1035. public string name { get; set; }
  1036. }
  1037. protected class Keywords
  1038. {
  1039. public List<Keyword> keywords { get; set; }
  1040. }
  1041. protected class CompleteMovieData
  1042. {
  1043. public bool adult { get; set; }
  1044. public string backdrop_path { get; set; }
  1045. public BelongsToCollection belongs_to_collection { get; set; }
  1046. public int budget { get; set; }
  1047. public List<GenreItem> genres { get; set; }
  1048. public string homepage { get; set; }
  1049. public int id { get; set; }
  1050. public string imdb_id { get; set; }
  1051. public string original_title { get; set; }
  1052. public string overview { get; set; }
  1053. public double popularity { get; set; }
  1054. public string poster_path { get; set; }
  1055. public List<ProductionCompany> production_companies { get; set; }
  1056. public List<ProductionCountry> production_countries { get; set; }
  1057. public DateTime release_date { get; set; }
  1058. public int revenue { get; set; }
  1059. public int runtime { get; set; }
  1060. public List<SpokenLanguage> spoken_languages { get; set; }
  1061. public string status { get; set; }
  1062. public string tagline { get; set; }
  1063. public string title { get; set; }
  1064. public double vote_average { get; set; }
  1065. public int vote_count { get; set; }
  1066. public Casts casts { get; set; }
  1067. public Releases releases { get; set; }
  1068. public Keywords keywords { get; set; }
  1069. public Trailers trailers { get; set; }
  1070. }
  1071. public class Trailers
  1072. {
  1073. public List<Youtube> youtube { get; set; }
  1074. }
  1075. public class Youtube
  1076. {
  1077. public string name { get; set; }
  1078. public string size { get; set; }
  1079. public string source { get; set; }
  1080. }
  1081. public class TmdbImageSettings
  1082. {
  1083. public List<string> backdrop_sizes { get; set; }
  1084. public string base_url { get; set; }
  1085. public List<string> poster_sizes { get; set; }
  1086. public List<string> profile_sizes { get; set; }
  1087. }
  1088. public class TmdbSettingsResult
  1089. {
  1090. public TmdbImageSettings images { get; set; }
  1091. }
  1092. #endregion
  1093. public void Dispose()
  1094. {
  1095. Dispose(true);
  1096. }
  1097. }
  1098. }