TmdbClientManager.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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
  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);
  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="cancellationToken">The cancellation token.</param>
  241. /// <returns>The TMDb person information or null if not found.</returns>
  242. public async Task<Person> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
  243. {
  244. var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}";
  245. if (_memoryCache.TryGetValue(key, out Person person))
  246. {
  247. return person;
  248. }
  249. await EnsureClientConfigAsync().ConfigureAwait(false);
  250. person = await _tmDbClient.GetPersonAsync(
  251. personTmdbId,
  252. PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
  253. cancellationToken).ConfigureAwait(false);
  254. if (person != null)
  255. {
  256. _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
  257. }
  258. return person;
  259. }
  260. /// <summary>
  261. /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
  262. /// </summary>
  263. /// <param name="externalId">The item's external id.</param>
  264. /// <param name="source">The source of the id eg. IMDb.</param>
  265. /// <param name="language">The item's language.</param>
  266. /// <param name="cancellationToken">The cancellation token.</param>
  267. /// <returns>The TMDb item or null if not found.</returns>
  268. public async Task<FindContainer> FindByExternalIdAsync(
  269. string externalId,
  270. FindExternalSource source,
  271. string language,
  272. CancellationToken cancellationToken)
  273. {
  274. var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
  275. if (_memoryCache.TryGetValue(key, out FindContainer result))
  276. {
  277. return result;
  278. }
  279. await EnsureClientConfigAsync().ConfigureAwait(false);
  280. result = await _tmDbClient.FindAsync(
  281. source,
  282. externalId,
  283. TmdbUtils.NormalizeLanguage(language),
  284. cancellationToken).ConfigureAwait(false);
  285. if (result != null)
  286. {
  287. _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
  288. }
  289. return result;
  290. }
  291. /// <summary>
  292. /// Searches for a tv show using the TMDb API based on its name.
  293. /// </summary>
  294. /// <param name="name">The name of the tv show.</param>
  295. /// <param name="language">The tv show's language.</param>
  296. /// <param name="year">The year the tv show first aired.</param>
  297. /// <param name="cancellationToken">The cancellation token.</param>
  298. /// <returns>The TMDb tv show information.</returns>
  299. public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
  300. {
  301. var key = $"searchseries-{name}-{language}";
  302. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
  303. {
  304. return series.Results;
  305. }
  306. await EnsureClientConfigAsync().ConfigureAwait(false);
  307. var searchResults = await _tmDbClient
  308. .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
  309. .ConfigureAwait(false);
  310. if (searchResults.Results.Count > 0)
  311. {
  312. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  313. }
  314. return searchResults.Results;
  315. }
  316. /// <summary>
  317. /// Searches for a person based on their name using the TMDb API.
  318. /// </summary>
  319. /// <param name="name">The name of the person.</param>
  320. /// <param name="cancellationToken">The cancellation token.</param>
  321. /// <returns>The TMDb person information.</returns>
  322. public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
  323. {
  324. var key = $"searchperson-{name}";
  325. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
  326. {
  327. return person.Results;
  328. }
  329. await EnsureClientConfigAsync().ConfigureAwait(false);
  330. var searchResults = await _tmDbClient
  331. .SearchPersonAsync(name, cancellationToken: cancellationToken)
  332. .ConfigureAwait(false);
  333. if (searchResults.Results.Count > 0)
  334. {
  335. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  336. }
  337. return searchResults.Results;
  338. }
  339. /// <summary>
  340. /// Searches for a movie based on its name using the TMDb API.
  341. /// </summary>
  342. /// <param name="name">The name of the movie.</param>
  343. /// <param name="language">The movie's language.</param>
  344. /// <param name="cancellationToken">The cancellation token.</param>
  345. /// <returns>The TMDb movie information.</returns>
  346. public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
  347. {
  348. return SearchMovieAsync(name, 0, language, cancellationToken);
  349. }
  350. /// <summary>
  351. /// Searches for a movie based on its name using the TMDb API.
  352. /// </summary>
  353. /// <param name="name">The name of the movie.</param>
  354. /// <param name="year">The release year of the movie.</param>
  355. /// <param name="language">The movie's language.</param>
  356. /// <param name="cancellationToken">The cancellation token.</param>
  357. /// <returns>The TMDb movie information.</returns>
  358. public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
  359. {
  360. var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
  361. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
  362. {
  363. return movies.Results;
  364. }
  365. await EnsureClientConfigAsync().ConfigureAwait(false);
  366. var searchResults = await _tmDbClient
  367. .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
  368. .ConfigureAwait(false);
  369. if (searchResults.Results.Count > 0)
  370. {
  371. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  372. }
  373. return searchResults.Results;
  374. }
  375. /// <summary>
  376. /// Searches for a collection based on its name using the TMDb API.
  377. /// </summary>
  378. /// <param name="name">The name of the collection.</param>
  379. /// <param name="language">The collection's language.</param>
  380. /// <param name="cancellationToken">The cancellation token.</param>
  381. /// <returns>The TMDb collection information.</returns>
  382. public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
  383. {
  384. var key = $"collectionsearch-{name}-{language}";
  385. if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
  386. {
  387. return collections.Results;
  388. }
  389. await EnsureClientConfigAsync().ConfigureAwait(false);
  390. var searchResults = await _tmDbClient
  391. .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
  392. .ConfigureAwait(false);
  393. if (searchResults.Results.Count > 0)
  394. {
  395. _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
  396. }
  397. return searchResults.Results;
  398. }
  399. /// <summary>
  400. /// Gets the absolute URL of the poster.
  401. /// </summary>
  402. /// <param name="posterPath">The relative URL of the poster.</param>
  403. /// <returns>The absolute URL.</returns>
  404. public string GetPosterUrl(string posterPath)
  405. {
  406. if (string.IsNullOrEmpty(posterPath))
  407. {
  408. return null;
  409. }
  410. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
  411. }
  412. /// <summary>
  413. /// Gets the absolute URL of the backdrop image.
  414. /// </summary>
  415. /// <param name="posterPath">The relative URL of the backdrop image.</param>
  416. /// <returns>The absolute URL.</returns>
  417. public string GetBackdropUrl(string posterPath)
  418. {
  419. if (string.IsNullOrEmpty(posterPath))
  420. {
  421. return null;
  422. }
  423. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
  424. }
  425. /// <summary>
  426. /// Gets the absolute URL of the profile image.
  427. /// </summary>
  428. /// <param name="actorProfilePath">The relative URL of the profile image.</param>
  429. /// <returns>The absolute URL.</returns>
  430. public string GetProfileUrl(string actorProfilePath)
  431. {
  432. if (string.IsNullOrEmpty(actorProfilePath))
  433. {
  434. return null;
  435. }
  436. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
  437. }
  438. /// <summary>
  439. /// Gets the absolute URL of the still image.
  440. /// </summary>
  441. /// <param name="filePath">The relative URL of the still image.</param>
  442. /// <returns>The absolute URL.</returns>
  443. public string GetStillUrl(string filePath)
  444. {
  445. if (string.IsNullOrEmpty(filePath))
  446. {
  447. return null;
  448. }
  449. return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
  450. }
  451. private Task EnsureClientConfigAsync()
  452. {
  453. return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
  454. }
  455. }
  456. }