TmdbClientManager.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Microsoft.Extensions.Caching.Memory;
  7. using TMDbLib.Client;
  8. using TMDbLib.Objects.Collections;
  9. using TMDbLib.Objects.Find;
  10. using TMDbLib.Objects.General;
  11. using TMDbLib.Objects.Movies;
  12. using TMDbLib.Objects.People;
  13. using TMDbLib.Objects.Search;
  14. using TMDbLib.Objects.TvShows;
  15. namespace MediaBrowser.Providers.Plugins.Tmdb
  16. {
  17. /// <summary>
  18. /// Manager class for abstracting the TMDb API client library.
  19. /// </summary>
  20. public class TmdbClientManager : IDisposable
  21. {
  22. private const int CacheDurationInHours = 1;
  23. private readonly IMemoryCache _memoryCache;
  24. private readonly TMDbClient _tmDbClient;
  25. /// <summary>
  26. /// Initializes a new instance of the <see cref="TmdbClientManager"/> class.
  27. /// </summary>
  28. /// <param name="memoryCache">An instance of <see cref="IMemoryCache"/>.</param>
  29. public TmdbClientManager(IMemoryCache memoryCache)
  30. {
  31. _memoryCache = memoryCache;
  32. _tmDbClient = new TMDbClient(TmdbUtils.ApiKey);
  33. // Not really interested in NotFoundException
  34. _tmDbClient.ThrowApiExceptions = false;
  35. }
  36. /// <summary>
  37. /// Gets a movie from the TMDb API based on its TMDb id.
  38. /// </summary>
  39. /// <param name="tmdbId">The movie's TMDb id.</param>
  40. /// <param name="language">The movie's language.</param>
  41. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  42. /// <param name="cancellationToken">The cancellation token.</param>
  43. /// <returns>The TMDb movie or null if not found.</returns>
  44. public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
  45. {
  46. var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
  47. if (_memoryCache.TryGetValue(key, out Movie movie))
  48. {
  49. return movie;
  50. }
  51. await EnsureClientConfigAsync().ConfigureAwait(false);
  52. movie = await _tmDbClient.GetMovieAsync(
  53. tmdbId,
  54. TmdbUtils.NormalizeLanguage(language),
  55. imageLanguages,
  56. MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
  57. cancellationToken).ConfigureAwait(false);
  58. if (movie != null)
  59. {
  60. _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours));
  61. }
  62. return movie;
  63. }
  64. /// <summary>
  65. /// Gets a collection from the TMDb API based on its TMDb id.
  66. /// </summary>
  67. /// <param name="tmdbId">The collection's TMDb id.</param>
  68. /// <param name="language">The collection's language.</param>
  69. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  70. /// <param name="cancellationToken">The cancellation token.</param>
  71. /// <returns>The TMDb collection or null if not found.</returns>
  72. public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
  73. {
  74. var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
  75. if (_memoryCache.TryGetValue(key, out Collection collection))
  76. {
  77. return collection;
  78. }
  79. await EnsureClientConfigAsync().ConfigureAwait(false);
  80. collection = await _tmDbClient.GetCollectionAsync(
  81. tmdbId,
  82. TmdbUtils.NormalizeLanguage(language),
  83. imageLanguages,
  84. CollectionMethods.Images,
  85. cancellationToken).ConfigureAwait(false);
  86. if (collection != null)
  87. {
  88. _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours));
  89. }
  90. return collection;
  91. }
  92. /// <summary>
  93. /// Gets a tv show from the TMDb API based on its TMDb id.
  94. /// </summary>
  95. /// <param name="tmdbId">The tv show's TMDb id.</param>
  96. /// <param name="language">The tv show's language.</param>
  97. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  98. /// <param name="cancellationToken">The cancellation token.</param>
  99. /// <returns>The TMDb tv show information or null if not found.</returns>
  100. public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
  101. {
  102. var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
  103. if (_memoryCache.TryGetValue(key, out TvShow series))
  104. {
  105. return series;
  106. }
  107. await EnsureClientConfigAsync().ConfigureAwait(false);
  108. series = await _tmDbClient.GetTvShowAsync(
  109. tmdbId,
  110. language: TmdbUtils.NormalizeLanguage(language),
  111. includeImageLanguage: imageLanguages,
  112. extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups,
  113. cancellationToken: cancellationToken).ConfigureAwait(false);
  114. if (series != null)
  115. {
  116. _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours));
  117. }
  118. return series;
  119. }
  120. /// <summary>
  121. /// Gets a tv show episode group from the TMDb API based on the show id and the display order.
  122. /// </summary>
  123. /// <param name="tvShowId">The tv show's TMDb id.</param>
  124. /// <param name="displayOrder">The display order.</param>
  125. /// <param name="language">The tv show's language.</param>
  126. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  127. /// <param name="cancellationToken">The cancellation token.</param>
  128. /// <returns>The TMDb tv show episode group information or null if not found.</returns>
  129. private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
  130. {
  131. TvGroupType? groupType =
  132. string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute :
  133. string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD :
  134. null;
  135. if (groupType == null)
  136. {
  137. return null;
  138. }
  139. var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
  140. if (_memoryCache.TryGetValue(key, out TvGroupCollection group))
  141. {
  142. return group;
  143. }
  144. await EnsureClientConfigAsync().ConfigureAwait(false);
  145. var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false);
  146. var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
  147. if (episodeGroupId == null)
  148. {
  149. return null;
  150. }
  151. group = await _tmDbClient.GetTvEpisodeGroupsAsync(
  152. episodeGroupId,
  153. language: TmdbUtils.NormalizeLanguage(language),
  154. cancellationToken: cancellationToken).ConfigureAwait(false);
  155. if (group != null)
  156. {
  157. _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours));
  158. }
  159. return group;
  160. }
  161. /// <summary>
  162. /// Gets a tv season from the TMDb API based on the tv show's TMDb id.
  163. /// </summary>
  164. /// <param name="tvShowId">The tv season's TMDb id.</param>
  165. /// <param name="seasonNumber">The season number.</param>
  166. /// <param name="language">The tv season's language.</param>
  167. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  168. /// <param name="cancellationToken">The cancellation token.</param>
  169. /// <returns>The TMDb tv season information or null if not found.</returns>
  170. public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
  171. {
  172. var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
  173. if (_memoryCache.TryGetValue(key, out TvSeason season))
  174. {
  175. return season;
  176. }
  177. await EnsureClientConfigAsync().ConfigureAwait(false);
  178. season = await _tmDbClient.GetTvSeasonAsync(
  179. tvShowId,
  180. seasonNumber,
  181. language: TmdbUtils.NormalizeLanguage(language),
  182. includeImageLanguage: imageLanguages,
  183. extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
  184. cancellationToken: cancellationToken).ConfigureAwait(false);
  185. if (season != null)
  186. {
  187. _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours));
  188. }
  189. return season;
  190. }
  191. /// <summary>
  192. /// Gets a movie from the TMDb API based on the tv show's TMDb id.
  193. /// </summary>
  194. /// <param name="tvShowId">The tv show's TMDb id.</param>
  195. /// <param name="seasonNumber">The season number.</param>
  196. /// <param name="episodeNumber">The episode number.</param>
  197. /// <param name="displayOrder">The display order.</param>
  198. /// <param name="language">The episode's language.</param>
  199. /// <param name="imageLanguages">A comma-separated list of image languages.</param>
  200. /// <param name="cancellationToken">The cancellation token.</param>
  201. /// <returns>The TMDb tv episode information or null if not found.</returns>
  202. public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken)
  203. {
  204. var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
  205. if (_memoryCache.TryGetValue(key, out TvEpisode episode))
  206. {
  207. return episode;
  208. }
  209. await EnsureClientConfigAsync().ConfigureAwait(false);
  210. var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false);
  211. if (group != null)
  212. {
  213. var season = group.Groups.Find(s => s.Order == seasonNumber);
  214. // Episode order starts at 0
  215. var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1);
  216. if (ep != null)
  217. {
  218. seasonNumber = ep.SeasonNumber;
  219. episodeNumber = ep.EpisodeNumber;
  220. }
  221. }
  222. episode = await _tmDbClient.GetTvEpisodeAsync(
  223. tvShowId,
  224. seasonNumber,
  225. episodeNumber,
  226. language: TmdbUtils.NormalizeLanguage(language),
  227. includeImageLanguage: imageLanguages,
  228. extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
  229. cancellationToken: cancellationToken).ConfigureAwait(false);
  230. if (episode != null)
  231. {
  232. _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours));
  233. }
  234. return episode;
  235. }
  236. /// <summary>
  237. /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
  238. /// </summary>
  239. /// <param name="personTmdbId">The person's TMDb id.</param>
  240. /// <param name="language">The episode's language.</param>
  241. /// <param name="cancellationToken">The cancellation token.</param>
  242. /// <returns>The TMDb person information or null if not found.</returns>
  243. public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
  244. {
  245. var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
  246. if (_memoryCache.TryGetValue(key, out Person person))
  247. {
  248. return person;
  249. }
  250. await EnsureClientConfigAsync().ConfigureAwait(false);
  251. person = await _tmDbClient.GetPersonAsync(
  252. personTmdbId,
  253. TmdbUtils.NormalizeLanguage(language),
  254. PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
  255. cancellationToken).ConfigureAwait(false);
  256. if (person != null)
  257. {
  258. _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
  259. }
  260. return person;
  261. }
  262. /// <summary>
  263. /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
  264. /// </summary>
  265. /// <param name="externalId">The item's external id.</param>
  266. /// <param name="source">The source of the id eg. IMDb.</param>
  267. /// <param name="language">The item's language.</param>
  268. /// <param name="cancellationToken">The cancellation token.</param>
  269. /// <returns>The TMDb item or null if not found.</returns>
  270. public async Task<FindContainer> FindByExternalIdAsync(
  271. string externalId,
  272. FindExternalSource source,
  273. string language,
  274. CancellationToken cancellationToken)
  275. {
  276. var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
  277. if (_memoryCache.TryGetValue(key, out FindContainer result))
  278. {
  279. return result;
  280. }
  281. await EnsureClientConfigAsync().ConfigureAwait(false);
  282. result = await _tmDbClient.FindAsync(
  283. source,
  284. externalId,
  285. TmdbUtils.NormalizeLanguage(language),
  286. cancellationToken).ConfigureAwait(false);
  287. if (result != null)
  288. {
  289. _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
  290. }
  291. return result;
  292. }
  293. /// <summary>
  294. /// Searches for a tv show using the TMDb API based on its name.
  295. /// </summary>
  296. /// <param name="name">The name of the tv show.</param>
  297. /// <param name="language">The tv show's language.</param>
  298. /// <param name="year">The year the tv show first aired.</param>
  299. /// <param name="cancellationToken">The cancellation token.</param>
  300. /// <returns>The TMDb tv show information.</returns>
  301. public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
  302. {
  303. var key = $"searchseries-{name}-{language}";
  304. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
  305. {
  306. return series.Results;
  307. }
  308. await EnsureClientConfigAsync().ConfigureAwait(false);
  309. var searchResults = await _tmDbClient
  310. .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
  311. .ConfigureAwait(false);
  312. if (searchResults.Results.Count > 0)
  313. {
  314. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  315. }
  316. return searchResults.Results;
  317. }
  318. /// <summary>
  319. /// Searches for a person based on their name using the TMDb API.
  320. /// </summary>
  321. /// <param name="name">The name of the person.</param>
  322. /// <param name="cancellationToken">The cancellation token.</param>
  323. /// <returns>The TMDb person information.</returns>
  324. public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
  325. {
  326. var key = $"searchperson-{name}";
  327. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
  328. {
  329. return person.Results;
  330. }
  331. await EnsureClientConfigAsync().ConfigureAwait(false);
  332. var searchResults = await _tmDbClient
  333. .SearchPersonAsync(name, cancellationToken: cancellationToken)
  334. .ConfigureAwait(false);
  335. if (searchResults.Results.Count > 0)
  336. {
  337. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  338. }
  339. return searchResults.Results;
  340. }
  341. /// <summary>
  342. /// Searches for a movie based on its name using the TMDb API.
  343. /// </summary>
  344. /// <param name="name">The name of the movie.</param>
  345. /// <param name="language">The movie's language.</param>
  346. /// <param name="cancellationToken">The cancellation token.</param>
  347. /// <returns>The TMDb movie information.</returns>
  348. public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
  349. {
  350. return SearchMovieAsync(name, 0, language, cancellationToken);
  351. }
  352. /// <summary>
  353. /// Searches for a movie based on its name using the TMDb API.
  354. /// </summary>
  355. /// <param name="name">The name of the movie.</param>
  356. /// <param name="year">The release year of the movie.</param>
  357. /// <param name="language">The movie's language.</param>
  358. /// <param name="cancellationToken">The cancellation token.</param>
  359. /// <returns>The TMDb movie information.</returns>
  360. public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
  361. {
  362. var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
  363. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
  364. {
  365. return movies.Results;
  366. }
  367. await EnsureClientConfigAsync().ConfigureAwait(false);
  368. var searchResults = await _tmDbClient
  369. .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
  370. .ConfigureAwait(false);
  371. if (searchResults.Results.Count > 0)
  372. {
  373. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  374. }
  375. return searchResults.Results;
  376. }
  377. /// <summary>
  378. /// Searches for a collection based on its name using the TMDb API.
  379. /// </summary>
  380. /// <param name="name">The name of the collection.</param>
  381. /// <param name="language">The collection's language.</param>
  382. /// <param name="cancellationToken">The cancellation token.</param>
  383. /// <returns>The TMDb collection information.</returns>
  384. public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
  385. {
  386. var key = $"collectionsearch-{name}-{language}";
  387. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
  388. {
  389. return collections.Results;
  390. }
  391. await EnsureClientConfigAsync().ConfigureAwait(false);
  392. var searchResults = await _tmDbClient
  393. .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
  394. .ConfigureAwait(false);
  395. if (searchResults.Results.Count > 0)
  396. {
  397. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  398. }
  399. return searchResults.Results;
  400. }
  401. /// <summary>
  402. /// Gets the absolute URL of the poster.
  403. /// </summary>
  404. /// <param name="posterPath">The relative URL of the poster.</param>
  405. /// <returns>The absolute URL.</returns>
  406. public string GetPosterUrl(string posterPath)
  407. {
  408. if (string.IsNullOrEmpty(posterPath))
  409. {
  410. return null;
  411. }
  412. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
  413. }
  414. /// <summary>
  415. /// Gets the absolute URL of the backdrop image.
  416. /// </summary>
  417. /// <param name="posterPath">The relative URL of the backdrop image.</param>
  418. /// <returns>The absolute URL.</returns>
  419. public string GetBackdropUrl(string posterPath)
  420. {
  421. if (string.IsNullOrEmpty(posterPath))
  422. {
  423. return null;
  424. }
  425. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
  426. }
  427. /// <summary>
  428. /// Gets the absolute URL of the profile image.
  429. /// </summary>
  430. /// <param name="actorProfilePath">The relative URL of the profile image.</param>
  431. /// <returns>The absolute URL.</returns>
  432. public string GetProfileUrl(string actorProfilePath)
  433. {
  434. if (string.IsNullOrEmpty(actorProfilePath))
  435. {
  436. return null;
  437. }
  438. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
  439. }
  440. /// <summary>
  441. /// Gets the absolute URL of the still image.
  442. /// </summary>
  443. /// <param name="filePath">The relative URL of the still image.</param>
  444. /// <returns>The absolute URL.</returns>
  445. public string GetStillUrl(string filePath)
  446. {
  447. if (string.IsNullOrEmpty(filePath))
  448. {
  449. return null;
  450. }
  451. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
  452. }
  453. private Task EnsureClientConfigAsync()
  454. {
  455. return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
  456. }
  457. /// <inheritdoc />
  458. public void Dispose()
  459. {
  460. Dispose(true);
  461. GC.SuppressFinalize(this);
  462. }
  463. /// <summary>
  464. /// Releases unmanaged and - optionally - managed resources.
  465. /// </summary>
  466. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  467. protected virtual void Dispose(bool disposing)
  468. {
  469. if (disposing)
  470. {
  471. _memoryCache?.Dispose();
  472. _tmDbClient?.Dispose();
  473. }
  474. }
  475. }
  476. }