diff options
| author | crobibero <cody@robibe.ro> | 2020-09-11 15:53:04 -0600 |
|---|---|---|
| committer | crobibero <cody@robibe.ro> | 2020-09-11 15:53:04 -0600 |
| commit | f13b87afa3e81e7fa2710caec58a7d6cb20f7635 (patch) | |
| tree | 0f269ac5baa27b334c5377d9dec4010aedf25183 /MediaBrowser.Providers/Plugins | |
| parent | 2363ad544979adf32207fa927f106fadb784f1fb (diff) | |
| parent | 6bf0acb854683377bebad3ca27de17706519c420 (diff) | |
Merge remote-tracking branch 'upstream/master' into api-upload-subtitle
Diffstat (limited to 'MediaBrowser.Providers/Plugins')
106 files changed, 6107 insertions, 739 deletions
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index dee2d59f0f..e3a1decb88 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -1,4 +1,7 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -15,13 +18,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public class AudioDbAlbumImageProvider : IRemoteImageProvider, IHasOrder { private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _json; - public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IJsonSerializer json) + public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IJsonSerializer json) { _config = config; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _json = json; } @@ -45,7 +48,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)) { @@ -92,13 +95,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } /// <inheritdoc /> - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); + return httpClient.GetAsync(url, cancellationToken); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index 1a0e878719..e6d89e6880 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -24,16 +26,16 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _json; public static AudioDbAlbumProvider Current; - public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json) + public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json) { _config = config; _fileSystem = fileSystem; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _json = json; Current = this; @@ -104,11 +106,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; @@ -172,18 +174,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); - using (var httpResponse = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var response = httpResponse.Content) - using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) - { - await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); - } + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } private static string GetAlbumDataPath(IApplicationPaths appPaths, string musicBrainzReleaseGroupId) @@ -210,42 +204,79 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public class Album { public string idAlbum { get; set; } + public string idArtist { get; set; } + public string strAlbum { get; set; } + public string strArtist { get; set; } + public string intYearReleased { get; set; } + public string strGenre { get; set; } + public string strSubGenre { get; set; } + public string strReleaseFormat { get; set; } + public string intSales { get; set; } + public string strAlbumThumb { get; set; } + public string strAlbumCDart { get; set; } + public string strDescriptionEN { get; set; } + public string strDescriptionDE { get; set; } + public string strDescriptionFR { get; set; } + public string strDescriptionCN { get; set; } + public string strDescriptionIT { get; set; } + public string strDescriptionJP { get; set; } + public string strDescriptionRU { get; set; } + public string strDescriptionES { get; set; } + public string strDescriptionPT { get; set; } + public string strDescriptionSE { get; set; } + public string strDescriptionNL { get; set; } + public string strDescriptionHU { get; set; } + public string strDescriptionNO { get; set; } + public string strDescriptionIL { get; set; } + public string strDescriptionPL { get; set; } + public object intLoved { get; set; } + public object intScore { get; set; } + public string strReview { get; set; } + public object strMood { get; set; } + public object strTheme { get; set; } + public object strSpeed { get; set; } + public object strLocation { get; set; } + public string strMusicBrainzID { get; set; } + public string strMusicBrainzArtistID { get; set; } + public object strItunesID { get; set; } + public object strAmazonID { get; set; } + public string strLocked { get; set; } } @@ -255,7 +286,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } /// <inheritdoc /> - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index 18afd5dd5c..54851c4d07 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -1,4 +1,7 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -15,14 +18,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public class AudioDbArtistImageProvider : IRemoteImageProvider, IHasOrder { private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _json; - public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClient httpClient) + public AudioDbArtistImageProvider(IServerConfigurationManager config, IJsonSerializer json, IHttpClientFactory httpClientFactory) { _config = config; _json = json; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; } /// <inheritdoc /> @@ -47,7 +50,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)) { @@ -133,13 +136,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb return list; } - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); + return httpClient.GetAsync(url, cancellationToken); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index df0f3df8fa..72dad8a25a 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; @@ -21,25 +23,25 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IHasOrder { + private const string ApiKey = "195003"; + public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey; + private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _json; - public static AudioDbArtistProvider Current; - - private const string ApiKey = "195003"; - public const string BaseUrl = "https://www.theaudiodb.com/api/v1/json/" + ApiKey; - - public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json) + public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory, IJsonSerializer json) { _config = config; _fileSystem = fileSystem; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _json = json; Current = this; } + public static AudioDbArtistProvider Current { get; private set; } + /// <inheritdoc /> public string Name => "TheAudioDB"; @@ -85,15 +87,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb private void ProcessResult(MusicArtist item, Artist result, string preferredLanguage) { - //item.HomePageUrl = result.strWebsite; + // item.HomePageUrl = result.strWebsite; if (!string.IsNullOrEmpty(result.strGenre)) { 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; @@ -153,23 +155,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - using (var httpResponse = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - using (var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) - { - await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); - } - } + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } /// <summary> @@ -199,45 +191,85 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public class Artist { public string idArtist { get; set; } + public string strArtist { get; set; } + public string strArtistAlternate { get; set; } + public object idLabel { get; set; } + public string intFormedYear { get; set; } + public string intBornYear { get; set; } + public object intDiedYear { get; set; } + public object strDisbanded { get; set; } + public string strGenre { get; set; } + public string strSubGenre { get; set; } + public string strWebsite { get; set; } + public string strFacebook { get; set; } + public string strTwitter { get; set; } + public string strBiographyEN { get; set; } + public string strBiographyDE { get; set; } + public string strBiographyFR { get; set; } + public string strBiographyCN { get; set; } + public string strBiographyIT { get; set; } + public string strBiographyJP { get; set; } + public string strBiographyRU { get; set; } + public string strBiographyES { get; set; } + public string strBiographyPT { get; set; } + public string strBiographySE { get; set; } + public string strBiographyNL { get; set; } + public string strBiographyHU { get; set; } + public string strBiographyNO { get; set; } + public string strBiographyIL { get; set; } + public string strBiographyPL { get; set; } + public string strGender { get; set; } + public string intMembers { get; set; } + public string strCountry { get; set; } + public string strCountryCode { get; set; } + public string strArtistThumb { get; set; } + public string strArtistLogo { get; set; } + public string strArtistFanart { get; set; } + public string strArtistFanart2 { get; set; } + public string strArtistFanart3 { get; set; } + public string strArtistBanner { get; set; } + public string strMusicBrainzID { get; set; } + public object strLastFMChart { get; set; } + public string strLocked { get; set; } } @@ -247,7 +279,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } /// <inheritdoc /> - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs new file mode 100644 index 0000000000..138cfef19a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbAlbum.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs new file mode 100644 index 0000000000..8aceb48c0c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs new file mode 100644 index 0000000000..014481da24 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbOtherAlbumExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbAlbum.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs new file mode 100644 index 0000000000..7875391043 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.AudioDb +{ + public class AudioDbOtherArtistExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "TheAudioDb"; + + /// <inheritdoc /> + public string Key => MetadataProvider.AudioDbArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; + + /// <inheritdoc /> + public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs index ad3c7eb4bd..9657a290fc 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Model.Plugins; +#pragma warning disable CS1591 + +using MediaBrowser.Model.Plugins; namespace MediaBrowser.Providers.Plugins.AudioDb { diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 34494644d4..82f26a8f26 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -28,29 +28,31 @@ pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8" }; - $('.configPage').on('pageshow', function () { - Dashboard.showLoadingMsg(); - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#enable').checked(config.Enable); - $('#replaceAlbumName').checked(config.ReplaceAlbumName); - - Dashboard.hideLoadingMsg(); + document.querySelector('.configPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + document.querySelector('#enable').checked = config.Enable; + document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName; + + Dashboard.hideLoadingMsg(); + }); }); - }); - - $('.configForm').on('submit', function (e) { - Dashboard.showLoadingMsg(); - var form = this; - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = $('#enable', form).checked(); - config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); - - ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + document.querySelector('.configForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.Enable = document.querySelector('#enable').checked; + config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked; + + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; }); - - return false; - }); </script> </div> </body> diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs b/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs deleted file mode 100644 index 2d8cb431ca..0000000000 --- a/MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs +++ /dev/null @@ -1,66 +0,0 @@ -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Plugins.AudioDb -{ - public class AudioDbAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbAlbum.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicAlbum; - } - - public class AudioDbOtherAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "TheAudioDb Album"; - - /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbAlbum.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } - - public class AudioDbArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "TheAudioDb"; - - /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbArtist.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } - - public class AudioDbOtherArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string Name => "TheAudioDb Artist"; - - /// <inheritdoc /> - public string Key => MetadataProviders.AudioDbArtist.ToString(); - - /// <inheritdoc /> - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } -} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs index 8532c4df3a..b5bd72ff0d 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CS1591 + +using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; @@ -9,6 +11,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + public static Plugin Instance { get; private set; } public override Guid Id => new Guid("a629c0da-fac5-4c7e-931a-7174223f14c8"); @@ -17,11 +25,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public override string Description => "Get artist and album metadata or images from AudioDB."; - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml"; public IEnumerable<PluginPageInfo> GetPages() { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index 260a3b6e7b..781b716406 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -1,14 +1,16 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; @@ -35,21 +37,19 @@ namespace MediaBrowser.Providers.Music { var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture); - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - { - return GetResultsFromResponse(stream); - } + using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return GetResultsFromResponse(stream); } else { // They seem to throw bad request failures on any term with a slash var nameToSearch = searchInfo.Name.Replace('/', ' '); - var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); + var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) + await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var results = GetResultsFromResponse(stream).ToList(); @@ -62,15 +62,11 @@ namespace MediaBrowser.Providers.Music if (HasDiacritics(searchInfo.Name)) { // Try again using the search with accent characters url - url = string.Format("/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); + url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - { - using (var stream = response.Content) - { - return GetResultsFromResponse(stream); - } - } + using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return GetResultsFromResponse(stream); } } @@ -108,11 +104,13 @@ namespace MediaBrowser.Providers.Music reader.Read(); continue; } + using (var subReader = reader.ReadSubtree()) { return ParseArtistList(subReader).ToList(); } } + default: { reader.Skip(); @@ -150,6 +148,7 @@ namespace MediaBrowser.Providers.Music reader.Read(); continue; } + var mbzId = reader.GetAttribute("id"); using (var subReader = reader.ReadSubtree()) @@ -160,8 +159,10 @@ namespace MediaBrowser.Providers.Music yield return artist; } } + break; } + default: { reader.Skip(); @@ -202,6 +203,7 @@ namespace MediaBrowser.Providers.Music result.Overview = reader.ReadElementContentAsString(); break; } + default: { // there is sort-name if ever needed @@ -216,7 +218,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 +251,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 +264,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; @@ -290,7 +292,7 @@ namespace MediaBrowser.Providers.Music public string Name => "MusicBrainz"; - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 5843b0c7d9..980da9a010 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Model.Plugins; +#pragma warning disable CS1591 + +using MediaBrowser.Model.Plugins; namespace MediaBrowser.Providers.Plugins.MusicBrainz { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1f02461da2..1945e6cb49 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -36,33 +36,47 @@ uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a" }; - $('.musicBrainzConfigPage').on('pageshow', function () { - Dashboard.showLoadingMsg(); - 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); + document.querySelector('.musicBrainzConfigPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + var server = document.querySelector('#server'); + server.value = config.Server; + server.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: false + })); + + var rateLimit = document.querySelector('#rateLimit'); + rateLimit.value = config.RateLimit; + rateLimit.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: false + })); + + document.querySelector('#enable').checked = config.Enable; + document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName; - Dashboard.hideLoadingMsg(); + Dashboard.hideLoadingMsg(); + }); }); - }); - - $('.musicBrainzConfigForm').on('submit', function (e) { - Dashboard.showLoadingMsg(); - - var form = this; - 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(); - - ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + + document.querySelector('.musicBrainzConfigForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + config.Server = document.querySelector('#server').value; + config.RateLimit = document.querySelector('#rateLimit').value; + config.Enable = document.querySelector('#enable').checked; + config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked; + + ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; }); - - return false; - }); </script> </div> </body> diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs index 03565a34c4..5600c389c0 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Plugins.MusicBrainz; namespace MediaBrowser.Providers.Music @@ -8,10 +11,13 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzReleaseGroupExternalId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz Release Group"; + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString(); + public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; @@ -23,10 +29,13 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzAlbumArtistExternalId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz Album Artist"; + public string ProviderName => "MusicBrainz"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -38,10 +47,13 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzAlbumExternalId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz Album"; + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzAlbum.ToString(); + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; @@ -53,10 +65,13 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzArtistExternalId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz"; + public string ProviderName => "MusicBrainz"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -68,11 +83,14 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzOtherArtistExternalId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz Artist"; + public string ProviderName => "MusicBrainz"; /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; @@ -84,10 +102,13 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzTrackId : IExternalId { /// <inheritdoc /> - public string Name => "MusicBrainz Track"; + public string ProviderName => "MusicBrainz"; + + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzTrack.ToString(); /// <inheritdoc /> - public string Key => MetadataProviders.MusicBrainzTrack.ToString(); + public ExternalIdMediaType? Type => ExternalIdMediaType.Track; /// <inheritdoc /> public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 31cdaf616a..46f8988f27 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Diagnostics; @@ -24,36 +26,34 @@ namespace MediaBrowser.Providers.Music public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder { /// <summary> - /// The Jellyfin user-agent is unrestricted but source IP must not exceed - /// one request per second, therefore we rate limit to avoid throttling. - /// Be prudent, use a value slightly above the minimun required. - /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting - /// </summary> - private readonly long _musicBrainzQueryIntervalMs; - - /// <summary> /// For each single MB lookup/search, this is the maximum number of /// attempts that shall be made whilst receiving a 503 Server /// Unavailable (indicating throttled) response. /// </summary> private const uint MusicBrainzQueryAttempts = 5u; - internal static MusicBrainzAlbumProvider Current; + /// <summary> + /// The Jellyfin user-agent is unrestricted but source IP must not exceed + /// one request per second, therefore we rate limit to avoid throttling. + /// Be prudent, use a value slightly above the minimun required. + /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting + /// </summary> + private readonly long _musicBrainzQueryIntervalMs; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger<MusicBrainzAlbumProvider> _logger; private readonly string _musicBrainzBaseUrl; private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); public MusicBrainzAlbumProvider( - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IApplicationHost appHost, ILogger<MusicBrainzAlbumProvider> logger) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _logger = logger; @@ -66,6 +66,8 @@ namespace MediaBrowser.Providers.Music Current = this; } + internal static MusicBrainzAlbumProvider Current { get; private set; } + /// <inheritdoc /> public string Name => "MusicBrainz"; @@ -109,7 +111,7 @@ namespace MediaBrowser.Providers.Music else { // I'm sure there is a better way but for now it resolves search for 12" Mixes - var queryName = searchInfo.Name.Replace("\"", string.Empty); + var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal); url = string.Format( CultureInfo.InvariantCulture, @@ -121,11 +123,9 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(url)) { - using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - { - return GetResultsFromResponse(stream); - } + using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return GetResultsFromResponse(stream); } return Enumerable.Empty<RemoteSearchResult>(); @@ -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); } } @@ -276,27 +276,25 @@ namespace MediaBrowser.Providers.Music private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) { - var url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}", + var url = string.Format( + CultureInfo.InvariantCulture, + "/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(albumName), artistId); - using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - using (var reader = XmlReader.Create(oReader, settings)) - { - return ReleaseResult.Parse(reader).FirstOrDefault(); - } - } + using var reader = XmlReader.Create(oReader, settings); + return ReleaseResult.Parse(reader).FirstOrDefault(); } private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) @@ -307,23 +305,19 @@ namespace MediaBrowser.Providers.Music WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(artistName)); - using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings() { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - using (var reader = XmlReader.Create(oReader, settings)) - { - return ReleaseResult.Parse(reader).FirstOrDefault(); - } - } + using var reader = XmlReader.Create(oReader, settings); + return ReleaseResult.Parse(reader).FirstOrDefault(); } private class ReleaseResult @@ -361,6 +355,7 @@ namespace MediaBrowser.Providers.Music return ParseReleaseList(subReader).ToList(); } } + default: { reader.Skip(); @@ -396,6 +391,7 @@ namespace MediaBrowser.Providers.Music reader.Read(); continue; } + var releaseId = reader.GetAttribute("id"); using (var subReader = reader.ReadSubtree()) @@ -406,8 +402,10 @@ namespace MediaBrowser.Providers.Music yield return release; } } + break; } + default: { reader.Skip(); @@ -453,6 +451,7 @@ namespace MediaBrowser.Providers.Music { result.Year = date.Year; } + break; } case "annotation": @@ -480,6 +479,7 @@ namespace MediaBrowser.Providers.Music break; } + default: { reader.Skip(); @@ -497,7 +497,7 @@ namespace MediaBrowser.Providers.Music } } - private static ValueTuple<string, string> ParseArtistCredit(XmlReader reader) + private static (string, string) ParseArtistCredit(XmlReader reader) { reader.MoveToContent(); reader.Read(); @@ -518,6 +518,7 @@ namespace MediaBrowser.Providers.Music return ParseArtistNameCredit(subReader); } } + default: { reader.Skip(); @@ -531,7 +532,7 @@ namespace MediaBrowser.Providers.Music } } - return new ValueTuple<string, string>(); + return default; } private static (string, string) ParseArtistNameCredit(XmlReader reader) @@ -556,6 +557,7 @@ namespace MediaBrowser.Providers.Music return ParseArtistArtistCredit(subReader, id); } } + default: { reader.Skip(); @@ -593,6 +595,7 @@ namespace MediaBrowser.Providers.Music name = reader.ReadElementContentAsString(); break; } + default: { reader.Skip(); @@ -613,30 +616,21 @@ namespace MediaBrowser.Providers.Music { var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); - using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; - - using (var reader = XmlReader.Create(oReader, settings)) - { - var result = ReleaseResult.Parse(reader).FirstOrDefault(); + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - if (result != null) - { - return result.ReleaseId; - } - } - } + using var reader = XmlReader.Create(oReader, settings); + var result = ReleaseResult.Parse(reader).FirstOrDefault(); - return null; + return result?.ReleaseId; } /// <summary> @@ -649,57 +643,57 @@ namespace MediaBrowser.Providers.Music { var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); - using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - using (var stream = response.Content) - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - using (var reader = XmlReader.Create(oReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + using (var reader = XmlReader.Create(oReader, settings)) + { + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) { - if (reader.NodeType == XmlNodeType.Element) + switch (reader.Name) { - switch (reader.Name) + case "release-group-list": { - case "release-group-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - using (var subReader = reader.ReadSubtree()) - { - return GetFirstReleaseGroupId(subReader); - } - } - default: - { - reader.Skip(); - break; - } + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + using (var subReader = reader.ReadSubtree()) + { + return GetFirstReleaseGroupId(subReader); + } + } + + default: + { + reader.Skip(); + break; } - } - else - { - reader.Read(); } } - - return null; + else + { + reader.Read(); + } } + + return null; } } @@ -719,6 +713,7 @@ namespace MediaBrowser.Providers.Music { return reader.GetAttribute("id"); } + default: { reader.Skip(); @@ -741,23 +736,19 @@ namespace MediaBrowser.Providers.Music /// A number of retries shall be made in order to try and satisfy the request before /// giving up and returning null. /// </summary> - internal async Task<HttpResponseInfo> GetMusicBrainzResponse(string url, CancellationToken cancellationToken) + internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken) { - var options = new HttpRequestOptions - { - Url = _musicBrainzBaseUrl.TrimEnd('/') + url, - CancellationToken = cancellationToken, - // MusicBrainz request a contact email address is supplied, as comment, in user agent field: - // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent - UserAgent = string.Format( - CultureInfo.InvariantCulture, - "{0} ( {1} )", - _appHost.ApplicationUserAgent, - _appHost.ApplicationUserAgentAddress), - BufferContent = false - }; + using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url); + + // MusicBrainz request a contact email address is supplied, as comment, in user agent field: + // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent + options.Headers.UserAgent.ParseAdd(string.Format( + CultureInfo.InvariantCulture, + "{0} ( {1} )", + _appHost.ApplicationUserAgent, + _appHost.ApplicationUserAgentAddress)); - HttpResponseInfo response; + HttpResponseMessage response; var attempts = 0u; do @@ -776,7 +767,7 @@ namespace MediaBrowser.Providers.Music _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _stopWatchMusicBrainz.Restart(); - response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); + response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false); // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) } @@ -785,14 +776,14 @@ namespace MediaBrowser.Providers.Music // Log error if unable to query MB database due to throttling if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.Url); + _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri); } return response; } /// <inheritdoc /> - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index 8e1b3ea376..90266e4409 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CS1591 + +using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; @@ -21,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz public const long DefaultRateLimit = 2000u; + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..196f14e7c8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..f4375b3cb5 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html @@ -0,0 +1,52 @@ +<!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" + }; + + document.querySelector('.configPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + document.querySelector('#castAndCrew').checked = config.CastAndCrew; + Dashboard.hideLoadingMsg(); + }); + }); + + + document.querySelector('.configForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.CastAndCrew = document.querySelector('#castAndCrew').checked; + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; + }); + </script> + </div> +</body> +</html> diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index 37160dd2c0..bfc840ea59 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -1,8 +1,10 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -11,31 +13,39 @@ 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; + private readonly IHttpClientFactory _httpClientFactory; private readonly OmdbItemProvider _itemProvider; private readonly IFileSystem _fileSystem; 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, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; - _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager); + _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClientFactory, 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,23 +65,19 @@ 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); + result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager) + .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) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _itemProvider.GetImageResponse(url, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index a450c2a6db..8f4240dc11 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -1,4 +1,8 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; +using System.Net.Http; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -17,21 +21,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbImageProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + public string Name => "The Open Movie Database"; + + // After other internet providers, because they're better + // But before fallback providers like screengrab + public int Order => 90; + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -42,11 +52,11 @@ 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>(); - var provider = new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager); + var provider = new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager); if (!string.IsNullOrWhiteSpace(imdbId)) { @@ -68,7 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb list.Add(new RemoteImageInfo { ProviderName = Name, - Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId) }); } } @@ -77,23 +87,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb return list; } - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - public string Name => "The Open Movie Database"; - public bool Supports(BaseItem item) { 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 3aadda5d07..705359d2c7 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -1,8 +1,11 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -17,7 +20,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 { @@ -25,23 +27,30 @@ namespace MediaBrowser.Providers.Plugins.Omdb IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder { private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; 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, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; - _httpClient = httpClient; - _logger = logger; + _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + + public string Name => "The Open Movie Database"; + // After primary option public int Order => 2; @@ -64,12 +73,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 +89,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 +108,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { urlQuery += "&t=" + WebUtility.UrlEncode(name); } + urlQuery += "&type=" + type; } else @@ -113,75 +123,72 @@ 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); } } - var url = OmdbProvider.GetOmdbUrl(urlQuery, _appHost, cancellationToken); + var url = OmdbProvider.GetOmdbUrl(urlQuery); - using (var response = await OmdbProvider.GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false)) + using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var resultList = new List<SearchResult>(); + + if (isSearch) { - using (var stream = response.Content) + var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false); + if (searchResultList != null && searchResultList.Search != null) { - var resultList = new List<SearchResult>(); - - if (isSearch) - { - var searchResultList = await _jsonSerializer.DeserializeFromStreamAsync<SearchResultList>(stream).ConfigureAwait(false); - if (searchResultList != null && searchResultList.Search != null) - { - resultList.AddRange(searchResultList.Search); - } - } - else - { - var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false); - if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase)) - { - resultList.Add(result); - } - } - - return resultList.Select(result => - { - var item = new RemoteSearchResult - { - IndexNumber = searchInfo.IndexNumber, - Name = result.Title, - ParentIndexNumber = searchInfo.ParentIndexNumber, - SearchProviderName = Name - }; - - if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue) - { - item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value; - } - - item.SetProviderId(MetadataProviders.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)) - { - item.ProductionYear = parsedYear; - } - - if (!string.IsNullOrEmpty(result.Released) - && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released)) - { - item.PremiereDate = released; - } - - if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase)) - { - item.ImageUrl = result.Poster; - } - - return item; - }); + resultList.AddRange(searchResultList.Search); } } + else + { + var result = await _jsonSerializer.DeserializeFromStreamAsync<SearchResult>(stream).ConfigureAwait(false); + if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase)) + { + resultList.Add(result); + } + } + + return resultList.Select(result => + { + var item = new RemoteSearchResult + { + IndexNumber = searchInfo.IndexNumber, + Name = result.Title, + ParentIndexNumber = searchInfo.ParentIndexNumber, + SearchProviderName = Name + }; + + if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue) + { + item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value; + } + + item.SetProviderId(MetadataProvider.Imdb, result.imdbID); + + if (result.Year.Length > 0 + && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) + { + item.ProductionYear = parsedYear; + } + + if (!string.IsNullOrEmpty(result.Released) + && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released)) + { + item.PremiereDate = released; + } + + if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase)) + { + item.ImageUrl = result.Poster; + } + + return item; + }); } public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken) @@ -194,8 +201,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb return GetSearchResults(searchInfo, "movie", cancellationToken); } - public string Name => "The Open Movie Database"; - public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<Series> @@ -204,7 +209,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,10 +218,10 @@ 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); + await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } return result; @@ -236,7 +241,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,10 +250,10 @@ 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); + await new OmdbProvider(_jsonSerializer, _httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); } return result; @@ -258,49 +263,67 @@ 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?.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?.GetProviderId(MetadataProvider.Imdb); } - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - class SearchResult + private class SearchResult { public string Title { get; set; } + public string Year { get; set; } + public string Rated { get; set; } + public string Released { get; set; } + public string Season { get; set; } + public string Episode { 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 string Metascore { get; set; } + public string imdbRating { get; set; } + public string imdbVotes { get; set; } + public string imdbID { get; set; } + public string seriesID { get; set; } + public string Type { get; set; } + public string Response { get; set; } } @@ -312,6 +335,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 fbdd293edf..32dab60a6b 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -23,14 +25,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IApplicationHost _appHost; - public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager) + public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; @@ -60,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) && year >= 0) { item.ProductionYear = year; @@ -77,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount) && voteCount >= 0) { - //item.VoteCount = voteCount; + // item.VoteCount = voteCount; } if (!string.IsNullOrEmpty(result.imdbRating) @@ -87,14 +89,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 +123,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 +136,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) { @@ -161,7 +163,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) && year >= 0) { item.ProductionYear = year; @@ -178,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount) && voteCount >= 0) { - //item.VoteCount = voteCount; + // item.VoteCount = voteCount; } if (!string.IsNullOrEmpty(result.imdbRating) @@ -188,14 +190,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 +245,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)) @@ -255,15 +257,16 @@ namespace MediaBrowser.Providers.Plugins.Omdb return false; } - public static string GetOmdbUrl(string query, IApplicationHost appHost, CancellationToken cancellationToken) + public static string GetOmdbUrl(string query) { - const string url = "https://www.omdbapi.com?apikey=2c9d9507"; + const string Url = "https://www.omdbapi.com?apikey=2c9d9507"; if (string.IsNullOrWhiteSpace(query)) { - return url; + return Url; } - return url + "&" + query; + + return Url + "&" + query; } private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken) @@ -288,17 +291,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - var url = GetOmdbUrl(string.Format("i={0}&plot=short&tomatoes=true&r=json", imdbParam), _appHost, cancellationToken); + var url = GetOmdbUrl( + string.Format( + CultureInfo.InvariantCulture, + "i={0}&plot=short&tomatoes=true&r=json", + imdbParam)); - using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(rootObject, path); - } - } + using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(stream).ConfigureAwait(false); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + _jsonSerializer.SerializeToFile(rootObject, path); return path; } @@ -325,30 +328,25 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - var url = GetOmdbUrl(string.Format("i={0}&season={1}&detail=full", imdbParam, seasonId), _appHost, cancellationToken); + var url = GetOmdbUrl( + string.Format( + CultureInfo.InvariantCulture, + "i={0}&season={1}&detail=full", + imdbParam, + seasonId)); - using (var response = await GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - _jsonSerializer.SerializeToFile(rootObject, path); - } - } + using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var rootObject = await _jsonSerializer.DeserializeFromStreamAsync<SeasonRootObject>(stream).ConfigureAwait(false); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + _jsonSerializer.SerializeToFile(rootObject, path); return path; } - public static Task<HttpResponseInfo> GetOmdbResponse(IHttpClient httpClient, string url, CancellationToken cancellationToken) + public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken) { - return httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true, - EnableDefaultUserAgent = true - }, HttpMethod.Get); + return httpClient.GetAsync(url, cancellationToken); } internal string GetDataFilePath(string imdbId) @@ -360,7 +358,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb"); - var filename = string.Format("{0}.json", imdbId); + var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId); return Path.Combine(dataPath, filename); } @@ -374,7 +372,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb"); - var filename = string.Format("{0}_season_{1}.json", imdbId, seasonId); + var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId); return Path.Combine(dataPath, filename); } @@ -386,7 +384,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 +405,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 +462,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 +542,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 0000000000..41ca561643 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -0,0 +1,40 @@ +#pragma warning disable CS1591 + +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."; + + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml"; + + 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 0000000000..690a52c4d3 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs @@ -0,0 +1,10 @@ +#pragma warning disable CS1591 + +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 0000000000..e7079ed3cd --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs @@ -0,0 +1,29 @@ +#pragma warning disable CS1591 + +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."; + + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml"; + + 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 b73834155c..f22d484abc 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -1,7 +1,11 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Providers; @@ -16,7 +20,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { private const string DefaultLanguage = "en"; - private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1); private readonly IMemoryCache _cache; private readonly TvDbClient _tvDbClient; private DateTime _tokenCreatedAt; @@ -120,6 +123,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 +176,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb string language, CancellationToken cancellationToken) { - searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), + searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb), out var seriesTvdbId); var episodeQuery = new EpisodeQuery(); @@ -190,7 +194,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; @@ -199,10 +203,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb else if (searchInfo.PremiereDate.HasValue) { // tvdb expects yyyy-mm-dd format - episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd"); + episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } - return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken); + return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken); } public async Task<string> GetEpisodeTvdbId( @@ -214,7 +218,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var episodePage = await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken) .ConfigureAwait(false); - return episodePage.Data.FirstOrDefault()?.Id.ToString(); + return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); } public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync( @@ -226,30 +230,56 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); } - private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) + public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) { - if (_cache.TryGetValue(key, out T cachedValue)) + var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); + var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); + + if (imagesSummary.Data.Fanart > 0) { - return cachedValue; + yield return KeyType.Fanart; } - await _cacheWriteLock.WaitAsync().ConfigureAwait(false); - try + if (imagesSummary.Data.Series > 0) { - if (_cache.TryGetValue(key, out cachedValue)) - { - return cachedValue; - } + yield return KeyType.Series; + } - _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; - var result = await resultFactory.Invoke().ConfigureAwait(false); - _cache.Set(key, result, TimeSpan.FromHours(1)); - return result; + if (imagesSummary.Data.Poster > 0) + { + yield return KeyType.Poster; } - finally + } + + public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); + var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); + + if (imagesSummary.Data.Season > 0) { - _cacheWriteLock.Release(); + yield return KeyType.Season; } + + if (imagesSummary.Data.Fanart > 0) + { + yield return KeyType.Fanart; + } + + // TODO seasonwide is not supported in TvDbSharper + } + + private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) + { + if (_cache.TryGetValue(key, out T cachedValue)) + { + return cachedValue; + } + + _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; + var result = await resultFactory.Invoke().ConfigureAwait(false); + _cache.Set(key, result, TimeSpan.FromHours(1)); + return result; } private static string GenerateKey(params object[] objects) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index 6118a9c53e..de2f6875f8 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -1,5 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Net.Http; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -16,13 +20,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbEpisodeImageProvider : IRemoteImageProvider { - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbEpisodeImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) + public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _tvdbClientManager = tvdbClientManager; } @@ -68,13 +72,13 @@ 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; } var episodeResult = await _tvdbClientManager - .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken) + .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken) .ConfigureAwait(false); var image = GetImageInfo(episodeResult.Data); @@ -85,7 +89,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)); } } @@ -101,8 +105,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return new RemoteImageInfo { - Width = Convert.ToInt32(episode.ThumbWidth), - Height = Convert.ToInt32(episode.ThumbHeight), + Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture), + Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture), ProviderName = Name, Url = TvdbUtils.BannerUrl + episode.Filename, Type = ImageType.Primary @@ -111,13 +115,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public int Order => 0; - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 08c2a74d2c..c088d8cec1 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -14,19 +17,18 @@ 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 IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbEpisodeProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) + public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _tvdbClientManager = tvdbClientManager; } @@ -95,7 +97,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,20 +141,26 @@ 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)) { item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber); item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason; } + else if (string.Equals(id.SeriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase)) + { + if (episode.AbsoluteNumber.GetValueOrDefault() != 0) + { + item.IndexNumber = episode.AbsoluteNumber; + } + } else if (episode.AiredEpisodeNumber.HasValue) { item.IndexNumber = episode.AiredEpisodeNumber; @@ -188,7 +196,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb for (var i = 0; i < episode.GuestStars.Length; ++i) { var currentActor = episode.GuestStars[i]; - var roleStartIndex = currentActor.IndexOf('('); + var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal); if (roleStartIndex == -1) { @@ -207,7 +215,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb for (var j = i + 1; j < episode.GuestStars.Length; ++j) { var currentRole = episode.GuestStars[j]; - var roleEndIndex = currentRole.IndexOf(')'); + var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal); if (roleEndIndex == -1) { @@ -242,13 +250,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return result; } - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } public int Order => 0; diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index c1cdc90e90..dc3c60dee0 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -18,15 +21,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbPersonImageProvider> _logger; private readonly ILibraryManager _libraryManager; private readonly TvdbClientManager _tvdbClientManager; - public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager) + public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager) { _libraryManager = libraryManager; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _tvdbClientManager = tvdbClientManager; } @@ -57,7 +60,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { EnableImages = false } - }).Cast<Series>() .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds)) .ToList(); @@ -73,7 +75,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 { @@ -103,13 +105,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb } /// <inheritdoc /> - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index a5d183df77..49576d488d 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -18,13 +21,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbSeasonImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) + public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _tvdbClientManager = tvdbClientManager; } @@ -55,16 +58,16 @@ 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>(); - var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart }; - foreach (var keyType in keyTypes) + var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false); + await foreach (var keyType in keyTypes) { var imageQuery = new ImagesQuery { @@ -89,7 +92,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 +117,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)) @@ -143,13 +147,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public int Order => 0; - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 1bad607565..d96840e51c 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -18,13 +21,13 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbSeriesImageProvider> _logger; private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) + public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _tvdbClientManager = tvdbClientManager; } @@ -57,9 +60,10 @@ 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)); - foreach (KeyType keyType in keyTypes) + var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb)); + var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken) + .ConfigureAwait(false); + await foreach (KeyType keyType in allowedKeyTypes) { var imageQuery = new ImagesQuery { @@ -79,6 +83,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb tvdbId); } } + return remoteImages; } @@ -110,8 +115,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)) @@ -140,13 +145,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public int Order => 0; - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index f6cd249f51..ca9b1d738f 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -22,15 +25,16 @@ 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 IHttpClientFactory _httpClientFactory; + private readonly ILogger<TvdbSeriesProvider> _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager) + public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _libraryManager = libraryManager; _localizationManager = localizationManager; @@ -94,22 +98,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 +149,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 +179,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> @@ -245,24 +248,29 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { Name = tvdbTitles.FirstOrDefault(), ProductionYear = firstAired.Year, - SearchProviderName = Name, - ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner - + SearchProviderName = Name }; + + if (!string.IsNullOrEmpty(seriesSearchResult.Banner)) + { + // Results from their Search endpoints already include the /banners/ part in the url, because reasons... + remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + 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 +282,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 +290,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; @@ -394,10 +371,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb Type = PersonType.Actor, Name = (actor.Name ?? string.Empty).Trim(), Role = actor.Role, - ImageUrl = TvdbUtils.BannerUrl + actor.Image, SortOrder = actor.SortOrder }; + if (!string.IsNullOrEmpty(actor.Image)) + { + personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image; + } + if (!string.IsNullOrWhiteSpace(personInfo.Name)) { result.AddPerson(personInfo); @@ -409,7 +390,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,21 +402,16 @@ 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); } } public int Order => 0; - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - BufferContent = false - }); + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs index 79d879aa1b..37a8d04a6f 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; @@ -7,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K"; public const string TvdbBaseUrl = "https://www.thetvdb.com/"; - public const string BannerUrl = TvdbBaseUrl + "banners/"; + public const string TvdbImageBaseUrl = "https://www.thetvdb.com"; + public const string BannerUrl = TvdbImageBaseUrl + "/banners/"; public static ImageType GetImageTypeFromKeyType(string keyType) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs new file mode 100644 index 0000000000..1f7ec64338 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -0,0 +1,32 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets +{ + /// <summary> + /// External ID for a TMDB box set. + /// </summary> + public class TmdbBoxSetExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.TmdbCollection.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet; + + /// <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 0000000000..f6592afe46 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -0,0 +1,162 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + + public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + 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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs new file mode 100644 index 0000000000..e7328b5535 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -0,0 +1,275 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +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 IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + + public TmdbBoxSetProvider( + ILogger<TmdbBoxSetProvider> logger, + IJsonSerializer json, + IServerConfigurationManager config, + IFileSystem fileSystem, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager) + { + _logger = logger; + _json = json; + _config = config; + _fileSystem = fileSystem; + _httpClientFactory = httpClientFactory; + _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(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); + + // Get images in english and with no language + url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + if (mainResult != null && string.IsNullOrEmpty(mainResult.Name)) + { + if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + { + url = string.Format(CultureInfo.InvariantCulture, 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 langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).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(CultureInfo.InvariantCulture, "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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} 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 0000000000..0a8994d540 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 + +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 0000000000..c6b851c237 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +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 0000000000..a48124b3e1 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 + +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 0000000000..5b7627f6e8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 + +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 0000000000..339ecb6285 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +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 0000000000..aac4420e8b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 + +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 0000000000..9ba1c15c65 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..0538cf174d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +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 0000000000..fff86931be --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..235ecb5682 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..4f61e978be --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs @@ -0,0 +1,21 @@ +#pragma warning disable CS1591 + +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 0000000000..0a1f8843eb --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 + +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 0000000000..61de819b93 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +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 0000000000..59ab18b7bf --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..ebd5c7acee --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +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 0000000000..241dcab4de --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..e8745be140 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +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 0000000000..937cfb8f6b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +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 0000000000..37547640f5 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 + +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 0000000000..edd656a461 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +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 0000000000..704ebcd5a1 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs @@ -0,0 +1,80 @@ +#pragma warning disable CS1591 + +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 long 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 long 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 0000000000..2788731b2e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..1b6f2cc67a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..276fbaaf55 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..67231d219d --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..166860f517 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..6885b7dab5 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +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 0000000000..3ea12334e8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS1591 + +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 0000000000..460ced49a0 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs @@ -0,0 +1,38 @@ +#pragma warning disable CS1591 + +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 0000000000..87c2a723d1 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..401c75c319 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs @@ -0,0 +1,78 @@ +#pragma warning disable CS1591 + +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 0000000000..4cff45ca64 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs @@ -0,0 +1,31 @@ +#pragma warning disable CS1591 + +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 0000000000..3b9257b623 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS1591 + +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 0000000000..b2bb068b58 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 + +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 0000000000..4ce26c65ee --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +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 0000000000..aef4e28632 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..ae1b5668d3 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..ba36632e04 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +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 0000000000..47205d8751 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 + +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 0000000000..53e3c26959 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +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 0000000000..9707e4bf49 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 + +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 0000000000..4458bad367 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs @@ -0,0 +1,38 @@ +#pragma warning disable CS1591 + +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 0000000000..8f39886418 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs @@ -0,0 +1,19 @@ +#pragma warning disable CS1591 + +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 0000000000..3dc310d330 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs @@ -0,0 +1,11 @@ +#pragma warning disable CS1591 + +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 0000000000..9cbd283a95 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 + +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 0000000000..f364d4921b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS1591 + +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 0000000000..e98048eacc --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS1591 + +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 0000000000..331cd59fa6 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs @@ -0,0 +1,71 @@ +#pragma warning disable CS1591 + +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 0000000000..01a887eed5 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs @@ -0,0 +1,310 @@ +#pragma warning disable CS1591 + +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(CultureInfo.InvariantCulture, "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 0000000000..a975fb8f6a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs @@ -0,0 +1,211 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + private readonly IFileSystem _fileSystem; + + public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) + { + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + _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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs new file mode 100644 index 0000000000..9610e40585 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -0,0 +1,39 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.Movies +{ + /// <summary> + /// External ID for a TMBD movie. + /// </summary> + public class TmdbMovieExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.Tmdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Movie; + + /// <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 0000000000..5d383722a0 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -0,0 +1,399 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +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.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 + { + 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"; + + internal static TmdbMovieProvider Current { get; private set; } + + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClientFactory _httpClientFactory; + 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, + IHttpClientFactory httpClientFactory, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILogger<TmdbMovieProvider> logger, + ILibraryManager libraryManager, + IApplicationHost appHost) + { + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + _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 var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey)); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false); + return _tmdbSettings; + } + + /// <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(CultureInfo.InvariantCulture, "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); + } + + 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(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language)); + + // Get images in english and with no language + url += "&include_image_language=" + GetImageLanguagesParam(language); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + if (mainResponse.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false); + + 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(CultureInfo.InvariantCulture, 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 langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false); + + await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false); + mainResult.Overview = langResult.Overview; + } + + return mainResult; + } + + /// <summary> + /// Gets the movie db response. + /// </summary> + internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default) + { + message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent); + return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken); + } + + /// <inheritdoc /> + public int Order => 1; + + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs new file mode 100644 index 0000000000..d885cd90b3 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs @@ -0,0 +1,297 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +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 const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; + private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}"; + private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}"; + + 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 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, StringComparison.Ordinal) && !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("String can't be null or empty.", nameof(name)); + } + + string url3; + if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase)) + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrlMovieWithYear, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + year); + } + else + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrl, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + type); + } + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).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("String can't be null or empty.", nameof(name)); + } + + string url3; + if (year == null) + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrl, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + "tv"); + } + else + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrlTvWithYear, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + year); + } + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).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 0000000000..128258ab36 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +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 0000000000..73e49ba5bf --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs @@ -0,0 +1,34 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +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<HttpResponseMessage> 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 0000000000..de74a7a4c7 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.People +{ + /// <summary> + /// External ID for a TMDB person. + /// </summary> + public class TmdbPersonExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.Tmdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Person; + + /// <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 0000000000..f2d2c8120e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -0,0 +1,138 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + + public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + { + _config = config; + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + } + + public static string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Name => ProviderName; + + /// <inheritdoc /> + public int Order => 0; + + 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 Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs new file mode 100644 index 0000000000..8d1a854d6c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -0,0 +1,274 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +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"; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _configurationManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger<TmdbPersonProvider> _logger; + + public TmdbPersonProvider( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IHttpClientFactory httpClientFactory, + ILogger<TmdbPersonProvider> logger) + { + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + _logger = logger; + Current = this; + } + + internal static TmdbPersonProvider Current { get; private set; } + + 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( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", + WebUtility.UrlEncode(searchInfo.Name), + TmdbUtils.ApiKey); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false) + ?? new TmdbSearchResult<PersonSearchResult>(); + + return result2.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; + } + + /// <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( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", + TmdbUtils.ApiKey, + id); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await response.Content.CopyToAsync(fs).ConfigureAwait(false); + } + + private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) + { + var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1); + + return Path.Join(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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs new file mode 100644 index 0000000000..eebecdac67 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -0,0 +1,133 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +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(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + : base(httpClientFactory, 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<HttpResponseMessage> 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 0000000000..ed4739acbf --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -0,0 +1,214 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +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(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + : base(httpClientFactory, 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 != null && response.External_Ids.Tvdb_Id > 0) + { + item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.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(CultureInfo.InvariantCulture, "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<HttpResponseMessage> 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 0000000000..55b0f0409e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -0,0 +1,151 @@ +#pragma warning disable CS1591 + +using System; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +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 IHttpClientFactory _httpClientFactory; + private readonly IServerConfigurationManager _configurationManager; + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + private readonly ILogger<TmdbEpisodeProviderBase> _logger; + + protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + { + _httpClientFactory = httpClientFactory; + _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(CultureInfo.InvariantCulture, "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( + CultureInfo.InvariantCulture, + urlPattern, + id, + seasonNumber.ToString(CultureInfo.InvariantCulture), + episodeNumber, + TmdbUtils.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format(CultureInfo.InvariantCulture, "&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 requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false); + } + + protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs new file mode 100644 index 0000000000..e7e2fd05b8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -0,0 +1,143 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + + public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory) + { + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + } + + public int Order => 1; + + public string Name => ProviderName; + + public static string ProviderName => TmdbUtils.ProviderName; + + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + + 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 0000000000..40f1c4e69f --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -0,0 +1,232 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +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 IHttpClientFactory _httpClientFactory; + 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(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogger<TmdbSeasonProvider> logger) + { + _httpClientFactory = httpClientFactory; + _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 != null && seasonInfo.External_Ids.Tvdb_Id > 0) + { + result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + + 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(CultureInfo.InvariantCulture, "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( + CultureInfo.InvariantCulture, + GetTvInfo3, + id, + seasonNumber.ToString(CultureInfo.InvariantCulture), + TmdbUtils.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format(CultureInfo.InvariantCulture, "&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 requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).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 0000000000..6ecc055d71 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.TV +{ + /// <summary> + /// External ID for a TMDB series. + /// </summary> + public class TmdbSeriesExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.Tmdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Series; + + /// <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 0000000000..125560175e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -0,0 +1,189 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + private readonly IFileSystem _fileSystem; + + public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) + { + _jsonSerializer = jsonSerializer; + _httpClientFactory = httpClientFactory; + _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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs new file mode 100644 index 0000000000..fa0873b9de --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -0,0 +1,560 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +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 IHttpClientFactory _httpClientFactory; + 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, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _logger = logger; + _localization = localization; + _httpClientFactory = httpClientFactory; + _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 != null && obj.External_Ids.Tvdb_Id > 0) + { + remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.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.Value.ToString(_usCulture)); + } + + if (ids.Tvdb_Id > 0) + { + series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.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(CultureInfo.InvariantCulture, 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(); + + using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage, cancellationToken).ConfigureAwait(false); + await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).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(CultureInfo.InvariantCulture, 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 requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).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(CultureInfo.InvariantCulture, "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( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}", + id, + TmdbUtils.ApiKey, + externalSource); + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in TmdbUtils.AcceptHeaders) + { + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); + } + + using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs new file mode 100644 index 0000000000..1415d69761 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -0,0 +1,65 @@ +using System; +using System.Net.Mime; +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 static readonly string[] AcceptHeaders = { MediaTypeNames.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 0000000000..25296387ba --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs @@ -0,0 +1,43 @@ +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.Net.Http; +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 IHttpClientFactory _httpClientFactory; + + public TmdbTrailerProvider(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + 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<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + } + } +} |
