TmdbClientManager.cs 23 KB

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