using System; using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using TMDbLib.Client; using TMDbLib.Objects.Collections; using TMDbLib.Objects.Find; using TMDbLib.Objects.General; using TMDbLib.Objects.Movies; using TMDbLib.Objects.People; using TMDbLib.Objects.Search; using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb { /// /// Manager class for abstracting the TMDb API client library. /// public class TmdbClientManager { private const int CacheDurationInHours = 1; private readonly IMemoryCache _memoryCache; private readonly TMDbClient _tmDbClient; /// /// Initializes a new instance of the class. /// /// An instance of . public TmdbClientManager(IMemoryCache memoryCache) { _memoryCache = memoryCache; _tmDbClient = new TMDbClient(TmdbUtils.ApiKey); // Not really interested in NotFoundException _tmDbClient.ThrowApiExceptions = false; } /// /// Gets a movie from the TMDb API based on its TMDb id. /// /// The movie's TMDb id. /// The movie's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb movie or null if not found. public async Task GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) { var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out Movie movie)) { return movie; } await EnsureClientConfigAsync().ConfigureAwait(false); movie = await _tmDbClient.GetMovieAsync( tmdbId, TmdbUtils.NormalizeLanguage(language), imageLanguages, MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos, cancellationToken).ConfigureAwait(false); if (movie != null) { _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours)); } return movie; } /// /// Gets a collection from the TMDb API based on its TMDb id. /// /// The collection's TMDb id. /// The collection's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb collection or null if not found. public async Task GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) { var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out Collection collection)) { return collection; } await EnsureClientConfigAsync().ConfigureAwait(false); collection = await _tmDbClient.GetCollectionAsync( tmdbId, TmdbUtils.NormalizeLanguage(language), imageLanguages, CollectionMethods.Images, cancellationToken).ConfigureAwait(false); if (collection != null) { _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours)); } return collection; } /// /// Gets a tv show from the TMDb API based on its TMDb id. /// /// The tv show's TMDb id. /// The tv show's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb tv show information or null if not found. public async Task GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken) { var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out TvShow series)) { return series; } await EnsureClientConfigAsync().ConfigureAwait(false); series = await _tmDbClient.GetTvShowAsync( tmdbId, language: TmdbUtils.NormalizeLanguage(language), includeImageLanguage: imageLanguages, extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups, cancellationToken: cancellationToken).ConfigureAwait(false); if (series != null) { _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours)); } return series; } /// /// Gets a tv show episode group from the TMDb API based on the show id and the display order. /// /// The tv show's TMDb id. /// The display order. /// The tv show's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb tv show episode group information or null if not found. private async Task GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) { TvGroupType? groupType = string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute : string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD : null; if (groupType == null) { return null; } var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; if (_memoryCache.TryGetValue(key, out TvGroupCollection group)) { return group; } await EnsureClientConfigAsync().ConfigureAwait(false); var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false); var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id; if (episodeGroupId == null) { return null; } group = await _tmDbClient.GetTvEpisodeGroupsAsync( episodeGroupId, language: TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken).ConfigureAwait(false); if (group != null) { _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours)); } return group; } /// /// Gets a tv season from the TMDb API based on the tv show's TMDb id. /// /// The tv season's TMDb id. /// The season number. /// The tv season's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb tv season information or null if not found. public async Task GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken) { var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out TvSeason season)) { return season; } await EnsureClientConfigAsync().ConfigureAwait(false); season = await _tmDbClient.GetTvSeasonAsync( tvShowId, seasonNumber, language: TmdbUtils.NormalizeLanguage(language), includeImageLanguage: imageLanguages, extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false); if (season != null) { _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours)); } return season; } /// /// Gets a movie from the TMDb API based on the tv show's TMDb id. /// /// The tv show's TMDb id. /// The season number. /// The episode number. /// The display order. /// The episode's language. /// A comma-separated list of image languages. /// The cancellation token. /// The TMDb tv episode information or null if not found. public async Task GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) { var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; if (_memoryCache.TryGetValue(key, out TvEpisode episode)) { return episode; } await EnsureClientConfigAsync().ConfigureAwait(false); var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false); if (group != null) { var season = group.Groups.Find(s => s.Order == seasonNumber); // Episode order starts at 0 var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1); if (ep != null) { seasonNumber = ep.SeasonNumber; episodeNumber = ep.EpisodeNumber; } } episode = await _tmDbClient.GetTvEpisodeAsync( tvShowId, seasonNumber, episodeNumber, language: TmdbUtils.NormalizeLanguage(language), includeImageLanguage: imageLanguages, extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false); if (episode != null) { _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours)); } return episode; } /// /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id. /// /// The person's TMDb id. /// The episode's language. /// The cancellation token. /// The TMDb person information or null if not found. public async Task GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken) { var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out Person person)) { return person; } await EnsureClientConfigAsync().ConfigureAwait(false); person = await _tmDbClient.GetPersonAsync( personTmdbId, TmdbUtils.NormalizeLanguage(language), PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds, cancellationToken).ConfigureAwait(false); if (person != null) { _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours)); } return person; } /// /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id. /// /// The item's external id. /// The source of the id eg. IMDb. /// The item's language. /// The cancellation token. /// The TMDb item or null if not found. public async Task FindByExternalIdAsync( string externalId, FindExternalSource source, string language, CancellationToken cancellationToken) { var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out FindContainer result)) { return result; } await EnsureClientConfigAsync().ConfigureAwait(false); result = await _tmDbClient.FindAsync( source, externalId, TmdbUtils.NormalizeLanguage(language), cancellationToken).ConfigureAwait(false); if (result != null) { _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours)); } return result; } /// /// Searches for a tv show using the TMDb API based on its name. /// /// The name of the tv show. /// The tv show's language. /// The year the tv show first aired. /// The cancellation token. /// The TMDb tv show information. public async Task> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default) { var key = $"searchseries-{name}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer series)) { return series.Results; } await EnsureClientConfigAsync().ConfigureAwait(false); var searchResults = await _tmDbClient .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken) .ConfigureAwait(false); if (searchResults.Results.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } return searchResults.Results; } /// /// Searches for a person based on their name using the TMDb API. /// /// The name of the person. /// The cancellation token. /// The TMDb person information. public async Task> SearchPersonAsync(string name, CancellationToken cancellationToken) { var key = $"searchperson-{name}"; if (_memoryCache.TryGetValue(key, out SearchContainer person)) { return person.Results; } await EnsureClientConfigAsync().ConfigureAwait(false); var searchResults = await _tmDbClient .SearchPersonAsync(name, cancellationToken: cancellationToken) .ConfigureAwait(false); if (searchResults.Results.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } return searchResults.Results; } /// /// Searches for a movie based on its name using the TMDb API. /// /// The name of the movie. /// The movie's language. /// The cancellation token. /// The TMDb movie information. public Task> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) { return SearchMovieAsync(name, 0, language, cancellationToken); } /// /// Searches for a movie based on its name using the TMDb API. /// /// The name of the movie. /// The release year of the movie. /// The movie's language. /// The cancellation token. /// The TMDb movie information. public async Task> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken) { var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer movies)) { return movies.Results; } await EnsureClientConfigAsync().ConfigureAwait(false); var searchResults = await _tmDbClient .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken) .ConfigureAwait(false); if (searchResults.Results.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } return searchResults.Results; } /// /// Searches for a collection based on its name using the TMDb API. /// /// The name of the collection. /// The collection's language. /// The cancellation token. /// The TMDb collection information. public async Task> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken) { var key = $"collectionsearch-{name}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer collections)) { return collections.Results; } await EnsureClientConfigAsync().ConfigureAwait(false); var searchResults = await _tmDbClient .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken) .ConfigureAwait(false); if (searchResults.Results.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } return searchResults.Results; } /// /// Gets the absolute URL of the poster. /// /// The relative URL of the poster. /// The absolute URL. public string GetPosterUrl(string posterPath) { if (string.IsNullOrEmpty(posterPath)) { return null; } return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString(); } /// /// Gets the absolute URL of the backdrop image. /// /// The relative URL of the backdrop image. /// The absolute URL. public string GetBackdropUrl(string posterPath) { if (string.IsNullOrEmpty(posterPath)) { return null; } return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString(); } /// /// Gets the absolute URL of the profile image. /// /// The relative URL of the profile image. /// The absolute URL. public string GetProfileUrl(string actorProfilePath) { if (string.IsNullOrEmpty(actorProfilePath)) { return null; } return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString(); } /// /// Gets the absolute URL of the still image. /// /// The relative URL of the still image. /// The absolute URL. public string GetStillUrl(string filePath) { if (string.IsNullOrEmpty(filePath)) { return null; } return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString(); } private Task EnsureClientConfigAsync() { return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask; } } }