< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Plugins.Tmdb.TmdbClientManager
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
Line coverage
35%
Covered lines: 13
Uncovered lines: 24
Coverable lines: 37
Total lines: 690
Line coverage: 35.1%
Branch coverage
25%
Covered branches: 5
Total branches: 20
Branch coverage: 25%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using Jellyfin.Data.Enums;
 7using MediaBrowser.Model.Dto;
 8using MediaBrowser.Model.Entities;
 9using MediaBrowser.Model.Providers;
 10using Microsoft.Extensions.Caching.Memory;
 11using TMDbLib.Client;
 12using TMDbLib.Objects.Collections;
 13using TMDbLib.Objects.Find;
 14using TMDbLib.Objects.General;
 15using TMDbLib.Objects.Movies;
 16using TMDbLib.Objects.People;
 17using TMDbLib.Objects.Search;
 18using TMDbLib.Objects.TvShows;
 19
 20namespace MediaBrowser.Providers.Plugins.Tmdb
 21{
 22    /// <summary>
 23    /// Manager class for abstracting the TMDb API client library.
 24    /// </summary>
 25    public class TmdbClientManager : IDisposable
 26    {
 27        private const int CacheDurationInHours = 1;
 28
 29        private readonly IMemoryCache _memoryCache;
 30        private readonly TMDbClient _tmDbClient;
 31
 32        /// <summary>
 33        /// Initializes a new instance of the <see cref="TmdbClientManager"/> class.
 34        /// </summary>
 35        /// <param name="memoryCache">An instance of <see cref="IMemoryCache"/>.</param>
 36        public TmdbClientManager(IMemoryCache memoryCache)
 37        {
 2238            _memoryCache = memoryCache;
 39
 2240            var apiKey = Plugin.Instance.Configuration.TmdbApiKey;
 2241            apiKey = string.IsNullOrEmpty(apiKey) ? TmdbUtils.ApiKey : apiKey;
 2242            _tmDbClient = new TMDbClient(apiKey);
 43
 44            // Not really interested in NotFoundException
 2245            _tmDbClient.ThrowApiExceptions = false;
 2246        }
 47
 48        /// <summary>
 49        /// Gets a movie from the TMDb API based on its TMDb id.
 50        /// </summary>
 51        /// <param name="tmdbId">The movie's TMDb id.</param>
 52        /// <param name="language">The movie's language.</param>
 53        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 54        /// <param name="cancellationToken">The cancellation token.</param>
 55        /// <returns>The TMDb movie or null if not found.</returns>
 56        public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken 
 57        {
 58            var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
 59            if (_memoryCache.TryGetValue(key, out Movie? movie))
 60            {
 61                return movie;
 62            }
 63
 64            await EnsureClientConfigAsync().ConfigureAwait(false);
 65
 66            var extraMethods = MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Videos;
 67            if (!(Plugin.Instance?.Configuration.ExcludeTagsMovies).GetValueOrDefault())
 68            {
 69                extraMethods |= MovieMethods.Keywords;
 70            }
 71
 72            movie = await _tmDbClient.GetMovieAsync(
 73                tmdbId,
 74                TmdbUtils.NormalizeLanguage(language),
 75                imageLanguages,
 76                extraMethods,
 77                cancellationToken).ConfigureAwait(false);
 78
 79            if (movie is not null)
 80            {
 81                _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours));
 82            }
 83
 84            return movie;
 85        }
 86
 87        /// <summary>
 88        /// Gets a collection from the TMDb API based on its TMDb id.
 89        /// </summary>
 90        /// <param name="tmdbId">The collection's TMDb id.</param>
 91        /// <param name="language">The collection's language.</param>
 92        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 93        /// <param name="cancellationToken">The cancellation token.</param>
 94        /// <returns>The TMDb collection or null if not found.</returns>
 95        public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, Cancella
 96        {
 97            var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
 98            if (_memoryCache.TryGetValue(key, out Collection? collection))
 99            {
 100                return collection;
 101            }
 102
 103            await EnsureClientConfigAsync().ConfigureAwait(false);
 104
 105            collection = await _tmDbClient.GetCollectionAsync(
 106                tmdbId,
 107                TmdbUtils.NormalizeLanguage(language),
 108                imageLanguages,
 109                CollectionMethods.Images,
 110                cancellationToken).ConfigureAwait(false);
 111
 112            if (collection is not null)
 113            {
 114                _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours));
 115            }
 116
 117            return collection;
 118        }
 119
 120        /// <summary>
 121        /// Gets a tv show from the TMDb API based on its TMDb id.
 122        /// </summary>
 123        /// <param name="tmdbId">The tv show's TMDb id.</param>
 124        /// <param name="language">The tv show's language.</param>
 125        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 126        /// <param name="cancellationToken">The cancellation token.</param>
 127        /// <returns>The TMDb tv show information or null if not found.</returns>
 128        public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, CancellationToke
 129        {
 130            var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
 131            if (_memoryCache.TryGetValue(key, out TvShow? series))
 132            {
 133                return series;
 134            }
 135
 136            await EnsureClientConfigAsync().ConfigureAwait(false);
 137
 138            var extraMethods = TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.ExternalIds | TvShowMethods.
 139            if (!(Plugin.Instance?.Configuration.ExcludeTagsSeries).GetValueOrDefault())
 140            {
 141                extraMethods |= TvShowMethods.Keywords;
 142            }
 143
 144            series = await _tmDbClient.GetTvShowAsync(
 145                tmdbId,
 146                language: TmdbUtils.NormalizeLanguage(language),
 147                includeImageLanguage: imageLanguages,
 148                extraMethods: extraMethods,
 149                cancellationToken: cancellationToken).ConfigureAwait(false);
 150
 151            if (series is not null)
 152            {
 153                _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours));
 154            }
 155
 156            return series;
 157        }
 158
 159        /// <summary>
 160        /// Gets a tv show episode group from the TMDb API based on the show id and the display order.
 161        /// </summary>
 162        /// <param name="tvShowId">The tv show's TMDb id.</param>
 163        /// <param name="displayOrder">The display order.</param>
 164        /// <param name="language">The tv show's language.</param>
 165        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 166        /// <param name="cancellationToken">The cancellation token.</param>
 167        /// <returns>The TMDb tv show episode group information or null if not found.</returns>
 168        private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, 
 169        {
 170            TvGroupType? groupType =
 171                string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate :
 172                string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute :
 173                string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD :
 174                string.Equals(displayOrder, "digital", StringComparison.Ordinal) ? TvGroupType.Digital :
 175                string.Equals(displayOrder, "storyArc", StringComparison.Ordinal) ? TvGroupType.StoryArc :
 176                string.Equals(displayOrder, "production", StringComparison.Ordinal) ? TvGroupType.Production :
 177                string.Equals(displayOrder, "tv", StringComparison.Ordinal) ? TvGroupType.TV :
 178                null;
 179
 180            if (groupType is null)
 181            {
 182                return null;
 183            }
 184
 185            var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
 186            if (_memoryCache.TryGetValue(key, out TvGroupCollection? group))
 187            {
 188                return group;
 189            }
 190
 191            await EnsureClientConfigAsync().ConfigureAwait(false);
 192
 193            var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(fals
 194            var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
 195
 196            if (episodeGroupId is null)
 197            {
 198                return null;
 199            }
 200
 201            group = await _tmDbClient.GetTvEpisodeGroupsAsync(
 202                episodeGroupId,
 203                language: TmdbUtils.NormalizeLanguage(language),
 204                cancellationToken: cancellationToken).ConfigureAwait(false);
 205
 206            if (group is not null)
 207            {
 208                _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours));
 209            }
 210
 211            return group;
 212        }
 213
 214        /// <summary>
 215        /// Gets a tv season from the TMDb API based on the tv show's TMDb id.
 216        /// </summary>
 217        /// <param name="tvShowId">The tv season's TMDb id.</param>
 218        /// <param name="seasonNumber">The season number.</param>
 219        /// <param name="language">The tv season's language.</param>
 220        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 221        /// <param name="cancellationToken">The cancellation token.</param>
 222        /// <returns>The TMDb tv season information or null if not found.</returns>
 223        public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLangu
 224        {
 225            var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.Inv
 226            if (_memoryCache.TryGetValue(key, out TvSeason? season))
 227            {
 228                return season;
 229            }
 230
 231            await EnsureClientConfigAsync().ConfigureAwait(false);
 232
 233            season = await _tmDbClient.GetTvSeasonAsync(
 234                tvShowId,
 235                seasonNumber,
 236                language: TmdbUtils.NormalizeLanguage(language),
 237                includeImageLanguage: imageLanguages,
 238                extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonM
 239                cancellationToken: cancellationToken).ConfigureAwait(false);
 240
 241            if (season is not null)
 242            {
 243                _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours));
 244            }
 245
 246            return season;
 247        }
 248
 249        /// <summary>
 250        /// Gets a movie from the TMDb API based on the tv show's TMDb id.
 251        /// </summary>
 252        /// <param name="tvShowId">The tv show's TMDb id.</param>
 253        /// <param name="seasonNumber">The season number.</param>
 254        /// <param name="episodeNumber">The episode number.</param>
 255        /// <param name="displayOrder">The display order.</param>
 256        /// <param name="language">The episode's language.</param>
 257        /// <param name="imageLanguages">A comma-separated list of image languages.</param>
 258        /// <param name="cancellationToken">The cancellation token.</param>
 259        /// <returns>The TMDb tv episode information or null if not found.</returns>
 260        public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayO
 261        {
 262            var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.In
 263            if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
 264            {
 265                return episode;
 266            }
 267
 268            await EnsureClientConfigAsync().ConfigureAwait(false);
 269
 270            var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).C
 271            if (group is not null)
 272            {
 273                var season = group.Groups.Find(s => s.Order == seasonNumber);
 274                // Episode order starts at 0
 275                var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1);
 276                if (ep is not null)
 277                {
 278                    seasonNumber = ep.SeasonNumber;
 279                    episodeNumber = ep.EpisodeNumber;
 280                }
 281            }
 282
 283            episode = await _tmDbClient.GetTvEpisodeAsync(
 284                tvShowId,
 285                seasonNumber,
 286                episodeNumber,
 287                language: TmdbUtils.NormalizeLanguage(language),
 288                includeImageLanguage: imageLanguages,
 289                extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpis
 290                cancellationToken: cancellationToken).ConfigureAwait(false);
 291
 292            if (episode is not null)
 293            {
 294                _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours));
 295            }
 296
 297            return episode;
 298        }
 299
 300        /// <summary>
 301        /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
 302        /// </summary>
 303        /// <param name="personTmdbId">The person's TMDb id.</param>
 304        /// <param name="language">The episode's language.</param>
 305        /// <param name="cancellationToken">The cancellation token.</param>
 306        /// <returns>The TMDb person information or null if not found.</returns>
 307        public async Task<Person?> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken
 308        {
 309            var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
 310            if (_memoryCache.TryGetValue(key, out Person? person))
 311            {
 312                return person;
 313            }
 314
 315            await EnsureClientConfigAsync().ConfigureAwait(false);
 316
 317            person = await _tmDbClient.GetPersonAsync(
 318                personTmdbId,
 319                TmdbUtils.NormalizeLanguage(language),
 320                PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
 321                cancellationToken).ConfigureAwait(false);
 322
 323            if (person is not null)
 324            {
 325                _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
 326            }
 327
 328            return person;
 329        }
 330
 331        /// <summary>
 332        /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
 333        /// </summary>
 334        /// <param name="externalId">The item's external id.</param>
 335        /// <param name="source">The source of the id eg. IMDb.</param>
 336        /// <param name="language">The item's language.</param>
 337        /// <param name="cancellationToken">The cancellation token.</param>
 338        /// <returns>The TMDb item or null if not found.</returns>
 339        public async Task<FindContainer?> FindByExternalIdAsync(
 340            string externalId,
 341            FindExternalSource source,
 342            string language,
 343            CancellationToken cancellationToken)
 344        {
 345            var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
 346            if (_memoryCache.TryGetValue(key, out FindContainer? result))
 347            {
 348                return result;
 349            }
 350
 351            await EnsureClientConfigAsync().ConfigureAwait(false);
 352
 353            result = await _tmDbClient.FindAsync(
 354                source,
 355                externalId,
 356                TmdbUtils.NormalizeLanguage(language),
 357                cancellationToken).ConfigureAwait(false);
 358
 359            if (result is not null)
 360            {
 361                _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
 362            }
 363
 364            return result;
 365        }
 366
 367        /// <summary>
 368        /// Searches for a tv show using the TMDb API based on its name.
 369        /// </summary>
 370        /// <param name="name">The name of the tv show.</param>
 371        /// <param name="language">The tv show's language.</param>
 372        /// <param name="year">The year the tv show first aired.</param>
 373        /// <param name="cancellationToken">The cancellation token.</param>
 374        /// <returns>The TMDb tv show information.</returns>
 375        public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, Cancell
 376        {
 377            var key = $"searchseries-{name}-{language}";
 378            if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
 379            {
 380                return series.Results;
 381            }
 382
 383            await EnsureClientConfigAsync().ConfigureAwait(false);
 384
 385            var searchResults = await _tmDbClient
 386                .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configurat
 387                .ConfigureAwait(false);
 388
 389            if (searchResults.Results.Count > 0)
 390            {
 391                _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
 392            }
 393
 394            return searchResults.Results;
 395        }
 396
 397        /// <summary>
 398        /// Searches for a person based on their name using the TMDb API.
 399        /// </summary>
 400        /// <param name="name">The name of the person.</param>
 401        /// <param name="cancellationToken">The cancellation token.</param>
 402        /// <returns>The TMDb person information.</returns>
 403        public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToke
 404        {
 405            var key = $"searchperson-{name}";
 406            if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null)
 407            {
 408                return person.Results;
 409            }
 410
 411            await EnsureClientConfigAsync().ConfigureAwait(false);
 412
 413            var searchResults = await _tmDbClient
 414                .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: ca
 415                .ConfigureAwait(false);
 416
 417            if (searchResults.Results.Count > 0)
 418            {
 419                _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
 420            }
 421
 422            return searchResults.Results;
 423        }
 424
 425        /// <summary>
 426        /// Searches for a movie based on its name using the TMDb API.
 427        /// </summary>
 428        /// <param name="name">The name of the movie.</param>
 429        /// <param name="language">The movie's language.</param>
 430        /// <param name="cancellationToken">The cancellation token.</param>
 431        /// <returns>The TMDb movie information.</returns>
 432        public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancell
 433        {
 0434            return SearchMovieAsync(name, 0, language, cancellationToken);
 435        }
 436
 437        /// <summary>
 438        /// Searches for a movie based on its name using the TMDb API.
 439        /// </summary>
 440        /// <param name="name">The name of the movie.</param>
 441        /// <param name="year">The release year of the movie.</param>
 442        /// <param name="language">The movie's language.</param>
 443        /// <param name="cancellationToken">The cancellation token.</param>
 444        /// <returns>The TMDb movie information.</returns>
 445        public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, Cancellat
 446        {
 447            var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
 448            if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
 449            {
 450                return movies.Results;
 451            }
 452
 453            await EnsureClientConfigAsync().ConfigureAwait(false);
 454
 455            var searchResults = await _tmDbClient
 456                .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configurati
 457                .ConfigureAwait(false);
 458
 459            if (searchResults.Results.Count > 0)
 460            {
 461                _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
 462            }
 463
 464            return searchResults.Results;
 465        }
 466
 467        /// <summary>
 468        /// Searches for a collection based on its name using the TMDb API.
 469        /// </summary>
 470        /// <param name="name">The name of the collection.</param>
 471        /// <param name="language">The collection's language.</param>
 472        /// <param name="cancellationToken">The cancellation token.</param>
 473        /// <returns>The TMDb collection information.</returns>
 474        public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, Cancellat
 475        {
 476            var key = $"collectionsearch-{name}-{language}";
 477            if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not 
 478            {
 479                return collections.Results;
 480            }
 481
 482            await EnsureClientConfigAsync().ConfigureAwait(false);
 483
 484            var searchResults = await _tmDbClient
 485                .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken
 486                .ConfigureAwait(false);
 487
 488            if (searchResults.Results.Count > 0)
 489            {
 490                _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
 491            }
 492
 493            return searchResults.Results;
 494        }
 495
 496        /// <summary>
 497        /// Handles bad path checking and builds the absolute url.
 498        /// </summary>
 499        /// <param name="size">The image size to fetch.</param>
 500        /// <param name="path">The relative URL of the image.</param>
 501        /// <returns>The absolute URL.</returns>
 502        private string? GetUrl(string? size, string path)
 503        {
 0504            if (string.IsNullOrEmpty(path))
 505            {
 0506                return null;
 507            }
 508
 0509            return _tmDbClient.GetImageUrl(size, path, true).ToString();
 510        }
 511
 512        /// <summary>
 513        /// Gets the absolute URL of the poster.
 514        /// </summary>
 515        /// <param name="posterPath">The relative URL of the poster.</param>
 516        /// <returns>The absolute URL.</returns>
 517        public string? GetPosterUrl(string posterPath)
 518        {
 0519            return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
 520        }
 521
 522        /// <summary>
 523        /// Gets the absolute URL of the profile image.
 524        /// </summary>
 525        /// <param name="actorProfilePath">The relative URL of the profile image.</param>
 526        /// <returns>The absolute URL.</returns>
 527        public string? GetProfileUrl(string actorProfilePath)
 528        {
 0529            return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
 530        }
 531
 532        /// <summary>
 533        /// Converts poster <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 534        /// </summary>
 535        /// <param name="images">The input images.</param>
 536        /// <param name="requestLanguage">The requested language.</param>
 537        /// <returns>The remote images.</returns>
 538        public IEnumerable<RemoteImageInfo> ConvertPostersToRemoteImageInfo(IReadOnlyList<ImageData> images, string requ
 0539            => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLang
 540
 541        /// <summary>
 542        /// Converts backdrop <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 543        /// </summary>
 544        /// <param name="images">The input images.</param>
 545        /// <param name="requestLanguage">The requested language.</param>
 546        /// <returns>The remote images.</returns>
 547        public IEnumerable<RemoteImageInfo> ConvertBackdropsToRemoteImageInfo(IReadOnlyList<ImageData> images, string re
 0548            => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestL
 549
 550        /// <summary>
 551        /// Converts logo <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 552        /// </summary>
 553        /// <param name="images">The input images.</param>
 554        /// <param name="requestLanguage">The requested language.</param>
 555        /// <returns>The remote images.</returns>
 556        public IEnumerable<RemoteImageInfo> ConvertLogosToRemoteImageInfo(IReadOnlyList<ImageData> images, string reques
 0557            => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage)
 558
 559        /// <summary>
 560        /// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 561        /// </summary>
 562        /// <param name="images">The input images.</param>
 563        /// <param name="requestLanguage">The requested language.</param>
 564        /// <returns>The remote images.</returns>
 565        public IEnumerable<RemoteImageInfo> ConvertProfilesToRemoteImageInfo(IReadOnlyList<ImageData> images, string req
 0566            => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLan
 567
 568        /// <summary>
 569        /// Converts still <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 570        /// </summary>
 571        /// <param name="images">The input images.</param>
 572        /// <param name="requestLanguage">The requested language.</param>
 573        /// <returns>The remote images.</returns>
 574        public IEnumerable<RemoteImageInfo> ConvertStillsToRemoteImageInfo(IReadOnlyList<ImageData> images, string reque
 0575            => ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLangu
 576
 577        /// <summary>
 578        /// Converts <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
 579        /// </summary>
 580        /// <param name="images">The input images.</param>
 581        /// <param name="size">The size of the image to fetch.</param>
 582        /// <param name="type">The type of the image.</param>
 583        /// <param name="requestLanguage">The requested language.</param>
 584        /// <returns>The remote images.</returns>
 585        private IEnumerable<RemoteImageInfo> ConvertToRemoteImageInfo(IReadOnlyList<ImageData> images, string? size, Ima
 586        {
 587            // sizes provided are for original resolution, don't store them when downloading scaled images
 588            var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
 589
 590            for (var i = 0; i < images.Count; i++)
 591            {
 592                var image = images[i];
 593
 594                var imageType = type;
 595                var language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage);
 596
 597                // Return Backdrops with a language specified (it has text) as Thumb.
 598                if (imageType == ImageType.Backdrop && !string.IsNullOrEmpty(language))
 599                {
 600                    imageType = ImageType.Thumb;
 601                }
 602
 603                yield return new RemoteImageInfo
 604                {
 605                    Url = GetUrl(size, image.FilePath),
 606                    CommunityRating = image.VoteAverage,
 607                    VoteCount = image.VoteCount,
 608                    Width = scaleImage ? null : image.Width,
 609                    Height = scaleImage ? null : image.Height,
 610                    Language = language,
 611                    ProviderName = TmdbUtils.ProviderName,
 612                    Type = imageType,
 613                    RatingType = RatingType.Score
 614                };
 615            }
 616        }
 617
 618        private async Task EnsureClientConfigAsync()
 619        {
 620            if (!_tmDbClient.HasConfig)
 621            {
 622                var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false);
 623                ValidatePreferences(config);
 624            }
 625        }
 626
 627        private static void ValidatePreferences(TMDbConfig config)
 628        {
 0629            var imageConfig = config.Images;
 630
 0631            var pluginConfig = Plugin.Instance.Configuration;
 632
 0633            if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
 634            {
 0635                pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
 636            }
 637
 0638            if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
 639            {
 0640                pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
 641            }
 642
 0643            if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
 644            {
 0645                pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
 646            }
 647
 0648            if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
 649            {
 0650                pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
 651            }
 652
 0653            if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
 654            {
 0655                pluginConfig.StillSize = imageConfig.StillSizes[^1];
 656            }
 0657        }
 658
 659        /// <summary>
 660        /// Gets the <see cref="TMDbClient"/> configuration.
 661        /// </summary>
 662        /// <returns>The configuration.</returns>
 663        public async Task<TMDbConfig> GetClientConfiguration()
 664        {
 665            await EnsureClientConfigAsync().ConfigureAwait(false);
 666
 667            return _tmDbClient.Config;
 668        }
 669
 670        /// <inheritdoc />
 671        public void Dispose()
 672        {
 22673            Dispose(true);
 22674            GC.SuppressFinalize(this);
 22675        }
 676
 677        /// <summary>
 678        /// Releases unmanaged and - optionally - managed resources.
 679        /// </summary>
 680        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 681        protected virtual void Dispose(bool disposing)
 682        {
 22683            if (disposing)
 684            {
 22685                _memoryCache?.Dispose();
 22686                _tmDbClient?.Dispose();
 687            }
 22688        }
 689    }
 690}