aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Plugins/Tmdb
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers/Plugins/Tmdb')
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs163
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs284
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs49
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs65
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs309
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs209
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs446
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs266
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs22
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs134
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs276
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs131
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs212
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs149
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs144
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs230
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs190
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs566
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs64
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs44
71 files changed, 4680 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
new file mode 100644
index 000000000..ad0851cef
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
+{
+ public class TmdbBoxSetExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.TmdbCollection.ToString();
+
+ /// <inheritdoc />
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item)
+ {
+ return item is Movie || item is MusicVideo || item is Trailer;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
new file mode 100644
index 000000000..23eb00b5c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
+{
+ public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IHttpClient _httpClient;
+
+ public TmdbBoxSetImageProvider(IHttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ public string Name => ProviderName;
+
+ public static string ProviderName => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is BoxSet;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary,
+ ImageType.Backdrop
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ var language = item.GetPreferredMetadataLanguage();
+
+ var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false);
+
+ if (mainResult != null)
+ {
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ return GetImages(mainResult, language, tmdbImageUrl);
+ }
+ }
+
+ return new List<RemoteImageInfo>();
+ }
+
+ private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl)
+ {
+ var list = new List<RemoteImageInfo>();
+
+ var images = obj.Images ?? new CollectionImages();
+
+ list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo
+ {
+ Url = baseUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ }));
+
+ list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo
+ {
+ Url = baseUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ }));
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ /// <summary>
+ /// Gets the posters.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
+ private IEnumerable<Poster> GetPosters(CollectionImages images)
+ {
+ return images.Posters ?? new List<Poster>();
+ }
+
+ /// <summary>
+ /// Gets the backdrops.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
+ private IEnumerable<Backdrop> GetBackdrops(CollectionImages images)
+ {
+ var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
+ images.Backdrops;
+
+ return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
+ .ThenByDescending(i => i.Vote_Count);
+ }
+
+ public int Order => 0;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
new file mode 100644
index 000000000..15f0a9004
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
+{
+ public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo>
+ {
+ private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images";
+
+ internal static TmdbBoxSetProvider Current;
+
+ private readonly ILogger<TmdbBoxSetProvider> _logger;
+ private readonly IJsonSerializer _json;
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+ private readonly IHttpClient _httpClient;
+ private readonly ILibraryManager _libraryManager;
+
+ public TmdbBoxSetProvider(
+ ILogger<TmdbBoxSetProvider> logger,
+ IJsonSerializer json,
+ IServerConfigurationManager config,
+ IFileSystem fileSystem,
+ ILocalizationManager localization,
+ IHttpClient httpClient,
+ ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _json = json;
+ _config = config;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ _httpClient = httpClient;
+ _libraryManager = libraryManager;
+ Current = this;
+ }
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage);
+ var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath);
+
+ var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>();
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var result = new RemoteSearchResult
+ {
+ Name = info.Name,
+ SearchProviderName = Name,
+ ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
+ };
+
+ result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
+
+ return new[] { result };
+ }
+
+ return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
+ {
+ var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
+
+ // We don't already have an Id, need to fetch it
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false);
+
+ var searchResult = searchResults.FirstOrDefault();
+
+ if (searchResult != null)
+ {
+ tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ }
+ }
+
+ var result = new MetadataResult<BoxSet>();
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (mainResult != null)
+ {
+ result.HasMetadata = true;
+ result.Item = GetItem(mainResult);
+ }
+ }
+
+ return result;
+ }
+
+ internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language);
+
+ if (!string.IsNullOrEmpty(dataFilePath))
+ {
+ return _json.DeserializeFromFile<CollectionResult>(dataFilePath);
+ }
+
+ return null;
+ }
+
+ private BoxSet GetItem(CollectionResult obj)
+ {
+ var item = new BoxSet
+ {
+ Name = obj.Name,
+ Overview = obj.Overview
+ };
+
+ item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
+
+ return item;
+ }
+
+ private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (mainResult == null) return;
+
+ var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+
+ _json.SerializeToFile(mainResult, dataFilePath);
+ }
+
+ private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
+ {
+ var url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+
+ // Get images in english and with no language
+ url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ CollectionResult mainResult;
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
+ {
+ if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ // Get images in english and with no language
+ url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+ }
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ return mainResult;
+ }
+
+ internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ // If it's recent or automatic updates are enabled, don't re-download
+ if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken);
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage)
+ {
+ var path = GetDataPath(appPaths, tmdbId);
+
+ var filename = string.Format("all-{0}.json", preferredLanguage ?? string.Empty);
+
+ return Path.Combine(path, filename);
+ }
+
+ private static string GetDataPath(IApplicationPaths appPaths, string tmdbId)
+ {
+ var dataPath = GetCollectionsDataPath(appPaths);
+
+ return Path.Combine(dataPath, tmdbId);
+ }
+
+ private static string GetCollectionsDataPath(IApplicationPaths appPaths)
+ {
+ var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections");
+
+ return dataPath;
+ }
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
new file mode 100644
index 000000000..2410ca16b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
+{
+ public class CollectionImages
+ {
+ public List<Backdrop> Backdrops { get; set; }
+ public List<Poster> Posters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
new file mode 100644
index 000000000..3437552df
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
+{
+ public class CollectionResult
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Overview { get; set; }
+ public string Poster_Path { get; set; }
+ public string Backdrop_Path { get; set; }
+ public List<Part> Parts { get; set; }
+ public CollectionImages Images { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
new file mode 100644
index 000000000..462fdab53
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
+{
+ public class Part
+ {
+ public string Title { get; set; }
+ public int Id { get; set; }
+ public string Release_Date { get; set; }
+ public string Poster_Path { get; set; }
+ public string Backdrop_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
new file mode 100644
index 000000000..35e3e2112
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Backdrop
+ {
+ public double Aspect_Ratio { get; set; }
+ public string File_Path { get; set; }
+ public int Height { get; set; }
+ public string Iso_639_1 { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public int Width { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
new file mode 100644
index 000000000..6a5e74ddb
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Crew
+ {
+ public int Id { get; set; }
+ public string Credit_Id { get; set; }
+ public string Name { get; set; }
+ public string Department { get; set; }
+ public string Job { get; set; }
+ public string Profile_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
new file mode 100644
index 000000000..a083f6e9c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class ExternalIds
+ {
+ public string Imdb_Id { get; set; }
+ public object Freebase_Id { get; set; }
+ public string Freebase_Mid { get; set; }
+ public int Tvdb_Id { get; set; }
+ public int Tvrage_Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
new file mode 100644
index 000000000..7f1a394c3
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Genre
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
new file mode 100644
index 000000000..166f9b740
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Images
+ {
+ public List<Backdrop> Backdrops { get; set; }
+ public List<Poster> Posters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
new file mode 100644
index 000000000..72f417be5
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Keyword
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
new file mode 100644
index 000000000..ec2d7a035
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Keywords
+ {
+ public List<Keyword> Results { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
new file mode 100644
index 000000000..0cf04a6ce
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Poster
+ {
+ public double Aspect_Ratio { get; set; }
+ public string File_Path { get; set; }
+ public int Height { get; set; }
+ public string Iso_639_1 { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public int Width { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
new file mode 100644
index 000000000..b45cfc30f
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Profile
+ {
+ public string File_Path { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public object Iso_639_1 { get; set; }
+ public double Aspect_Ratio { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
new file mode 100644
index 000000000..9fc82cfee
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Still
+ {
+ public double Aspect_Ratio { get; set; }
+ public string File_Path { get; set; }
+ public int Height { get; set; }
+ public string Id { get; set; }
+ public string Iso_639_1 { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public int Width { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
new file mode 100644
index 000000000..23af4b697
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class StillImages
+ {
+ public List<Still> Stills { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
new file mode 100644
index 000000000..19bfd62f6
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Video
+ {
+ public string Id { get; set; }
+ public string Iso_639_1 { get; set; }
+ public string Iso_3166_1 { get; set; }
+ public string Key { get; set; }
+ public string Name { get; set; }
+ public string Site { get; set; }
+ public string Size { get; set; }
+ public string Type { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
new file mode 100644
index 000000000..26e839de7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
+{
+ public class Videos
+ {
+ public List<Video> Results { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
new file mode 100644
index 000000000..aaca57f05
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
@@ -0,0 +1,10 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class BelongsToCollection
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Poster_Path { get; set; }
+ public string Backdrop_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
new file mode 100644
index 000000000..d70f218aa
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Cast
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Character { get; set; }
+ public int Order { get; set; }
+ public int Cast_Id { get; set; }
+ public string Profile_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
new file mode 100644
index 000000000..c41699bc7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Casts
+ {
+ public List<Cast> Cast { get; set; }
+ public List<Crew> Crew { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
new file mode 100644
index 000000000..71d1f7c24
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Country
+ {
+ public string Iso_3166_1 { get; set; }
+ public string Certification { get; set; }
+ public DateTime Release_Date { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
new file mode 100644
index 000000000..2a9b9779a
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class MovieResult
+ {
+ public bool Adult { get; set; }
+ public string Backdrop_Path { get; set; }
+ public BelongsToCollection Belongs_To_Collection { get; set; }
+ public int Budget { get; set; }
+ public List<Genre> Genres { get; set; }
+ public string Homepage { get; set; }
+ public int Id { get; set; }
+ public string Imdb_Id { get; set; }
+ public string Original_Title { get; set; }
+ public string Original_Name { get; set; }
+ public string Overview { get; set; }
+ public double Popularity { get; set; }
+ public string Poster_Path { get; set; }
+ public List<ProductionCompany> Production_Companies { get; set; }
+ public List<ProductionCountry> Production_Countries { get; set; }
+ public string Release_Date { get; set; }
+ public int Revenue { get; set; }
+ public int Runtime { get; set; }
+ public List<SpokenLanguage> Spoken_Languages { get; set; }
+ public string Status { get; set; }
+ public string Tagline { get; set; }
+ public string Title { get; set; }
+ public string Name { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public Casts Casts { get; set; }
+ public Releases Releases { get; set; }
+ public Images Images { get; set; }
+ public Keywords Keywords { get; set; }
+ public Trailers Trailers { get; set; }
+
+ public string GetOriginalTitle()
+ {
+ return Original_Name ?? Original_Title;
+ }
+
+ public string GetTitle()
+ {
+ return Name ?? Title ?? GetOriginalTitle();
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
new file mode 100644
index 000000000..11158ade5
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class ProductionCompany
+ {
+ public string Name { get; set; }
+ public int Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
new file mode 100644
index 000000000..43d00fe7a
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class ProductionCountry
+ {
+ public string Iso_3166_1 { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
new file mode 100644
index 000000000..d35111dc4
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Releases
+ {
+ public List<Country> Countries { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
new file mode 100644
index 000000000..41defa9d0
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class SpokenLanguage
+ {
+ public string Iso_639_1 { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
new file mode 100644
index 000000000..bdc40b483
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Trailers
+ {
+ public List<Youtube> Youtube { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
new file mode 100644
index 000000000..6be4ef5b5
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
+{
+ public class Youtube
+ {
+ public string Name { get; set; }
+ public string Size { get; set; }
+ public string Source { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
new file mode 100644
index 000000000..59423c7bc
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
+{
+ public class PersonImages
+ {
+ public List<Profile> Profiles { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
new file mode 100644
index 000000000..50c47eefd
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
+{
+ public class PersonResult
+ {
+ public bool Adult { get; set; }
+ public List<string> Also_Known_As { get; set; }
+ public string Biography { get; set; }
+ public string Birthday { get; set; }
+ public string Deathday { get; set; }
+ public string Homepage { get; set; }
+ public int Id { get; set; }
+ public string Imdb_Id { get; set; }
+ public string Name { get; set; }
+ public string Place_Of_Birth { get; set; }
+ public double Popularity { get; set; }
+ public string Profile_Path { get; set; }
+ public PersonImages Images { get; set; }
+ public ExternalIds External_Ids { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
new file mode 100644
index 000000000..62b12aa97
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
+{
+ public class ExternalIdLookupResult
+ {
+ public List<TvResult> Tv_Results { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
new file mode 100644
index 000000000..51c26a61c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
@@ -0,0 +1,65 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
+{
+ public class MovieResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrop_path.
+ /// </summary>
+ /// <value>The backdrop_path.</value>
+ public string Backdrop_Path { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the original_title.
+ /// </summary>
+ /// <value>The original_title.</value>
+ public string Original_Title { get; set; }
+ /// <summary>
+ /// Gets or sets the original_name.
+ /// </summary>
+ /// <value>The original_name.</value>
+ public string Original_Name { get; set; }
+ /// <summary>
+ /// Gets or sets the release_date.
+ /// </summary>
+ /// <value>The release_date.</value>
+ public string Release_Date { get; set; }
+ /// <summary>
+ /// Gets or sets the poster_path.
+ /// </summary>
+ /// <value>The poster_path.</value>
+ public string Poster_Path { get; set; }
+ /// <summary>
+ /// Gets or sets the popularity.
+ /// </summary>
+ /// <value>The popularity.</value>
+ public double Popularity { get; set; }
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string Title { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_average.
+ /// </summary>
+ /// <value>The vote_average.</value>
+ public double Vote_Average { get; set; }
+ /// <summary>
+ /// For collection search results
+ /// </summary>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_count.
+ /// </summary>
+ /// <value>The vote_count.</value>
+ public int Vote_Count { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
new file mode 100644
index 000000000..c3ad7253a
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
@@ -0,0 +1,29 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
+{
+ public class PersonSearchResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile_ path.
+ /// </summary>
+ /// <value>The profile_ path.</value>
+ public string Profile_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
new file mode 100644
index 000000000..7a33acbc7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
+{
+ public class TmdbSearchResult<T>
+ {
+ /// <summary>
+ /// Gets or sets the page.
+ /// </summary>
+ /// <value>The page.</value>
+ public int Page { get; set; }
+
+ /// <summary>
+ /// Gets or sets the results.
+ /// </summary>
+ /// <value>The results.</value>
+ public List<T> Results { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total_pages.
+ /// </summary>
+ /// <value>The total_pages.</value>
+ public int Total_Pages { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total_results.
+ /// </summary>
+ /// <value>The total_results.</value>
+ public int Total_Results { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
new file mode 100644
index 000000000..b7fbd294c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
@@ -0,0 +1,15 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
+{
+ public class TvResult
+ {
+ public string Backdrop_Path { get; set; }
+ public string First_Air_Date { get; set; }
+ public int Id { get; set; }
+ public string Original_Name { get; set; }
+ public string Poster_Path { get; set; }
+ public double Popularity { get; set; }
+ public string Name { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
new file mode 100644
index 000000000..9c770545c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class Cast
+ {
+ public string Character { get; set; }
+ public string Credit_Id { get; set; }
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Profile_Path { get; set; }
+ public int Order { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
new file mode 100644
index 000000000..bccb234e7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class ContentRating
+ {
+ public string Iso_3166_1 { get; set; }
+ public string Rating { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
new file mode 100644
index 000000000..360c20c66
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class ContentRatings
+ {
+ public List<ContentRating> Results { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
new file mode 100644
index 000000000..35e8eaecb
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class CreatedBy
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Profile_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
new file mode 100644
index 000000000..ebf412c2d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class Credits
+ {
+ public List<Cast> Cast { get; set; }
+ public List<Crew> Crew { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
new file mode 100644
index 000000000..8203632b7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class Episode
+ {
+ public string Air_Date { get; set; }
+ public int Episode_Number { get; set; }
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Overview { get; set; }
+ public string Still_Path { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
new file mode 100644
index 000000000..f89859f85
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class EpisodeCredits
+ {
+ public List<Cast> Cast { get; set; }
+ public List<Crew> Crew { get; set; }
+ public List<GuestStar> Guest_Stars { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
new file mode 100644
index 000000000..e25b65d70
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
@@ -0,0 +1,23 @@
+using System;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class EpisodeResult
+ {
+ public DateTime Air_Date { get; set; }
+ public int Episode_Number { get; set; }
+ public string Name { get; set; }
+ public string Overview { get; set; }
+ public int Id { get; set; }
+ public object Production_Code { get; set; }
+ public int Season_Number { get; set; }
+ public string Still_Path { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public StillImages Images { get; set; }
+ public ExternalIds External_Ids { get; set; }
+ public EpisodeCredits Credits { get; set; }
+ public Tmdb.Models.General.Videos Videos { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
new file mode 100644
index 000000000..260f3f610
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class GuestStar
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Credit_Id { get; set; }
+ public string Character { get; set; }
+ public int Order { get; set; }
+ public string Profile_Path { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
new file mode 100644
index 000000000..5ed310827
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class Network
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
new file mode 100644
index 000000000..fddf950ee
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class Season
+ {
+ public string Air_Date { get; set; }
+ public int Episode_Count { get; set; }
+ public int Id { get; set; }
+ public string Poster_Path { get; set; }
+ public int Season_Number { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
new file mode 100644
index 000000000..13f6d57c8
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class SeasonImages
+ {
+ public List<Poster> Posters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
new file mode 100644
index 000000000..13b4c30f8
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class SeasonResult
+ {
+ public DateTime Air_Date { get; set; }
+ public List<Episode> Episodes { get; set; }
+ public string Name { get; set; }
+ public string Overview { get; set; }
+ public int Id { get; set; }
+ public string Poster_Path { get; set; }
+ public int Season_Number { get; set; }
+ public Credits Credits { get; set; }
+ public SeasonImages Images { get; set; }
+ public ExternalIds External_Ids { get; set; }
+ public General.Videos Videos { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
new file mode 100644
index 000000000..5c1666c77
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
+{
+ public class SeriesResult
+ {
+ public string Backdrop_Path { get; set; }
+ public List<CreatedBy> Created_By { get; set; }
+ public List<int> Episode_Run_Time { get; set; }
+ public DateTime First_Air_Date { get; set; }
+ public List<Genre> Genres { get; set; }
+ public string Homepage { get; set; }
+ public int Id { get; set; }
+ public bool In_Production { get; set; }
+ public List<string> Languages { get; set; }
+ public DateTime Last_Air_Date { get; set; }
+ public string Name { get; set; }
+ public List<Network> Networks { get; set; }
+ public int Number_Of_Episodes { get; set; }
+ public int Number_Of_Seasons { get; set; }
+ public string Original_Name { get; set; }
+ public List<string> Origin_Country { get; set; }
+ public string Overview { get; set; }
+ public string Popularity { get; set; }
+ public string Poster_Path { get; set; }
+ public List<Season> Seasons { get; set; }
+ public string Status { get; set; }
+ public double Vote_Average { get; set; }
+ public int Vote_Count { get; set; }
+ public Credits Credits { get; set; }
+ public Images Images { get; set; }
+ public Keywords Keywords { get; set; }
+ public ExternalIds External_Ids { get; set; }
+ public General.Videos Videos { get; set; }
+ public ContentRatings Content_Ratings { get; set; }
+ public string ResultLanguage { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
new file mode 100644
index 000000000..60f37dc17
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class GenericTmdbMovieInfo<T>
+ where T : BaseItem, new()
+ {
+ private readonly ILogger _logger;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IFileSystem _fileSystem;
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ public GenericTmdbMovieInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem)
+ {
+ _logger = logger;
+ _jsonSerializer = jsonSerializer;
+ _libraryManager = libraryManager;
+ _fileSystem = fileSystem;
+ }
+
+ public async Task<MetadataResult<T>> GetMetadata(ItemLookupInfo itemId, CancellationToken cancellationToken)
+ {
+ var tmdbId = itemId.GetProviderId(MetadataProvider.Tmdb);
+ var imdbId = itemId.GetProviderId(MetadataProvider.Imdb);
+
+ // Don't search for music video id's because it is very easy to misidentify.
+ if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId) && typeof(T) != typeof(MusicVideo))
+ {
+ var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false);
+
+ var searchResult = searchResults.FirstOrDefault();
+
+ if (searchResult != null)
+ {
+ tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ }
+ }
+
+ if (!string.IsNullOrEmpty(tmdbId) || !string.IsNullOrEmpty(imdbId))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return await FetchMovieData(tmdbId, imdbId, itemId.MetadataLanguage, itemId.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new MetadataResult<T>();
+ }
+
+ /// <summary>
+ /// Fetches the movie data.
+ /// </summary>
+ /// <param name="tmdbId">The TMDB identifier.</param>
+ /// <param name="imdbId">The imdb identifier.</param>
+ /// <param name="language">The language.</param>
+ /// <param name="preferredCountryCode">The preferred country code.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{`0}.</returns>
+ private async Task<MetadataResult<T>> FetchMovieData(string tmdbId, string imdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
+ {
+ var item = new MetadataResult<T>
+ {
+ Item = new T()
+ };
+
+ string dataFilePath = null;
+ MovieResult movieInfo = null;
+
+ // Id could be ImdbId or TmdbId
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
+ if (movieInfo != null)
+ {
+ tmdbId = movieInfo.Id.ToString(_usCulture);
+
+ dataFilePath = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+ _jsonSerializer.SerializeToFile(movieInfo, dataFilePath);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(tmdbId))
+ {
+ await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ dataFilePath = dataFilePath ?? TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
+ movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
+
+ var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ ProcessMainInfo(item, settings, preferredCountryCode, movieInfo);
+ item.HasMetadata = true;
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Processes the main info.
+ /// </summary>
+ /// <param name="resultItem">The result item.</param>
+ /// <param name="settings">The settings.</param>
+ /// <param name="preferredCountryCode">The preferred country code.</param>
+ /// <param name="movieData">The movie data.</param>
+ private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieResult movieData)
+ {
+ var movie = resultItem.Item;
+
+ movie.Name = movieData.GetTitle() ?? movie.Name;
+
+ movie.OriginalTitle = movieData.GetOriginalTitle();
+
+ movie.Overview = string.IsNullOrWhiteSpace(movieData.Overview) ? null : WebUtility.HtmlDecode(movieData.Overview);
+ movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
+
+ //movie.HomePageUrl = movieData.homepage;
+
+ if (!string.IsNullOrEmpty(movieData.Tagline))
+ {
+ movie.Tagline = movieData.Tagline;
+ }
+
+ if (movieData.Production_Countries != null)
+ {
+ movie.ProductionLocations = movieData
+ .Production_Countries
+ .Select(i => i.Name)
+ .ToArray();
+ }
+
+ movie.SetProviderId(MetadataProvider.Tmdb, movieData.Id.ToString(_usCulture));
+ movie.SetProviderId(MetadataProvider.Imdb, movieData.Imdb_Id);
+
+ if (movieData.Belongs_To_Collection != null)
+ {
+ movie.SetProviderId(MetadataProvider.TmdbCollection,
+ movieData.Belongs_To_Collection.Id.ToString(CultureInfo.InvariantCulture));
+
+ if (movie is Movie movieItem)
+ {
+ movieItem.CollectionName = movieData.Belongs_To_Collection.Name;
+ }
+ }
+
+ string voteAvg = movieData.Vote_Average.ToString(CultureInfo.InvariantCulture);
+
+ if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var rating))
+ {
+ movie.CommunityRating = rating;
+ }
+
+ //movie.VoteCount = movieData.vote_count;
+
+ if (movieData.Releases != null && movieData.Releases.Countries != null)
+ {
+ var releases = movieData.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
+
+ var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
+ var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
+
+ if (ourRelease != null)
+ {
+ var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-";
+ var newRating = ratingPrefix + ourRelease.Certification;
+
+ newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
+
+ movie.OfficialRating = newRating;
+ }
+ else if (usRelease != null)
+ {
+ movie.OfficialRating = usRelease.Certification;
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(movieData.Release_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParse(movieData.Release_Date, _usCulture, DateTimeStyles.None, out var r))
+ {
+ movie.PremiereDate = r.ToUniversalTime();
+ movie.ProductionYear = movie.PremiereDate.Value.Year;
+ }
+ }
+
+ //studios
+ if (movieData.Production_Companies != null)
+ {
+ movie.SetStudios(movieData.Production_Companies.Select(c => c.Name));
+ }
+
+ // genres
+ // Movies get this from imdb
+ var genres = movieData.Genres ?? new List<Tmdb.Models.General.Genre>();
+
+ foreach (var genre in genres.Select(g => g.Name))
+ {
+ movie.AddGenre(genre);
+ }
+
+ resultItem.ResetPeople();
+ var tmdbImageUrl = settings.images.GetImageUrl("original");
+
+ //Actors, Directors, Writers - all in People
+ //actors come from cast
+ if (movieData.Casts != null && movieData.Casts.Cast != null)
+ {
+ foreach (var actor in movieData.Casts.Cast.OrderBy(a => a.Order))
+ {
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
+
+ if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
+ {
+ personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+ }
+
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ resultItem.AddPerson(personInfo);
+ }
+ }
+
+ //and the rest from crew
+ if (movieData.Casts?.Crew != null)
+ {
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
+
+ foreach (var person in movieData.Casts.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var personInfo = new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
+
+ if (!string.IsNullOrWhiteSpace(person.Profile_Path))
+ {
+ personInfo.ImageUrl = tmdbImageUrl + person.Profile_Path;
+ }
+
+ if (person.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ resultItem.AddPerson(personInfo);
+ }
+ }
+
+ //if (movieData.keywords != null && movieData.keywords.keywords != null)
+ //{
+ // movie.Keywords = movieData.keywords.keywords.Select(i => i.name).ToList();
+ //}
+
+ if (movieData.Trailers != null && movieData.Trailers.Youtube != null)
+ {
+ movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl
+ {
+ Url = string.Format("https://www.youtube.com/watch?v={0}", i.Source),
+ Name = i.Name
+
+ }).ToArray();
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
new file mode 100644
index 000000000..a11c89459
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class TmdbImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+ private readonly IFileSystem _fileSystem;
+
+ public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+ {
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ _fileSystem = fileSystem;
+ }
+
+ public string Name => ProviderName;
+
+ public static string ProviderName => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Movie || item is MusicVideo || item is Trailer;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary,
+ ImageType.Backdrop
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var list = new List<RemoteImageInfo>();
+
+ var language = item.GetPreferredMetadataLanguage();
+
+ var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
+
+ if (results == null)
+ {
+ return list;
+ }
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var supportedImages = GetSupportedImages(item).ToList();
+
+ if (supportedImages.Contains(ImageType.Primary))
+ {
+ list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ }));
+ }
+
+ if (supportedImages.Contains(ImageType.Backdrop))
+ {
+ list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ }));
+ }
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ /// <summary>
+ /// Gets the posters.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
+ private IEnumerable<Poster> GetPosters(Images images)
+ {
+ return images.Posters ?? new List<Poster>();
+ }
+
+ /// <summary>
+ /// Gets the backdrops.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
+ private IEnumerable<Backdrop> GetBackdrops(Images images)
+ {
+ var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
+ images.Backdrops;
+
+ return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
+ .ThenByDescending(i => i.Vote_Count);
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="language">The language.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{MovieImages}.</returns>
+ private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken)
+ {
+ var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+
+ if (string.IsNullOrWhiteSpace(tmdbId))
+ {
+ var imdbId = item.GetProviderId(MetadataProvider.Imdb);
+ if (!string.IsNullOrWhiteSpace(imdbId))
+ {
+ var movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
+ if (movieInfo != null)
+ {
+ tmdbId = movieInfo.Id.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(tmdbId))
+ {
+ return null;
+ }
+
+ await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ var path = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ var fileInfo = _fileSystem.GetFileInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ return jsonSerializer.DeserializeFromFile<MovieResult>(path).Images;
+ }
+ }
+
+ return null;
+ }
+
+ public int Order => 0;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
new file mode 100644
index 000000000..7aec27e97
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class TmdbMovieExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tmdb.ToString();
+
+ /// <inheritdoc />
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item)
+ {
+ // Supports images for tv movies
+ if (item is LiveTvProgram tvProgram && tvProgram.IsMovie)
+ {
+ return true;
+ }
+
+ return item is Movie || item is MusicVideo || item is Trailer;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
new file mode 100644
index 000000000..64d3ecd7b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -0,0 +1,446 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ /// <summary>
+ /// Class MovieDbProvider
+ /// </summary>
+ public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder
+ {
+ internal static TmdbMovieProvider Current { get; private set; }
+
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly ILogger<TmdbMovieProvider> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IApplicationHost _appHost;
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ public TmdbMovieProvider(
+ IJsonSerializer jsonSerializer,
+ IHttpClient httpClient,
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager,
+ ILogger<TmdbMovieProvider> logger,
+ ILibraryManager libraryManager,
+ IApplicationHost appHost)
+ {
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ _fileSystem = fileSystem;
+ _configurationManager = configurationManager;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _appHost = appHost;
+ Current = this;
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return GetMovieSearchResults(searchInfo, cancellationToken);
+ }
+
+ public async Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
+
+ var obj = _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
+
+ var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = obj.GetTitle(),
+ SearchProviderName = Name,
+ ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
+ };
+
+ if (!string.IsNullOrWhiteSpace(obj.Release_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r))
+ {
+ remoteResult.PremiereDate = r.ToUniversalTime();
+ remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ }
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
+
+ if (!string.IsNullOrWhiteSpace(obj.Imdb_Id))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id);
+ }
+
+ return new[] { remoteResult };
+ }
+
+ return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ }
+
+ public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
+ {
+ return GetItemMetadata<Movie>(info, cancellationToken);
+ }
+
+ public Task<MetadataResult<T>> GetItemMetadata<T>(ItemLookupInfo id, CancellationToken cancellationToken)
+ where T : BaseItem, new()
+ {
+ var movieDb = new GenericTmdbMovieInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem);
+
+ return movieDb.GetMetadata(id, cancellationToken);
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <summary>
+ /// The _TMDB settings task
+ /// </summary>
+ private TmdbSettingsResult _tmdbSettings;
+
+ /// <summary>
+ /// Gets the TMDB settings.
+ /// </summary>
+ /// <returns>Task{TmdbSettingsResult}.</returns>
+ internal async Task<TmdbSettingsResult> GetTmdbSettings(CancellationToken cancellationToken)
+ {
+ if (_tmdbSettings != null)
+ {
+ return _tmdbSettings;
+ }
+
+ using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey),
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (Stream json = response.Content)
+ {
+ _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(json).ConfigureAwait(false);
+
+ return _tmdbSettings;
+ }
+ }
+ }
+
+ private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
+ private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
+
+ /// <summary>
+ /// Gets the movie data path.
+ /// </summary>
+ /// <param name="appPaths">The app paths.</param>
+ /// <param name="tmdbId">The TMDB id.</param>
+ /// <returns>System.String.</returns>
+ internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId)
+ {
+ var dataPath = GetMoviesDataPath(appPaths);
+
+ return Path.Combine(dataPath, tmdbId);
+ }
+
+ internal static string GetMoviesDataPath(IApplicationPaths appPaths)
+ {
+ var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2");
+
+ return dataPath;
+ }
+
+ /// <summary>
+ /// Downloads the movie info.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (mainResult == null) return;
+
+ var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+
+ _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ }
+
+ internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ var path = GetDataFilePath(tmdbId, language);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ // If it's recent or automatic updates are enabled, don't re-download
+ if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ return DownloadMovieInfo(tmdbId, language, cancellationToken);
+ }
+
+ internal string GetDataFilePath(string tmdbId, string preferredLanguage)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId);
+
+ if (string.IsNullOrWhiteSpace(preferredLanguage))
+ {
+ preferredLanguage = "alllang";
+ }
+
+ var filename = string.Format("all-{0}.json", preferredLanguage);
+
+ return Path.Combine(path, filename);
+ }
+
+ public static string GetImageLanguagesParam(string preferredLanguage)
+ {
+ var languages = new List<string>();
+
+ if (!string.IsNullOrEmpty(preferredLanguage))
+ {
+ preferredLanguage = NormalizeLanguage(preferredLanguage);
+
+ languages.Add(preferredLanguage);
+
+ if (preferredLanguage.Length == 5) // like en-US
+ {
+ // Currenty, TMDB supports 2-letter language codes only
+ // They are planning to change this in the future, thus we're
+ // supplying both codes if we're having a 5-letter code.
+ languages.Add(preferredLanguage.Substring(0, 2));
+ }
+ }
+
+ languages.Add("null");
+
+ if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ languages.Add("en");
+ }
+
+ return string.Join(",", languages.ToArray());
+ }
+
+ public static string NormalizeLanguage(string language)
+ {
+ if (!string.IsNullOrEmpty(language))
+ {
+ // They require this to be uppercase
+ // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
+ // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
+ var parts = language.Split('-');
+
+ if (parts.Length == 2)
+ {
+ language = parts[0] + "-" + parts[1].ToUpperInvariant();
+ }
+ }
+
+ return language;
+ }
+
+ public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
+ {
+ if (!string.IsNullOrEmpty(imageLanguage)
+ && !string.IsNullOrEmpty(requestLanguage)
+ && requestLanguage.Length > 2
+ && imageLanguage.Length == 2
+ && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
+ {
+ return requestLanguage;
+ }
+
+ return imageLanguage;
+ }
+
+ /// <summary>
+ /// Fetches the main result.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <param name="isTmdbId">if set to <c>true</c> [is TMDB identifier].</param>
+ /// <param name="language">The language.</param>
+ /// <param name="cancellationToken">The cancellation token</param>
+ /// <returns>Task{CompleteMovieData}.</returns>
+ internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
+ {
+ var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ url += string.Format("&language={0}", NormalizeLanguage(language));
+
+ // Get images in english and with no language
+ url += "&include_image_language=" + GetImageLanguagesParam(language);
+ }
+
+ MovieResult mainResult;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Cache if not using a tmdbId because we won't have the tmdb cache directory structure. So use the lower level cache.
+ var cacheMode = isTmdbId ? CacheMode.None : CacheMode.Unconditional;
+ var cacheLength = TimeSpan.FromDays(3);
+
+ try
+ {
+ using (var response = await GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader,
+ CacheMode = cacheMode,
+ CacheLength = cacheLength
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (HttpException ex)
+ {
+ // Return null so that callers know there is no metadata for this id
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+
+ throw;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // If the language preference isn't english, then have the overview fallback to english if it's blank
+ if (mainResult != null &&
+ string.IsNullOrEmpty(mainResult.Overview) &&
+ !string.IsNullOrEmpty(language) &&
+ !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
+
+ url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ // Get images in english and with no language
+ url += "&include_image_language=" + GetImageLanguagesParam(language);
+ }
+
+ using (var response = await GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader,
+ CacheMode = cacheMode,
+ CacheLength = cacheLength
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false);
+
+ mainResult.Overview = englishResult.Overview;
+ }
+ }
+ }
+
+ return mainResult;
+ }
+
+ private static long _lastRequestTicks;
+ // The limit is 40 requests per 10 seconds
+ private const int RequestIntervalMs = 300;
+
+ /// <summary>
+ /// Gets the movie db response.
+ /// </summary>
+ internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
+ {
+ var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks);
+ var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs);
+
+ if (delayMs > 0)
+ {
+ _logger.LogDebug("Throttling Tmdb by {0} ms", delayMs);
+ await Task.Delay(Convert.ToInt32(delayMs)).ConfigureAwait(false);
+ }
+
+ _lastRequestTicks = DateTime.UtcNow.Ticks;
+
+ options.BufferContent = true;
+ options.UserAgent = _appHost.ApplicationUserAgent;
+
+ return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public int Order => 1;
+
+ /// <inheritdoc />
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
new file mode 100644
index 000000000..e1e34afb9
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class TmdbSearch
+ {
+ private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled);
+ private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled);
+ private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary
+ 19[0-9]{2}|20[0-9]{2}| # 1900-2099
+ S[0-9]{2}| # Season
+ E[0-9]{2}| # Episode
+ (2160|1080|720|576|480)[ip]?| # Resolution
+ [xh]?264| # Encoding
+ (web|dvd|bd|hdtv|hd)rip| # *Rip
+ web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac
+ ).* # Match rest of string",
+ RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
+
+ private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
+
+ private readonly ILogger _logger;
+ private readonly IJsonSerializer _json;
+ private readonly ILibraryManager _libraryManager;
+
+ public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _json = json;
+ _libraryManager = libraryManager;
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken)
+ {
+ return GetSearchResults(idInfo, "tv", cancellationToken);
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken)
+ {
+ return GetSearchResults(idInfo, "movie", cancellationToken);
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken)
+ {
+ return GetSearchResults(idInfo, "collection", cancellationToken);
+ }
+
+ private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken)
+ {
+ var name = idInfo.Name;
+ var year = idInfo.Year;
+
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return new List<RemoteSearchResult>();
+ }
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ // ParseName is required here.
+ // Caller provides the filename with extension stripped and NOT the parsed filename
+ var parsedName = _libraryManager.ParseName(name);
+ var yearInName = parsedName.Year;
+ name = parsedName.Name;
+ year ??= yearInName;
+
+ var language = idInfo.MetadataLanguage.ToLowerInvariant();
+
+ // Replace sequences of non-word characters with space
+ // TMDB expects a space separated list of words make sure that is the case
+ name = _cleanNonWord.Replace(name, " ").Trim();
+
+ _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year);
+ var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
+
+ if (results.Count == 0)
+ {
+ //try in english if wasn't before
+ if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ // TODO: retrying alternatives should be done outside the search
+ // provider so that the retry logic can be common for all search
+ // providers
+ if (results.Count == 0)
+ {
+ var name2 = parsedName.Name;
+
+ // Remove things enclosed in []{}() etc
+ name2 = _cleanEnclosed.Replace(name2, string.Empty);
+
+ // Replace sequences of non-word characters with space
+ name2 = _cleanNonWord.Replace(name2, " ");
+
+ // Clean based on common stop words / tokens
+ name2 = _cleanStopWords.Replace(name2, string.Empty);
+
+ // Trim whitespace
+ name2 = name2.Trim();
+
+ // Search again if the new name is different
+ if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2))
+ {
+ _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year);
+ results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
+
+ if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ //one more time, in english
+ results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ return results.Where(i =>
+ {
+ if (year.HasValue && i.ProductionYear.HasValue)
+ {
+ // Allow one year tolerance
+ return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
+ }
+
+ return true;
+ });
+ }
+
+ private Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
+ {
+ switch (type)
+ {
+ case "tv":
+ return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken);
+ default:
+ return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken);
+ }
+ }
+
+ private async Task<List<RemoteSearchResult>> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("name");
+ }
+
+ var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type);
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url3,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(json).ConfigureAwait(false);
+
+ var results = searchResults.Results ?? new List<MovieResult>();
+
+ return results
+ .Select(i =>
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ SearchProviderName = TmdbMovieProvider.Current.Name,
+ Name = i.Title ?? i.Name ?? i.Original_Title,
+ ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
+ };
+
+ if (!string.IsNullOrWhiteSpace(i.Release_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
+ {
+ remoteResult.PremiereDate = r.ToUniversalTime();
+ remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ }
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+ return remoteResult;
+
+ })
+ .ToList();
+ }
+ }
+ }
+
+ private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("name");
+ }
+
+ var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url3,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(json).ConfigureAwait(false);
+
+ var results = searchResults.Results ?? new List<TvResult>();
+
+ return results
+ .Select(i =>
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ SearchProviderName = TmdbMovieProvider.Current.Name,
+ Name = i.Name ?? i.Original_Name,
+ ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
+ };
+
+ if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
+ {
+ // These dates are always in this exact format
+ if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
+ {
+ remoteResult.PremiereDate = r.ToUniversalTime();
+ remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ }
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+ return remoteResult;
+
+ })
+ .ToList();
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs
new file mode 100644
index 000000000..03669ca67
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ internal class TmdbImageSettings
+ {
+ public List<string> backdrop_sizes { get; set; }
+ public string secure_base_url { get; set; }
+ public List<string> poster_sizes { get; set; }
+ public List<string> profile_sizes { get; set; }
+
+ public string GetImageUrl(string image)
+ {
+ return secure_base_url + image;
+ }
+ }
+
+ internal class TmdbSettingsResult
+ {
+ public TmdbImageSettings images { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
new file mode 100644
index 000000000..d173bcc9a
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Music
+{
+ public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo>
+ {
+ public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
+ {
+ return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken);
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
+ }
+
+ public string Name => TmdbMovieProvider.Current.Name;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
new file mode 100644
index 000000000..70cd1cd95
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.People
+{
+ public class TmdbPersonExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tmdb.ToString();
+
+ /// <inheritdoc />
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item)
+ {
+ return item is Person;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
new file mode 100644
index 000000000..525c0072b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.People
+{
+ public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IServerConfigurationManager _config;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+
+ public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient)
+ {
+ _config = config;
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ }
+
+ public string Name => ProviderName;
+
+ public static string ProviderName => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Person;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var person = (Person)item;
+ var id = person.GetProviderId(MetadataProvider.Tmdb);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id);
+
+ var result = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+
+ var images = result.Images ?? new PersonImages();
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl);
+ }
+
+ return new List<RemoteImageInfo>();
+ }
+
+ private IEnumerable<RemoteImageInfo> GetImages(PersonImages images, string preferredLanguage, string baseImageUrl)
+ {
+ var list = new List<RemoteImageInfo>();
+
+ if (images.Profiles != null)
+ {
+ list.AddRange(images.Profiles.Select(i => new RemoteImageInfo
+ {
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ Width = i.Width,
+ Height = i.Height,
+ Language = GetLanguage(i),
+ Url = baseImageUrl + i.File_Path
+ }));
+ }
+
+ var language = preferredLanguage;
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ private string GetLanguage(Profile profile)
+ {
+ return profile.Iso_639_1?.ToString();
+ }
+
+ public int Order => 0;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
new file mode 100644
index 000000000..3f28483f7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.People
+{
+ public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
+ {
+ const string DataFileName = "info.json";
+
+ internal static TmdbPersonProvider Current { get; private set; }
+
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IHttpClient _httpClient;
+ private readonly ILogger<TmdbPersonProvider> _logger;
+
+ public TmdbPersonProvider(
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager,
+ IJsonSerializer jsonSerializer,
+ IHttpClient httpClient,
+ ILogger<TmdbPersonProvider> logger)
+ {
+ _fileSystem = fileSystem;
+ _configurationManager = configurationManager;
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ _logger = logger;
+ Current = this;
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
+ var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+
+ var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>();
+
+ var result = new RemoteSearchResult
+ {
+ Name = info.Name,
+
+ SearchProviderName = Name,
+
+ ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
+ };
+
+ result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
+ result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+
+ return new[] { result };
+ }
+
+ if (searchInfo.IsAutomated)
+ {
+ // Don't hammer moviedb searching by name
+ return new List<RemoteSearchResult>();
+ }
+
+ var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey);
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var result = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(json).ConfigureAwait(false) ??
+ new TmdbSearchResult<PersonSearchResult>();
+
+ return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
+ }
+ }
+ }
+
+ private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl)
+ {
+ var result = new RemoteSearchResult
+ {
+ SearchProviderName = Name,
+
+ Name = i.Name,
+
+ ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path
+ };
+
+ result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+
+ return result;
+ }
+
+ public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken)
+ {
+ var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
+
+ // We don't already have an Id, need to fetch it
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false);
+ }
+
+ var result = new MetadataResult<Person>();
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ try
+ {
+ await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ return result;
+ }
+
+ throw;
+ }
+
+ var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
+
+ var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+
+ var item = new Person();
+ result.HasMetadata = true;
+
+ // Take name from incoming info, don't rename the person
+ // TODO: This should go in PersonMetadataService, not each person provider
+ item.Name = id.Name;
+
+ //item.HomePageUrl = info.homepage;
+
+ if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth))
+ {
+ item.ProductionLocations = new string[] { info.Place_Of_Birth };
+ }
+ item.Overview = info.Biography;
+
+ if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date))
+ {
+ item.PremiereDate = date.ToUniversalTime();
+ }
+
+ if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+ {
+ item.EndDate = date.ToUniversalTime();
+ }
+
+ item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
+
+ if (!string.IsNullOrEmpty(info.Imdb_Id))
+ {
+ item.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+ }
+
+ result.HasMetadata = true;
+ result.Item = item;
+ }
+
+ return result;
+ }
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets the TMDB id.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken)
+ {
+ var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
+
+ return results.Select(i => i.GetProviderId(MetadataProvider.Tmdb)).FirstOrDefault();
+ }
+
+ internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken)
+ {
+ var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath);
+
+ if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return;
+ }
+
+ var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id);
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+
+ using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+ {
+ await json.CopyToAsync(fs).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
+ {
+ var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
+
+ return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
+ }
+
+ internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
+ {
+ return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName);
+ }
+
+ private static string GetPersonsDataPath(IApplicationPaths appPaths)
+ {
+ return Path.Combine(appPaths.CachePath, "tmdb-people");
+ }
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
new file mode 100644
index 000000000..3fa47d54b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbEpisodeImageProvider :
+ TmdbEpisodeProviderBase,
+ IRemoteImageProvider,
+ IHasOrder
+ {
+ public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ { }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var episode = (Controller.Entities.TV.Episode)item;
+ var series = episode.Series;
+
+ var seriesId = series != null ? series.GetProviderId(MetadataProvider.Tmdb) : null;
+
+ var list = new List<RemoteImageInfo>();
+
+ if (string.IsNullOrEmpty(seriesId))
+ {
+ return list;
+ }
+
+ var seasonNumber = episode.ParentIndexNumber;
+ var episodeNumber = episode.IndexNumber;
+
+ if (!seasonNumber.HasValue || !episodeNumber.HasValue)
+ {
+ return list;
+ }
+
+ var language = item.GetPreferredMetadataLanguage();
+
+ var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value,
+ language, cancellationToken).ConfigureAwait(false);
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ }));
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ private IEnumerable<Still> GetPosters(StillImages images)
+ {
+ return images.Stills ?? new List<Still>();
+ }
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return GetResponse(url, cancellationToken);
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Controller.Entities.TV.Episode;
+ }
+
+ // After TheTvDb
+ public int Order => 1;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
new file mode 100644
index 000000000..01b295f86
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbEpisodeProvider :
+ TmdbEpisodeProviderBase,
+ IRemoteMetadataProvider<Episode, EpisodeInfo>,
+ IHasOrder
+ {
+ public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ { }
+
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var list = new List<RemoteSearchResult>();
+
+ // The search query must either provide an episode number or date
+ if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
+ {
+ return list;
+ }
+
+ var metadataResult = await GetMetadata(searchInfo, cancellationToken);
+
+ if (metadataResult.HasMetadata)
+ {
+ var item = metadataResult.Item;
+
+ list.Add(new RemoteSearchResult
+ {
+ IndexNumber = item.IndexNumber,
+ Name = item.Name,
+ ParentIndexNumber = item.ParentIndexNumber,
+ PremiereDate = item.PremiereDate,
+ ProductionYear = item.ProductionYear,
+ ProviderIds = item.ProviderIds,
+ SearchProviderName = Name,
+ IndexNumberEnd = item.IndexNumberEnd
+ });
+ }
+
+ return list;
+ }
+
+ public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<Episode>();
+
+ // Allowing this will dramatically increase scan times
+ if (info.IsMissingEpisode)
+ {
+ return result;
+ }
+
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+
+ if (string.IsNullOrEmpty(seriesTmdbId))
+ {
+ return result;
+ }
+
+ var seasonNumber = info.ParentIndexNumber;
+ var episodeNumber = info.IndexNumber;
+
+ if (!seasonNumber.HasValue || !episodeNumber.HasValue)
+ {
+ return result;
+ }
+
+ try
+ {
+ var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ result.HasMetadata = true;
+ result.QueriedById = true;
+
+ if (!string.IsNullOrEmpty(response.Overview))
+ {
+ // if overview is non-empty, we can assume that localized data was returned
+ result.ResultLanguage = info.MetadataLanguage;
+ }
+
+ var item = new Episode();
+ result.Item = item;
+
+ item.Name = info.Name;
+ item.IndexNumber = info.IndexNumber;
+ item.ParentIndexNumber = info.ParentIndexNumber;
+ item.IndexNumberEnd = info.IndexNumberEnd;
+
+ if (response.External_Ids.Tvdb_Id > 0)
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ item.PremiereDate = response.Air_Date;
+ item.ProductionYear = result.Item.PremiereDate.Value.Year;
+
+ item.Name = response.Name;
+ item.Overview = response.Overview;
+
+ item.CommunityRating = (float)response.Vote_Average;
+
+ if (response.Videos?.Results != null)
+ {
+ foreach (var video in response.Videos.Results)
+ {
+ if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase)
+ || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase))
+ {
+ if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
+ {
+ var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.Key);
+ item.AddTrailerUrl(videoUrl);
+ }
+ }
+ }
+ }
+
+ result.ResetPeople();
+
+ var credits = response.Credits;
+ if (credits != null)
+ {
+ //Actors, Directors, Writers - all in People
+ //actors come from cast
+ if (credits.Cast != null)
+ {
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order))
+ {
+ result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order });
+ }
+ }
+
+ // guest stars
+ if (credits.Guest_Stars != null)
+ {
+ foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order))
+ {
+ result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order });
+ }
+ }
+
+ //and the rest from crew
+ if (credits.Crew != null)
+ {
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
+
+ foreach (var person in credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type });
+ }
+ }
+ }
+ }
+ catch (HttpException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ return result;
+ }
+
+ throw;
+ }
+
+ return result;
+ }
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return GetResponse(url, cancellationToken);
+ }
+
+ // After TheTvDb
+ public int Order => 1;
+
+ public string Name => TmdbUtils.ProviderName;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
new file mode 100644
index 000000000..f82f5f2ab
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public abstract class TmdbEpisodeProviderBase
+ {
+ private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
+ private readonly IHttpClient _httpClient;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+ private readonly ILogger<TmdbEpisodeProviderBase> _logger;
+
+ protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
+ {
+ _httpClient = httpClient;
+ _configurationManager = configurationManager;
+ _jsonSerializer = jsonSerializer;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ _logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
+ }
+
+ protected ILogger Logger => _logger;
+
+ protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage,
+ CancellationToken cancellationToken)
+ {
+ await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage);
+
+ return _jsonSerializer.DeserializeFromFile<EpisodeResult>(dataFilePath);
+ }
+
+ internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ if (string.IsNullOrEmpty(language))
+ {
+ throw new ArgumentNullException(nameof(language));
+ }
+
+ var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ // If it's recent or automatic updates are enabled, don't re-download
+ if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken);
+ }
+
+ internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ if (string.IsNullOrEmpty(preferredLanguage))
+ {
+ throw new ArgumentNullException(nameof(preferredLanguage));
+ }
+
+ var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
+
+ var filename = string.Format("season-{0}-episode-{1}-{2}.json",
+ seasonNumber.ToString(CultureInfo.InvariantCulture),
+ episodeNumber.ToString(CultureInfo.InvariantCulture),
+ preferredLanguage);
+
+ return Path.Combine(path, filename);
+ }
+
+ internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+ _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ }
+
+ internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
+ {
+ var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, TmdbUtils.ApiKey);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ url += string.Format("&language={0}", language);
+ }
+
+ var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
+ // Get images in english and with no language
+ url += "&include_image_language=" + includeImageLanguageParam;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(json).ConfigureAwait(false);
+ }
+ }
+ }
+
+ protected Task<HttpResponseInfo> GetResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
new file mode 100644
index 000000000..b5456b45c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+
+ public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+ {
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ }
+
+ public int Order => 1;
+
+ public string Name => ProviderName;
+
+ public static string ProviderName => TmdbUtils.ProviderName;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var season = (Season)item;
+ var series = season.Series;
+
+ var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
+
+ if (string.IsNullOrEmpty(seriesId))
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var seasonNumber = season.IndexNumber;
+
+ if (!seasonNumber.HasValue)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var language = item.GetPreferredMetadataLanguage();
+
+ var results = await FetchImages(season, seriesId, language, cancellationToken).ConfigureAwait(false);
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var list = results.Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ });
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
+ {
+ await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false);
+
+ var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ if (File.Exists(path))
+ {
+ return _jsonSerializer.DeserializeFromFile<Models.TV.SeasonResult>(path).Images.Posters;
+ }
+ }
+
+ return null;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary
+ };
+ }
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Season;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
new file mode 100644
index 000000000..c7cd672b4
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
+ {
+ private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
+ private readonly IHttpClient _httpClient;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILocalizationManager _localization;
+ private readonly ILogger<TmdbSeasonProvider> _logger;
+
+ internal static TmdbSeasonProvider Current { get; private set; }
+
+ public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger)
+ {
+ _httpClient = httpClient;
+ _configurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ _localization = localization;
+ _jsonSerializer = jsonSerializer;
+ _logger = logger;
+ Current = this;
+ }
+
+ public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<Season>();
+
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+
+ var seasonNumber = info.IndexNumber;
+
+ if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue)
+ {
+ try
+ {
+ var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ result.HasMetadata = true;
+ result.Item = new Season();
+
+ // Don't use moviedb season names for now until if/when we have field-level configuration
+ //result.Item.Name = seasonInfo.name;
+
+ result.Item.Name = info.Name;
+
+ result.Item.IndexNumber = seasonNumber;
+
+ result.Item.Overview = seasonInfo.Overview;
+
+ if (seasonInfo.External_Ids.Tvdb_Id > 0)
+ {
+ result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ var credits = seasonInfo.Credits;
+ if (credits != null)
+ {
+ //Actors, Directors, Writers - all in People
+ //actors come from cast
+ if (credits.Cast != null)
+ {
+ //foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order });
+ }
+
+ //and the rest from crew
+ if (credits.Crew != null)
+ {
+ //foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department });
+ }
+ }
+
+ result.Item.PremiereDate = seasonInfo.Air_Date;
+ result.Item.ProductionYear = result.Item.PremiereDate.Value.Year;
+ }
+ catch (HttpException ex)
+ {
+ _logger.LogError(ex, "No metadata found for {0}", seasonNumber.Value);
+
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ return result;
+ }
+
+ throw;
+ }
+ }
+
+ return result;
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
+ }
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+
+ private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage,
+ CancellationToken cancellationToken)
+ {
+ await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage);
+
+ return _jsonSerializer.DeserializeFromFile<SeasonResult>(dataFilePath);
+ }
+
+ internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ if (string.IsNullOrEmpty(language))
+ {
+ throw new ArgumentNullException(nameof(language));
+ }
+
+ var path = GetDataFilePath(tmdbId, seasonNumber, language);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ // If it's recent or automatic updates are enabled, don't re-download
+ if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken);
+ }
+
+ internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ if (string.IsNullOrEmpty(preferredLanguage))
+ {
+ throw new ArgumentNullException(nameof(preferredLanguage));
+ }
+
+ var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
+
+ var filename = string.Format("season-{0}-{1}.json",
+ seasonNumber.ToString(CultureInfo.InvariantCulture),
+ preferredLanguage);
+
+ return Path.Combine(path, filename);
+ }
+
+ internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+ _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ }
+
+ internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken)
+ {
+ var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), TmdbUtils.ApiKey);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+ }
+
+ var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
+ // Get images in english and with no language
+ url += "&include_image_language=" + includeImageLanguageParam;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(json).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
new file mode 100644
index 000000000..705f8041b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbSeriesExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.Tmdb.ToString();
+
+ /// <inheritdoc />
+ public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item)
+ {
+ return item is Series;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
new file mode 100644
index 000000000..40824d88d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IHttpClient _httpClient;
+ private readonly IFileSystem _fileSystem;
+
+ public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem)
+ {
+ _jsonSerializer = jsonSerializer;
+ _httpClient = httpClient;
+ _fileSystem = fileSystem;
+ }
+
+ public string Name => ProviderName;
+
+ public static string ProviderName => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Series;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary,
+ ImageType.Backdrop
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var list = new List<RemoteImageInfo>();
+
+ var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
+
+ if (results == null)
+ {
+ return list;
+ }
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var language = item.GetPreferredMetadataLanguage();
+
+ list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ }));
+
+ list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo
+ {
+ Url = tmdbImageUrl + i.File_Path,
+ CommunityRating = i.Vote_Average,
+ VoteCount = i.Vote_Count,
+ Width = i.Width,
+ Height = i.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ }));
+
+ var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+ return list.OrderByDescending(i =>
+ {
+ if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 3;
+ }
+
+ if (!isLanguageEn)
+ {
+ if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+ }
+
+ if (string.IsNullOrEmpty(i.Language))
+ {
+ return isLanguageEn ? 3 : 2;
+ }
+
+ return 0;
+ })
+ .ThenByDescending(i => i.CommunityRating ?? 0)
+ .ThenByDescending(i => i.VoteCount ?? 0);
+ }
+
+ /// <summary>
+ /// Gets the posters.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ private IEnumerable<Poster> GetPosters(Images images)
+ {
+ return images.Posters ?? new List<Poster>();
+ }
+
+ /// <summary>
+ /// Gets the backdrops.
+ /// </summary>
+ /// <param name="images">The images.</param>
+ private IEnumerable<Backdrop> GetBackdrops(Images images)
+ {
+ var eligibleBackdrops = images.Backdrops ?? new List<Backdrop>();
+
+ return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
+ .ThenByDescending(i => i.Vote_Count);
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="language">The language.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{MovieImages}.</returns>
+ private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer,
+ CancellationToken cancellationToken)
+ {
+ var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ return null;
+ }
+
+ await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ var fileInfo = _fileSystem.GetFileInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
+ }
+ }
+
+ return null;
+ }
+
+ // After tvdb and fanart
+ public int Order => 2;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
new file mode 100644
index 000000000..7e46a65bb
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -0,0 +1,566 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV
+{
+ public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
+ {
+ private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
+
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly ILogger<TmdbSeriesProvider> _logger;
+ private readonly ILocalizationManager _localization;
+ private readonly IHttpClient _httpClient;
+ private readonly ILibraryManager _libraryManager;
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ internal static TmdbSeriesProvider Current { get; private set; }
+
+ public TmdbSeriesProvider(
+ IJsonSerializer jsonSerializer,
+ IFileSystem fileSystem,
+ IServerConfigurationManager configurationManager,
+ ILogger<TmdbSeriesProvider> logger,
+ ILocalizationManager localization,
+ IHttpClient httpClient,
+ ILibraryManager libraryManager)
+ {
+ _jsonSerializer = jsonSerializer;
+ _fileSystem = fileSystem;
+ _configurationManager = configurationManager;
+ _logger = logger;
+ _localization = localization;
+ _httpClient = httpClient;
+ _libraryManager = libraryManager;
+ Current = this;
+ }
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
+
+ var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath);
+
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = obj.Name,
+ SearchProviderName = Name,
+ ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
+ remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
+
+ if (obj.External_Ids.Tvdb_Id > 0)
+ {
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.ToString(_usCulture));
+ }
+
+ return new[] { remoteResult };
+ }
+
+ var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
+
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+
+ if (searchResult != null)
+ {
+ return new[] { searchResult };
+ }
+ }
+
+ var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
+
+ if (!string.IsNullOrEmpty(tvdbId))
+ {
+ var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+
+ if (searchResult != null)
+ {
+ return new[] { searchResult };
+ }
+ }
+
+ return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<Series>();
+ result.QueriedById = true;
+
+ var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
+
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ var imdbId = info.GetProviderId(MetadataProvider.Imdb);
+
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+
+ if (searchResult != null)
+ {
+ tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ var tvdbId = info.GetProviderId(MetadataProvider.Tvdb);
+
+ if (!string.IsNullOrEmpty(tvdbId))
+ {
+ var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+
+ if (searchResult != null)
+ {
+ tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ result.QueriedById = false;
+ var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false);
+
+ var searchResult = searchResults.FirstOrDefault();
+
+ if (searchResult != null)
+ {
+ tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ }
+ }
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+
+ result.HasMetadata = result.Item != null;
+ }
+
+ return result;
+ }
+
+ private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
+ {
+ SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ if (seriesInfo == null)
+ {
+ return null;
+ }
+
+ tmdbId = seriesInfo.Id.ToString(_usCulture);
+
+ string dataFilePath = GetDataFilePath(tmdbId, language);
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+ _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
+
+ await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+
+ var result = new MetadataResult<Series>();
+ result.Item = new Series();
+ result.ResultLanguage = seriesInfo.ResultLanguage;
+
+ var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+
+ ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings);
+
+ return result;
+ }
+
+ private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings)
+ {
+ var series = seriesResult.Item;
+
+ series.Name = seriesInfo.Name;
+ series.OriginalTitle = seriesInfo.Original_Name;
+ series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture));
+
+ string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
+
+ if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
+ {
+ series.CommunityRating = rating;
+ }
+
+ series.Overview = seriesInfo.Overview;
+
+ if (seriesInfo.Networks != null)
+ {
+ series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray();
+ }
+
+ if (seriesInfo.Genres != null)
+ {
+ series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
+ }
+
+ series.HomePageUrl = seriesInfo.Homepage;
+
+ series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
+
+ if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase))
+ {
+ series.Status = SeriesStatus.Ended;
+ series.EndDate = seriesInfo.Last_Air_Date;
+ }
+ else
+ {
+ series.Status = SeriesStatus.Continuing;
+ }
+
+ series.PremiereDate = seriesInfo.First_Air_Date;
+
+ var ids = seriesInfo.External_Ids;
+ if (ids != null)
+ {
+ if (!string.IsNullOrWhiteSpace(ids.Imdb_Id))
+ {
+ series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id);
+ }
+
+ if (ids.Tvrage_Id > 0)
+ {
+ series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.ToString(_usCulture));
+ }
+
+ if (ids.Tvdb_Id > 0)
+ {
+ series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.ToString(_usCulture));
+ }
+ }
+
+ var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>();
+
+ var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
+ var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
+ var minimumRelease = contentRatings.FirstOrDefault();
+
+ if (ourRelease != null)
+ {
+ series.OfficialRating = ourRelease.Rating;
+ }
+ else if (usRelease != null)
+ {
+ series.OfficialRating = usRelease.Rating;
+ }
+ else if (minimumRelease != null)
+ {
+ series.OfficialRating = minimumRelease.Rating;
+ }
+
+ if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null)
+ {
+ foreach (var video in seriesInfo.Videos.Results)
+ {
+ if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
+ || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase))
+ && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase))
+ {
+ series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}");
+ }
+ }
+ }
+
+ seriesResult.ResetPeople();
+ var tmdbImageUrl = settings.images.GetImageUrl("original");
+
+ if (seriesInfo.Credits != null)
+ {
+ if (seriesInfo.Credits.Cast != null)
+ {
+ foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
+ {
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
+
+ if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
+ {
+ personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
+ }
+
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ seriesResult.AddPerson(personInfo);
+ }
+ }
+
+ if (seriesInfo.Credits.Crew != null)
+ {
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
+
+ foreach (var person in seriesInfo.Credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ seriesResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
+ }
+ }
+ }
+
+ internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
+ {
+ var dataPath = GetSeriesDataPath(appPaths);
+
+ return Path.Combine(dataPath, tmdbId);
+ }
+
+ internal static string GetSeriesDataPath(IApplicationPaths appPaths)
+ {
+ var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv");
+
+ return dataPath;
+ }
+
+ internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ {
+ SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (mainResult == null)
+ {
+ return;
+ }
+
+ var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+
+ _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ }
+
+ internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
+ {
+ var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language)
+ + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SeriesResult mainResult;
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ mainResult.ResultLanguage = language;
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // If the language preference isn't english, then have the overview fallback to english if it's blank
+ if (mainResult != null &&
+ string.IsNullOrEmpty(mainResult.Overview) &&
+ !string.IsNullOrEmpty(language) &&
+ !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
+
+ url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+
+ if (!string.IsNullOrEmpty(language))
+ {
+ // Get images in english and with no language
+ url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+ }
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false);
+
+ mainResult.Overview = englishResult.Overview;
+ mainResult.ResultLanguage = "en";
+ }
+ }
+ }
+
+ return mainResult;
+ }
+
+ internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ var path = GetDataFilePath(tmdbId, language);
+
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ // If it's recent or automatic updates are enabled, don't re-download
+ if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ return DownloadSeriesInfo(tmdbId, language, cancellationToken);
+ }
+
+ internal string GetDataFilePath(string tmdbId, string preferredLanguage)
+ {
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ throw new ArgumentNullException(nameof(tmdbId));
+ }
+
+ var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
+
+ var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty);
+
+ return Path.Combine(path, filename);
+ }
+
+ private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
+ {
+ var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
+ id,
+ TmdbUtils.ApiKey,
+ externalSource);
+
+ using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+ {
+ Url = url,
+ CancellationToken = cancellationToken,
+ AcceptHeader = TmdbUtils.AcceptHeader
+ }).ConfigureAwait(false))
+ {
+ using (var json = response.Content)
+ {
+ var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(json).ConfigureAwait(false);
+
+ if (result != null && result.Tv_Results != null)
+ {
+ var tv = result.Tv_Results.FirstOrDefault();
+
+ if (tv != null)
+ {
+ var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = tv.Name,
+ SearchProviderName = Name,
+ ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) ? null : tmdbImageUrl + tv.Poster_Path
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
+
+ return remoteResult;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // After TheTVDB
+ public int Order => 1;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
new file mode 100644
index 000000000..2f1e8b791
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -0,0 +1,64 @@
+using System;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Utilities for the TMDb provider.
+ /// </summary>
+ public static class TmdbUtils
+ {
+ /// <summary>
+ /// URL of the TMDB instance to use.
+ /// </summary>
+ public const string BaseTmdbUrl = "https://www.themoviedb.org/";
+
+ /// <summary>
+ /// URL of the TMDB API instance to use.
+ /// </summary>
+ public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
+
+ /// <summary>
+ /// Name of the provider.
+ /// </summary>
+ public const string ProviderName = "TheMovieDb";
+
+ /// <summary>
+ /// API key to use when performing an API call.
+ /// </summary>
+ public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
+
+ /// <summary>
+ /// Value of the Accept header for requests to the provider.
+ /// </summary>
+ public const string AcceptHeader = "application/json,image/*";
+
+ /// <summary>
+ /// Maps the TMDB provided roles for crew members to Jellyfin roles.
+ /// </summary>
+ /// <param name="crew">Crew member to map against the Jellyfin person types.</param>
+ /// <returns>The Jellyfin person type.</returns>
+ public static string MapCrewToPersonType(Crew crew)
+ {
+ if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
+ && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return PersonType.Director;
+ }
+
+ if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
+ && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return PersonType.Producer;
+ }
+
+ if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return PersonType.Writer;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
new file mode 100644
index 000000000..ee5128db4
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
+{
+ public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo>
+ {
+ private readonly IHttpClient _httpClient;
+
+ public TmdbTrailerProvider(IHttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken);
+ }
+
+ public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
+ {
+ return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
+ }
+
+ public string Name => TmdbMovieProvider.Current.Name;
+
+ public int Order => 0;
+
+ public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClient.GetResponse(new HttpRequestOptions
+ {
+ CancellationToken = cancellationToken,
+ Url = url
+ });
+ }
+ }
+}