diff options
Diffstat (limited to 'MediaBrowser.Providers/Plugins/Tmdb')
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 + }); + } + } +} |
