MovieDbProvider.cs 48 KB

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