aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Plugins
diff options
context:
space:
mode:
authorcrobibero <cody@robibe.ro>2020-09-11 15:53:04 -0600
committercrobibero <cody@robibe.ro>2020-09-11 15:53:04 -0600
commitf13b87afa3e81e7fa2710caec58a7d6cb20f7635 (patch)
tree0f269ac5baa27b334c5377d9dec4010aedf25183 /MediaBrowser.Providers/Plugins
parent2363ad544979adf32207fa927f106fadb784f1fb (diff)
parent6bf0acb854683377bebad3ca27de17706519c420 (diff)
Merge remote-tracking branch 'upstream/master' into api-upload-subtitle
Diffstat (limited to 'MediaBrowser.Providers/Plugins')
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs71
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs88
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html42
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs66
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs42
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html62
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs45
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs (renamed from MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs)265
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html52
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs35
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs192
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs224
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Configuration/PluginConfiguration.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs72
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs30
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs40
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs22
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs29
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs122
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs32
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs162
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs275
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs80
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs78
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs71
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs310
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs211
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs39
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs399
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs297
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettings.cs27
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs34
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs138
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs274
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs133
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs214
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs151
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs143
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs232
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs189
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs560
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs65
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs43
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);
+ }
+ }
+}