diff options
Diffstat (limited to 'MediaBrowser.Providers/Plugins')
97 files changed, 5022 insertions, 198 deletions
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index dee2d59f0..3c314acb3 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -45,7 +45,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (!string.IsNullOrWhiteSpace(id)) { diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index 1a0e87871..b1a54f22f 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -104,11 +104,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Genres = new[] { result.strGenre }; } - item.SetProviderId(MetadataProviders.AudioDbArtist, result.idArtist); - item.SetProviderId(MetadataProviders.AudioDbAlbum, result.idAlbum); + item.SetProviderId(MetadataProvider.AudioDbArtist, result.idArtist); + item.SetProviderId(MetadataProvider.AudioDbAlbum, result.idAlbum); - item.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, result.strMusicBrainzArtistID); - item.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, result.strMusicBrainzID); + item.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, result.strMusicBrainzArtistID); + item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, result.strMusicBrainzID); string overview = null; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index 18afd5dd5..04cdab66a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); + var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (!string.IsNullOrWhiteSpace(id)) { diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index df0f3df8f..d8a18a6bc 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -92,8 +92,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Genres = new[] { result.strGenre }; } - item.SetProviderId(MetadataProviders.AudioDbArtist, result.idArtist); - item.SetProviderId(MetadataProviders.MusicBrainzArtist, result.strMusicBrainzID); + item.SetProviderId(MetadataProvider.AudioDbArtist, result.idArtist); + item.SetProviderId(MetadataProvider.MusicBrainzArtist, result.strMusicBrainzID); string overview = null; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 34494644d..fbf413f2b 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -31,8 +31,8 @@ $('.configPage').on('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#enable').checked(config.Enable); - $('#replaceAlbumName').checked(config.ReplaceAlbumName); + $('#enable').checked = config.Enable; + $('#replaceAlbumName').checked = config.ReplaceAlbumName; Dashboard.hideLoadingMsg(); }); @@ -43,8 +43,8 @@ var form = this; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = $('#enable', form).checked(); - config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceAlbumName = $('#replaceAlbumName', form).checked; ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs index 2d8cb431c..478ea5190 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string Name => "TheAudioDb"; /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbAlbum.ToString(); + public string Key => MetadataProvider.AudioDbAlbum.ToString(); /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; @@ -25,7 +25,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string Name => "TheAudioDb Album"; /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbAlbum.ToString(); + public string Key => MetadataProvider.AudioDbAlbum.ToString(); /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; @@ -40,7 +40,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string Name => "TheAudioDb"; /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbArtist.ToString(); + public string Key => MetadataProvider.AudioDbArtist.ToString(); /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string Name => "TheAudioDb Artist"; /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbArtist.ToString(); + public string Key => MetadataProvider.AudioDbArtist.ToString(); /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 31cdaf616..0a2c7c124 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Music private readonly IHttpClient _httpClient; private readonly IApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger<MusicBrainzAlbumProvider> _logger; private readonly string _musicBrainzBaseUrl; @@ -163,17 +163,17 @@ namespace MediaBrowser.Providers.Music Name = i.Artists[0].Item1 }; - result.AlbumArtist.SetProviderId(MetadataProviders.MusicBrainzArtist, i.Artists[0].Item2); + result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); } if (!string.IsNullOrWhiteSpace(i.ReleaseId)) { - result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseId); + result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); } if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) { - result.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, i.ReleaseGroupId); + result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); } return result; @@ -247,12 +247,12 @@ namespace MediaBrowser.Providers.Music { if (!string.IsNullOrEmpty(releaseId)) { - result.Item.SetProviderId(MetadataProviders.MusicBrainzAlbum, releaseId); + result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); } if (!string.IsNullOrEmpty(releaseGroupId)) { - result.Item.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, releaseGroupId); + result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId); } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index 260a3b6e7..9d93dbdd1 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -216,7 +216,7 @@ namespace MediaBrowser.Providers.Music } } - result.SetProviderId(MetadataProviders.MusicBrainzArtist, artistId); + result.SetProviderId(MetadataProvider.MusicBrainzArtist, artistId); if (string.IsNullOrWhiteSpace(artistId) || string.IsNullOrWhiteSpace(result.Name)) { @@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Music if (singleResult != null) { - musicBrainzId = singleResult.GetProviderId(MetadataProviders.MusicBrainzArtist); + musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist); result.Item.Overview = singleResult.Overview; if (Plugin.Instance.Configuration.ReplaceArtistName) @@ -262,7 +262,7 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(musicBrainzId)) { result.HasMetadata = true; - result.Item.SetProviderId(MetadataProviders.MusicBrainzArtist, musicBrainzId); + result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId); } return result; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1f02461da..90196b046 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -41,8 +41,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { $('#server').val(config.Server).change(); $('#rateLimit').val(config.RateLimit).change(); - $('#enable').checked(config.Enable); - $('#replaceArtistName').checked(config.ReplaceArtistName); + $('#enable').checked = config.Enable; + $('#replaceArtistName').checked = config.ReplaceArtistName; Dashboard.hideLoadingMsg(); }); @@ -55,8 +55,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = $('#server', form).val(); config.RateLimit = $('#rateLimit', form).val(); - config.Enable = $('#enable', form).checked(); - config.ReplaceArtistName = $('#replaceArtistName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceArtistName = $('#replaceArtistName', form).checked; ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs index 03565a34c..3be6f570b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz Release Group"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString(); + public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; @@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz Album Artist"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz Album"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbum.ToString(); + public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; @@ -56,7 +56,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -72,7 +72,7 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -87,7 +87,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz Track"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzTrack.ToString(); + public string Key => MetadataProvider.MusicBrainzTrack.ToString(); /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs new file mode 100644 index 000000000..a9eecdd9e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs @@ -0,0 +1,9 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + public class PluginConfiguration : BasePluginConfiguration + { + public bool CastAndCrew { get; set; } + } +} diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html new file mode 100644 index 000000000..8b117ec8d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> +<head> + <title>OMDb</title> +</head> +<body> + <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox"> + <div data-role="content"> + <div class="content-primary"> + <form class="configForm"> + <label class="checkboxContainer"> + <input is="emby-checkbox" type="checkbox" id="castAndCrew" /> + <span>Collect information about the cast and other crew members from OMDb.</span> + </label> + <br /> + <div> + <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button> + </div> + </form> + </div> + </div> + <script type="text/javascript"> + var PluginConfig = { + pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8" + }; + + $('.configPage').on('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + $('#castAndCrew').checked = config.CastAndCrew; + Dashboard.hideLoadingMsg(); + }); + }); + + $('.configForm').on('submit', function (e) { + Dashboard.showLoadingMsg(); + + var form = this; + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.CastAndCrew = $('#castAndCrew', form).checked; + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + return false; + }); + </script> + </div> +</body> +</html> diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index 37160dd2c..b074a1b0a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { - public class OmdbEpisodeProvider : - IRemoteMetadataProvider<Episode, EpisodeInfo>, - IHasOrder + public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; @@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbEpisodeProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; - _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager); + _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager); } + // After TheTvDb + public int Order => 1; + + public string Name => "The Open Movie Database"; + public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); @@ -55,21 +63,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb return result; } - if (info.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out string seriesImdbId) && !string.IsNullOrEmpty(seriesImdbId)) + if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string seriesImdbId) && !string.IsNullOrEmpty(seriesImdbId)) { if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue) { result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager) - .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProviders.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } } return result; } - // After TheTvDb - public int Order => 1; - - public string Name => "The Open Movie Database"; public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index a450c2a6d..d78a37784 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { - var imdbId = item.GetProviderId(MetadataProviders.Imdb); + var imdbId = item.GetProviderId(MetadataProvider.Imdb); var list = new List<RemoteImageInfo>(); @@ -92,6 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { return item is Movie || item is Trailer || item is Episode; } + // After other internet providers, because they're better // But before fallback providers like screengrab public int Order => 90; diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 3aadda5d0..4a29ba4d0 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { @@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; - private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbItemProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; - _logger = logger; _libraryManager = libraryManager; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + // After primary option public int Order => 2; @@ -64,12 +68,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var episodeSearchInfo = searchInfo as EpisodeInfo; - var imdbId = searchInfo.GetProviderId(MetadataProviders.Imdb); + var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb); var urlQuery = "plot=full&r=json"; if (type == "episode" && episodeSearchInfo != null) { - episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out imdbId); + episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out imdbId); } var name = searchInfo.Name; @@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var parsedName = _libraryManager.ParseName(name); var yearInName = parsedName.Year; name = parsedName.Name; - year = year ?? yearInName; + year ??= yearInName; } if (string.IsNullOrWhiteSpace(imdbId)) @@ -99,6 +103,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { urlQuery += "&t=" + WebUtility.UrlEncode(name); } + urlQuery += "&type=" + type; } else @@ -113,6 +118,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { urlQuery += string.Format(CultureInfo.InvariantCulture, "&Episode={0}", searchInfo.IndexNumber); } + if (searchInfo.ParentIndexNumber.HasValue) { urlQuery += string.Format(CultureInfo.InvariantCulture, "&Season={0}", searchInfo.ParentIndexNumber); @@ -159,7 +165,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value; } - item.SetProviderId(MetadataProviders.Imdb, result.imdbID); + item.SetProviderId(MetadataProvider.Imdb, result.imdbID); if (result.Year.Length > 0 && int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) @@ -204,7 +210,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb QueriedById = true }; - var imdbId = info.GetProviderId(MetadataProviders.Imdb); + var imdbId = info.GetProviderId(MetadataProvider.Imdb); if (string.IsNullOrWhiteSpace(imdbId)) { imdbId = await GetSeriesImdbId(info, cancellationToken).ConfigureAwait(false); @@ -213,7 +219,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb if (!string.IsNullOrEmpty(imdbId)) { - result.Item.SetProviderId(MetadataProviders.Imdb, imdbId); + result.Item.SetProviderId(MetadataProvider.Imdb, imdbId); result.HasMetadata = true; await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); @@ -236,7 +242,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb QueriedById = true }; - var imdbId = info.GetProviderId(MetadataProviders.Imdb); + var imdbId = info.GetProviderId(MetadataProvider.Imdb); if (string.IsNullOrWhiteSpace(imdbId)) { imdbId = await GetMovieImdbId(info, cancellationToken).ConfigureAwait(false); @@ -245,7 +251,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb if (!string.IsNullOrEmpty(imdbId)) { - result.Item.SetProviderId(MetadataProviders.Imdb, imdbId); + result.Item.SetProviderId(MetadataProvider.Imdb, imdbId); result.HasMetadata = true; await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); @@ -258,14 +264,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false); var first = results.FirstOrDefault(); - return first == null ? null : first.GetProviderId(MetadataProviders.Imdb); + return first == null ? null : first.GetProviderId(MetadataProvider.Imdb); } private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken) { var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false); var first = results.FirstOrDefault(); - return first == null ? null : first.GetProviderId(MetadataProviders.Imdb); + return first == null ? null : first.GetProviderId(MetadataProvider.Imdb); } public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) @@ -312,6 +318,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb /// <value>The results.</value> public List<SearchResult> Search { get; set; } } - } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index fbdd293ed..19b4bd1e3 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -87,14 +87,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.CommunityRating = imdbRating; } - //if (!string.IsNullOrEmpty(result.Website)) - //{ - // item.HomePageUrl = result.Website; - //} + if (!string.IsNullOrEmpty(result.Website)) + { + item.HomePageUrl = result.Website; + } if (!string.IsNullOrWhiteSpace(result.imdbID)) { - item.SetProviderId(MetadataProviders.Imdb, result.imdbID); + item.SetProviderId(MetadataProvider.Imdb, result.imdbID); } ParseAdditionalMetadata(itemResult, result); @@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb if (!string.IsNullOrWhiteSpace(episodeImdbId)) { - foreach (var episode in (seasonResult.Episodes ?? new RootObject[] { })) + foreach (var episode in seasonResult.Episodes) { if (string.Equals(episodeImdbId, episode.imdbID, StringComparison.OrdinalIgnoreCase)) { @@ -134,7 +134,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb // finally, search by numbers if (result == null) { - foreach (var episode in (seasonResult.Episodes ?? new RootObject[] { })) + foreach (var episode in seasonResult.Episodes) { if (episode.Episode == episodeNumber) { @@ -188,14 +188,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.CommunityRating = imdbRating; } - //if (!string.IsNullOrEmpty(result.Website)) - //{ - // item.HomePageUrl = result.Website; - //} + if (!string.IsNullOrEmpty(result.Website)) + { + item.HomePageUrl = result.Website; + } if (!string.IsNullOrWhiteSpace(result.imdbID)) { - item.SetProviderId(MetadataProviders.Imdb, result.imdbID); + item.SetProviderId(MetadataProvider.Imdb, result.imdbID); } ParseAdditionalMetadata(itemResult, result); @@ -243,7 +243,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) { - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out string id) && !string.IsNullOrEmpty(id)) + if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id) && !string.IsNullOrEmpty(id)) { // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. if (!string.IsNullOrWhiteSpace(id)) @@ -263,6 +263,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { return url; } + return url + "&" + query; } @@ -386,7 +387,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport; - // Grab series genres because imdb data is better than tvdb. Leave movies alone + // Grab series genres because IMDb data is better than TVDB. Leave movies alone // But only do it if english is the preferred language because this data will not be localized if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre)) { @@ -407,45 +408,50 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.Overview = result.Plot; } - //if (!string.IsNullOrWhiteSpace(result.Director)) - //{ - // var person = new PersonInfo - // { - // Name = result.Director.Trim(), - // Type = PersonType.Director - // }; - - // itemResult.AddPerson(person); - //} - - //if (!string.IsNullOrWhiteSpace(result.Writer)) - //{ - // var person = new PersonInfo - // { - // Name = result.Director.Trim(), - // Type = PersonType.Writer - // }; - - // itemResult.AddPerson(person); - //} - - //if (!string.IsNullOrWhiteSpace(result.Actors)) - //{ - // var actorList = result.Actors.Split(','); - // foreach (var actor in actorList) - // { - // if (!string.IsNullOrWhiteSpace(actor)) - // { - // var person = new PersonInfo - // { - // Name = actor.Trim(), - // Type = PersonType.Actor - // }; - - // itemResult.AddPerson(person); - // } - // } - //} + if (!Plugin.Instance.Configuration.CastAndCrew) + { + return; + } + + if (!string.IsNullOrWhiteSpace(result.Director)) + { + var person = new PersonInfo + { + Name = result.Director.Trim(), + Type = PersonType.Director + }; + + itemResult.AddPerson(person); + } + + if (!string.IsNullOrWhiteSpace(result.Writer)) + { + var person = new PersonInfo + { + Name = result.Director.Trim(), + Type = PersonType.Writer + }; + + itemResult.AddPerson(person); + } + + if (!string.IsNullOrWhiteSpace(result.Actors)) + { + var actorList = result.Actors.Split(','); + foreach (var actor in actorList) + { + if (!string.IsNullOrWhiteSpace(actor)) + { + var person = new PersonInfo + { + Name = actor.Trim(), + Type = PersonType.Actor + }; + + itemResult.AddPerson(person); + } + } + } } private bool IsConfiguredForEnglish(BaseItem item) @@ -459,40 +465,70 @@ namespace MediaBrowser.Providers.Plugins.Omdb internal class SeasonRootObject { public string Title { get; set; } + public string seriesID { get; set; } + public int Season { get; set; } + public int? totalSeasons { get; set; } + public RootObject[] Episodes { get; set; } + public string Response { get; set; } } internal class RootObject { public string Title { get; set; } + public string Year { get; set; } + public string Rated { get; set; } + public string Released { get; set; } + public string Runtime { get; set; } + public string Genre { get; set; } + public string Director { get; set; } + public string Writer { get; set; } + public string Actors { get; set; } + public string Plot { get; set; } + public string Language { get; set; } + public string Country { get; set; } + public string Awards { get; set; } + public string Poster { get; set; } + public List<OmdbRating> Ratings { get; set; } + public string Metascore { get; set; } + public string imdbRating { get; set; } + public string imdbVotes { get; set; } + public string imdbID { get; set; } + public string Type { get; set; } + public string DVD { get; set; } + public string BoxOffice { get; set; } + public string Production { get; set; } + public string Website { get; set; } + public string Response { get; set; } + public int Episode { get; set; } public float? GetRottenTomatoScore() @@ -509,12 +545,15 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } } + return null; } } + public class OmdbRating { public string Source { get; set; } + public string Value { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs new file mode 100644 index 000000000..6ce2333e0 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.Omdb +{ + public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages + { + public static Plugin Instance { get; private set; } + + public override Guid Id => new Guid("a628c0da-fac5-4c7e-9d1a-7134223f14c8"); + + public override string Name => "OMDb"; + + public override string Description => "Get metadata for movies and other video content from OMDb."; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public IEnumerable<PluginPageInfo> GetPages() + { + yield return new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" + }; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs new file mode 100644 index 000000000..0a73634dc --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs @@ -0,0 +1,8 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.TheTvdb +{ + public class PluginConfiguration : BasePluginConfiguration + { + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs new file mode 100644 index 000000000..2e6f548ca --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs @@ -0,0 +1,24 @@ +using System; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.TheTvdb +{ + public class Plugin : BasePlugin<PluginConfiguration> + { + public static Plugin Instance { get; private set; } + + public override Guid Id => new Guid("a677c0da-fac5-4cde-941a-7134223f14c8"); + + public override string Name => "TheTVDB"; + + public override string Description => "Get metadata for movies and other video content from TheTVDB."; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index b73834155..38e887be1 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -120,6 +120,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var cacheKey = GenerateKey("series", zap2ItId, language); return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); } + public Task<TvDbResponse<Actor[]>> GetActorsAsync( int tvdbId, string language, @@ -172,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb string language, CancellationToken cancellationToken) { - searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), + searchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var seriesTvdbId); var episodeQuery = new EpisodeQuery(); @@ -190,7 +191,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value; break; default: - //aired order + // aired order episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value; episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value; break; diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index 6118a9c53..1a4c78538 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbEpisodeImageProvider : IRemoteImageProvider { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbEpisodeImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) @@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", episodeInfo.ParentIndexNumber, episodeInfo.IndexNumber, - series.GetProviderId(MetadataProviders.Tvdb)); + series.GetProviderId(MetadataProvider.Tvdb)); return imageResult; } @@ -85,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb } catch (TvDbServerException e) { - _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProviders.Tvdb)); + _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProvider.Tvdb)); } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 08c2a74d2..b4876c24e 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -14,14 +14,13 @@ using TvDbSharper.Dto; namespace MediaBrowser.Providers.Plugins.TheTvdb { - /// <summary> - /// Class RemoteEpisodeProvider + /// Class RemoteEpisodeProvider. /// </summary> public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbEpisodeProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) @@ -95,7 +94,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb QueriedById = true }; - string seriesTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); + string seriesTvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb); string episodeTvdbId = null; try { @@ -139,14 +138,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb Name = episode.EpisodeName, Overview = episode.Overview, CommunityRating = (float?)episode.SiteRating, - } }; result.ResetPeople(); var item = result.Item; - item.SetProviderId(MetadataProviders.Tvdb, episode.Id.ToString()); - item.SetProviderId(MetadataProviders.Imdb, episode.ImdbId); + item.SetProviderId(MetadataProvider.Tvdb, episode.Id.ToString()); + item.SetProviderId(MetadataProvider.Imdb, episode.ImdbId); if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index c1cdc90e9..9a44573ed 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbPersonImageProvider> _logger; private readonly ILibraryManager _libraryManager; private readonly TvdbClientManager _tvdbClientManager; @@ -57,7 +57,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { EnableImages = false } - }).Cast<Series>() .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds)) .ToList(); @@ -73,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken) { - var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.Tvdb)); + var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb)); try { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index a5d183df7..7bd815f76 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbSeasonImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) @@ -55,10 +55,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) { - return new RemoteImageInfo[] { }; + return Array.Empty<RemoteImageInfo>(); } - var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.Tvdb)); + var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb)); var seasonNumber = season.IndexNumber.Value; var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List<RemoteImageInfo>(); @@ -89,7 +89,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { var list = new List<RemoteImageInfo>(); - var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + // any languages with null ids are ignored + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data.Where(x => x.Id.HasValue); foreach (Image image in images) { var imageInfo = new RemoteImageInfo @@ -113,8 +114,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); list.Add(imageInfo); } - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => { if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 1bad60756..50f66945f 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbSeriesImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) @@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List<RemoteImageInfo>(); var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart }; - var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProviders.Tvdb)); + var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb)); foreach (KeyType keyType in keyTypes) { var imageQuery = new ImagesQuery @@ -79,6 +79,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb tvdbId); } } + return remoteImages; } @@ -110,8 +111,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); list.Add(imageInfo); } - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); return list.OrderByDescending(i => { if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index f6cd249f5..c9edcc8e9 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -22,8 +22,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { internal static TvdbSeriesProvider Current { get; private set; } + private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly ILogger<TvdbSeriesProvider> _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; private readonly TvdbClientManager _tvdbClientManager; @@ -94,22 +95,22 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { var series = result.Item; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId)) + if (seriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId)) { - series.SetProviderId(MetadataProviders.Tvdb, tvdbId); + series.SetProviderId(MetadataProvider.Tvdb, tvdbId); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId)) + if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId)) { - series.SetProviderId(MetadataProviders.Imdb, imdbId); - tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProviders.Imdb.ToString(), metadataLanguage, + series.SetProviderId(MetadataProvider.Imdb, imdbId); + tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProvider.Imdb.ToString(), metadataLanguage, cancellationToken).ConfigureAwait(false); } - if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It)) + if (seriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It)) { - series.SetProviderId(MetadataProviders.Zap2It, zap2It); - tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProviders.Zap2It.ToString(), metadataLanguage, + series.SetProviderId(MetadataProvider.Zap2It, zap2It); + tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProvider.Zap2It.ToString(), metadataLanguage, cancellationToken).ConfigureAwait(false); } @@ -145,12 +146,11 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken) { - TvDbResponse<SeriesSearchResult[]> result = null; try { - if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(idType, MetadataProvider.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) { result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) .ConfigureAwait(false); @@ -176,9 +176,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb /// <returns>True, if the dictionary contains a valid TV provider ID, otherwise false.</returns> internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) { - return seriesProviderIds.ContainsKey(MetadataProviders.Tvdb.ToString()) || - seriesProviderIds.ContainsKey(MetadataProviders.Imdb.ToString()) || - seriesProviderIds.ContainsKey(MetadataProviders.Zap2It.ToString()); + return seriesProviderIds.ContainsKey(MetadataProvider.Tvdb.ToString()) || + seriesProviderIds.ContainsKey(MetadataProvider.Imdb.ToString()) || + seriesProviderIds.ContainsKey(MetadataProvider.Zap2It.ToString()); } /// <summary> @@ -249,20 +249,21 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner }; + try { var seriesSesult = await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) .ConfigureAwait(false); - remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSesult.Data.ImdbId); - remoteSearchResult.SetProviderId(MetadataProviders.Zap2It, seriesSesult.Data.Zap2itId); + remoteSearchResult.SetProviderId(MetadataProvider.Imdb, seriesSesult.Data.ImdbId); + remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, seriesSesult.Data.Zap2itId); } catch (TvDbServerException e) { _logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id); } - remoteSearchResult.SetProviderId(MetadataProviders.Tvdb, seriesSearchResult.Id.ToString()); + remoteSearchResult.SetProviderId(MetadataProvider.Tvdb, seriesSearchResult.Id.ToString()); list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult)); } @@ -274,15 +275,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb } /// <summary> - /// The remove - /// </summary> - const string remove = "\"'!`?"; - /// <summary> - /// The spacers - /// </summary> - const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are two types of dashes, short and long) - - /// <summary> /// Gets the name of the comparable. /// </summary> /// <param name="name">The name.</param> @@ -291,47 +283,25 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { name = name.ToLowerInvariant(); name = name.Normalize(NormalizationForm.FormKD); - var sb = new StringBuilder(); - foreach (var c in name) - { - if (c >= 0x2B0 && c <= 0x0333) - { - // skip char modifier and diacritics - } - else if (remove.IndexOf(c) > -1) - { - // skip chars we are removing - } - else if (spacers.IndexOf(c) > -1) - { - sb.Append(" "); - } - else if (c == '&') - { - sb.Append(" and "); - } - else - { - sb.Append(c); - } - } - sb.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " "); - - return Regex.Replace(sb.ToString().Trim(), @"\s+", " "); + name = name.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " "); + name = name.Replace("&", " and " ); + name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc + name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " " + return name.Trim(); } private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) { Series series = result.Item; - series.SetProviderId(MetadataProviders.Tvdb, tvdbSeries.Id.ToString()); + series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString()); series.Name = tvdbSeries.SeriesName; series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim(); result.ResultLanguage = metadataLanguage; series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek); series.AirTime = tvdbSeries.AirsTime; series.CommunityRating = (float?)tvdbSeries.SiteRating; - series.SetProviderId(MetadataProviders.Imdb, tvdbSeries.ImdbId); - series.SetProviderId(MetadataProviders.Zap2It, tvdbSeries.Zap2itId); + series.SetProviderId(MetadataProvider.Imdb, tvdbSeries.ImdbId); + series.SetProviderId(MetadataProvider.Zap2It, tvdbSeries.Zap2itId); if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus)) { series.Status = seriesStatus; @@ -409,7 +379,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public async Task Identify(SeriesInfo info) { - if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb))) + if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProvider.Tvdb))) { return; } @@ -421,8 +391,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb if (entry != null) { - var id = entry.GetProviderId(MetadataProviders.Tvdb); - info.SetProviderId(MetadataProviders.Tvdb, id); + var id = entry.GetProviderId(MetadataProvider.Tvdb); + info.SetProviderId(MetadataProvider.Tvdb, id); } } 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 + }); + } + } +} |
