MovieDbProvider.cs 45 KB

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