Merge pull request #2549 from dkanada/refactor
Refactor some provider classes
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
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
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
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)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
_configurationManager = configurationManager;
|
||||
_appHost = appHost;
|
||||
_itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Episode>()
|
||||
{
|
||||
Item = new Episode(),
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
// Allowing this will dramatically increase scan times
|
||||
if (info.IsMissingEpisode)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (info.SeriesProviderIds.TryGetValue(MetadataProviders.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);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// After TheTvDb
|
||||
public int Order => 1;
|
||||
|
||||
public string Name => "The Open Movie Database";
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _itemProvider.GetImageResponse(url, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
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)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
_configurationManager = configurationManager;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new List<ImageType>
|
||||
{
|
||||
ImageType.Primary
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var imdbId = item.GetProviderId(MetadataProviders.Imdb);
|
||||
|
||||
var list = new List<RemoteImageInfo>();
|
||||
|
||||
var provider = new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
var rootObject = await provider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(rootObject.Poster))
|
||||
{
|
||||
if (item is Episode)
|
||||
{
|
||||
// img.omdbapi.com is returning 404's
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Url = rootObject.Poster
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
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 Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>,
|
||||
IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IApplicationHost _appHost;
|
||||
|
||||
public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_fileSystem = fileSystem;
|
||||
_configurationManager = configurationManager;
|
||||
_appHost = appHost;
|
||||
}
|
||||
// After primary option
|
||||
public int Order => 2;
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetSearchResults(searchInfo, "series", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetSearchResults(searchInfo, "movie", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo searchInfo, string type, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetSearchResultsInternal(searchInfo, type, true, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, string type, bool isSearch, CancellationToken cancellationToken)
|
||||
{
|
||||
var episodeSearchInfo = searchInfo as EpisodeInfo;
|
||||
|
||||
var imdbId = searchInfo.GetProviderId(MetadataProviders.Imdb);
|
||||
|
||||
var urlQuery = "plot=full&r=json";
|
||||
if (type == "episode" && episodeSearchInfo != null)
|
||||
{
|
||||
episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out imdbId);
|
||||
}
|
||||
|
||||
var name = searchInfo.Name;
|
||||
var year = searchInfo.Year;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
var parsedName = _libraryManager.ParseName(name);
|
||||
var yearInName = parsedName.Year;
|
||||
name = parsedName.Name;
|
||||
year = year ?? yearInName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
if (year.HasValue)
|
||||
{
|
||||
urlQuery += "&y=" + year.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// &s means search and returns a list of results as opposed to t
|
||||
if (isSearch)
|
||||
{
|
||||
urlQuery += "&s=" + WebUtility.UrlEncode(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
urlQuery += "&t=" + WebUtility.UrlEncode(name);
|
||||
}
|
||||
urlQuery += "&type=" + type;
|
||||
}
|
||||
else
|
||||
{
|
||||
urlQuery += "&i=" + imdbId;
|
||||
isSearch = false;
|
||||
}
|
||||
|
||||
if (type == "episode")
|
||||
{
|
||||
if (searchInfo.IndexNumber.HasValue)
|
||||
{
|
||||
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);
|
||||
|
||||
using (var response = await OmdbProvider.GetOmdbResponse(_httpClient, url, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
using (var stream = response.Content)
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetMovieResult<Trailer>(info, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
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>
|
||||
{
|
||||
Item = new Series(),
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
var imdbId = info.GetProviderId(MetadataProviders.Imdb);
|
||||
if (string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
imdbId = await GetSeriesImdbId(info, cancellationToken).ConfigureAwait(false);
|
||||
result.QueriedById = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
|
||||
result.HasMetadata = true;
|
||||
|
||||
await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetMovieResult<Movie>(info, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<MetadataResult<T>> GetMovieResult<T>(ItemLookupInfo info, CancellationToken cancellationToken)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
var result = new MetadataResult<T>
|
||||
{
|
||||
Item = new T(),
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
var imdbId = info.GetProviderId(MetadataProviders.Imdb);
|
||||
if (string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
imdbId = await GetMovieImdbId(info, cancellationToken).ConfigureAwait(false);
|
||||
result.QueriedById = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
|
||||
result.HasMetadata = true;
|
||||
|
||||
await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<string> GetMovieImdbId(ItemLookupInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false);
|
||||
var first = results.FirstOrDefault();
|
||||
return first == null ? null : first.GetProviderId(MetadataProviders.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);
|
||||
}
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
private class SearchResultList
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the results.
|
||||
/// </summary>
|
||||
/// <value>The results.</value>
|
||||
public List<SearchResult> Search { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
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.Serialization;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
public class OmdbProvider
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly IApplicationHost _appHost;
|
||||
|
||||
public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
_configurationManager = configurationManager;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken)
|
||||
where T : BaseItem
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(imdbId));
|
||||
}
|
||||
|
||||
var item = itemResult.Item;
|
||||
|
||||
var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Only take the name and rating if the user's language is set to english, since Omdb has no localization
|
||||
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
|
||||
{
|
||||
item.Name = result.Title;
|
||||
|
||||
if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.OfficialRating = result.Rated;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
|
||||
&& int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
|
||||
&& year >= 0)
|
||||
{
|
||||
item.ProductionYear = year;
|
||||
}
|
||||
|
||||
var tomatoScore = result.GetRottenTomatoScore();
|
||||
|
||||
if (tomatoScore.HasValue)
|
||||
{
|
||||
item.CriticRating = tomatoScore;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.imdbVotes)
|
||||
&& int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
|
||||
&& voteCount >= 0)
|
||||
{
|
||||
//item.VoteCount = voteCount;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.imdbRating)
|
||||
&& float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
|
||||
&& imdbRating >= 0)
|
||||
{
|
||||
item.CommunityRating = imdbRating;
|
||||
}
|
||||
|
||||
//if (!string.IsNullOrEmpty(result.Website))
|
||||
//{
|
||||
// item.HomePageUrl = result.Website;
|
||||
//}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.imdbID))
|
||||
{
|
||||
item.SetProviderId(MetadataProviders.Imdb, result.imdbID);
|
||||
}
|
||||
|
||||
ParseAdditionalMetadata(itemResult, result);
|
||||
}
|
||||
|
||||
public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, string episodeImdbId, string seriesImdbId, string language, string country, CancellationToken cancellationToken)
|
||||
where T : BaseItem
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(seriesImdbId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(seriesImdbId));
|
||||
}
|
||||
|
||||
var item = itemResult.Item;
|
||||
|
||||
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (seasonResult == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RootObject result = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(episodeImdbId))
|
||||
{
|
||||
foreach (var episode in (seasonResult.Episodes ?? new RootObject[] { }))
|
||||
{
|
||||
if (string.Equals(episodeImdbId, episode.imdbID, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = episode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finally, search by numbers
|
||||
if (result == null)
|
||||
{
|
||||
foreach (var episode in (seasonResult.Episodes ?? new RootObject[] { }))
|
||||
{
|
||||
if (episode.Episode == episodeNumber)
|
||||
{
|
||||
result = episode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only take the name and rating if the user's language is set to english, since Omdb has no localization
|
||||
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
|
||||
{
|
||||
item.Name = result.Title;
|
||||
|
||||
if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.OfficialRating = result.Rated;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
|
||||
&& int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
|
||||
&& year >= 0)
|
||||
{
|
||||
item.ProductionYear = year;
|
||||
}
|
||||
|
||||
var tomatoScore = result.GetRottenTomatoScore();
|
||||
|
||||
if (tomatoScore.HasValue)
|
||||
{
|
||||
item.CriticRating = tomatoScore;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.imdbVotes)
|
||||
&& int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
|
||||
&& voteCount >= 0)
|
||||
{
|
||||
//item.VoteCount = voteCount;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.imdbRating)
|
||||
&& float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
|
||||
&& imdbRating >= 0)
|
||||
{
|
||||
item.CommunityRating = imdbRating;
|
||||
}
|
||||
|
||||
//if (!string.IsNullOrEmpty(result.Website))
|
||||
//{
|
||||
// item.HomePageUrl = result.Website;
|
||||
//}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.imdbID))
|
||||
{
|
||||
item.SetProviderId(MetadataProviders.Imdb, result.imdbID);
|
||||
}
|
||||
|
||||
ParseAdditionalMetadata(itemResult, result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string resultString;
|
||||
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
|
||||
{
|
||||
resultString = reader.ReadToEnd();
|
||||
resultString = resultString.Replace("\"N/A\"", "\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
var result = _jsonSerializer.DeserializeFromString<RootObject>(resultString);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string resultString;
|
||||
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
|
||||
{
|
||||
resultString = reader.ReadToEnd();
|
||||
resultString = resultString.Replace("\"N/A\"", "\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
var result = _jsonSerializer.DeserializeFromString<SeasonRootObject>(resultString);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
|
||||
{
|
||||
if (seriesProviderIds.TryGetValue(MetadataProviders.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))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string GetOmdbUrl(string query, IApplicationHost appHost, CancellationToken cancellationToken)
|
||||
{
|
||||
const string url = "https://www.omdbapi.com?apikey=2c9d9507";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
return url + "&" + query;
|
||||
}
|
||||
|
||||
private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imdbId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(imdbId));
|
||||
}
|
||||
|
||||
var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
|
||||
|
||||
var path = GetDataFilePath(imdbParam);
|
||||
|
||||
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 <= 1)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
var url = GetOmdbUrl(string.Format("i={0}&plot=short&tomatoes=true&r=json", imdbParam), _appHost, cancellationToken);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private async Task<string> EnsureSeasonInfo(string seriesImdbId, int seasonId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(seriesImdbId))
|
||||
{
|
||||
throw new ArgumentException("The series IMDb ID was null or whitespace.", nameof(seriesImdbId));
|
||||
}
|
||||
|
||||
var imdbParam = seriesImdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? seriesImdbId : "tt" + seriesImdbId;
|
||||
|
||||
var path = GetSeasonFilePath(imdbParam, seasonId);
|
||||
|
||||
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 <= 1)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
var url = GetOmdbUrl(string.Format("i={0}&season={1}&detail=full", imdbParam, seasonId), _appHost, cancellationToken);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static Task<HttpResponseInfo> GetOmdbResponse(IHttpClient httpClient, string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return httpClient.SendAsync(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = true,
|
||||
EnableDefaultUserAgent = true
|
||||
}, HttpMethod.Get);
|
||||
}
|
||||
|
||||
internal string GetDataFilePath(string imdbId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(imdbId));
|
||||
}
|
||||
|
||||
var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
|
||||
|
||||
var filename = string.Format("{0}.json", imdbId);
|
||||
|
||||
return Path.Combine(dataPath, filename);
|
||||
}
|
||||
|
||||
internal string GetSeasonFilePath(string imdbId, int seasonId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(imdbId));
|
||||
}
|
||||
|
||||
var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
|
||||
|
||||
var filename = string.Format("{0}_season_{1}.json", imdbId, seasonId);
|
||||
|
||||
return Path.Combine(dataPath, filename);
|
||||
}
|
||||
|
||||
private void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result)
|
||||
where T : BaseItem
|
||||
{
|
||||
var item = itemResult.Item;
|
||||
|
||||
var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
|
||||
|
||||
// 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))
|
||||
{
|
||||
item.Genres = Array.Empty<string>();
|
||||
|
||||
foreach (var genre in result.Genre
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i)))
|
||||
{
|
||||
item.AddGenre(genre);
|
||||
}
|
||||
}
|
||||
|
||||
if (isConfiguredForEnglish)
|
||||
{
|
||||
// Omdb is currently english only, so for other languages skip this and let secondary providers fill it in
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
private bool IsConfiguredForEnglish(BaseItem item)
|
||||
{
|
||||
var lang = item.GetPreferredMetadataLanguage();
|
||||
|
||||
// The data isn't localized and so can only be used for english users
|
||||
return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (Ratings != null)
|
||||
{
|
||||
var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison.OrdinalIgnoreCase));
|
||||
if (rating != null && rating.Value != null)
|
||||
{
|
||||
var value = rating.Value.TrimEnd('%');
|
||||
if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score))
|
||||
{
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public class OmdbRating
|
||||
{
|
||||
public string Source { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public class TvdbClientManager
|
||||
{
|
||||
private const string DefaultLanguage = "en";
|
||||
|
||||
private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1);
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly TvDbClient _tvDbClient;
|
||||
private DateTime _tokenCreatedAt;
|
||||
|
||||
public TvdbClientManager(IMemoryCache memoryCache)
|
||||
{
|
||||
_cache = memoryCache;
|
||||
_tvDbClient = new TvDbClient();
|
||||
}
|
||||
|
||||
private TvDbClient TvDbClient
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token))
|
||||
{
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
|
||||
_tokenCreatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
// Refresh if necessary
|
||||
if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20)))
|
||||
{
|
||||
try
|
||||
{
|
||||
_tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_tokenCreatedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
return _tvDbClient;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByNameAsync(string name, string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("series", name, language);
|
||||
return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("series", tvdbId, language);
|
||||
return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetAsync(tvdbId, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("episode", episodeTvdbId, language);
|
||||
return TryGetValue(cacheKey, language,() => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Traverse all episode pages and join them together
|
||||
var episodes = new List<EpisodeRecord>();
|
||||
var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
episodes.AddRange(episodePage.Data);
|
||||
if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue)
|
||||
{
|
||||
return episodes;
|
||||
}
|
||||
|
||||
int next = episodePage.Links.Next.Value;
|
||||
int last = episodePage.Links.Last.Value;
|
||||
|
||||
for (var page = next; page <= last; ++page)
|
||||
{
|
||||
episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
episodes.AddRange(episodePage.Data);
|
||||
}
|
||||
|
||||
return episodes;
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
|
||||
string imdbId,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("series", imdbId, language);
|
||||
return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync(
|
||||
string zap2ItId,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("series", zap2ItId, language);
|
||||
return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken));
|
||||
}
|
||||
public Task<TvDbResponse<Actor[]>> GetActorsAsync(
|
||||
int tvdbId,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("actors", tvdbId, language);
|
||||
return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<Image[]>> GetImagesAsync(
|
||||
int tvdbId,
|
||||
ImagesQuery imageQuery,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("images", tvdbId, language, imageQuery);
|
||||
return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TryGetValue("languages", null, () => TvDbClient.Languages.GetAllAsync(cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync(
|
||||
int tvdbId,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language);
|
||||
return TryGetValue(cacheKey, language,
|
||||
() => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
|
||||
int tvdbId,
|
||||
int page,
|
||||
EpisodeQuery episodeQuery,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = GenerateKey(language, tvdbId, episodeQuery);
|
||||
|
||||
return TryGetValue(cacheKey, language,
|
||||
() => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken));
|
||||
}
|
||||
|
||||
public Task<string> GetEpisodeTvdbId(
|
||||
EpisodeInfo searchInfo,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(),
|
||||
out var seriesTvdbId);
|
||||
|
||||
var episodeQuery = new EpisodeQuery();
|
||||
|
||||
// Prefer SxE over premiere date as it is more robust
|
||||
if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue)
|
||||
{
|
||||
switch (searchInfo.SeriesDisplayOrder)
|
||||
{
|
||||
case "dvd":
|
||||
episodeQuery.DvdEpisode = searchInfo.IndexNumber.Value;
|
||||
episodeQuery.DvdSeason = searchInfo.ParentIndexNumber.Value;
|
||||
break;
|
||||
case "absolute":
|
||||
episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value;
|
||||
break;
|
||||
default:
|
||||
//aired order
|
||||
episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value;
|
||||
episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (searchInfo.PremiereDate.HasValue)
|
||||
{
|
||||
// tvdb expects yyyy-mm-dd format
|
||||
episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> GetEpisodeTvdbId(
|
||||
int seriesTvdbId,
|
||||
EpisodeQuery episodeQuery,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var episodePage =
|
||||
await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return episodePage.Data.FirstOrDefault()?.Id.ToString();
|
||||
}
|
||||
|
||||
public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
|
||||
int tvdbId,
|
||||
EpisodeQuery episodeQuery,
|
||||
string language,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out T cachedValue))
|
||||
{
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
await _cacheWriteLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_cache.TryGetValue(key, out 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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cacheWriteLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateKey(params object[] objects)
|
||||
{
|
||||
var key = string.Empty;
|
||||
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
var objType = obj.GetType();
|
||||
if (objType.IsPrimitive || objType == typeof(string))
|
||||
{
|
||||
key += obj + ";";
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (PropertyInfo propertyInfo in objType.GetProperties())
|
||||
{
|
||||
var currentValue = propertyInfo.GetValue(obj, null);
|
||||
if (currentValue == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
key += propertyInfo.Name + "=" + currentValue + ";";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public class TvdbEpisodeImageProvider : IRemoteImageProvider
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
public string Name => "TheTVDB";
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Episode;
|
||||
}
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new List<ImageType>
|
||||
{
|
||||
ImageType.Primary
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var episode = (Episode)item;
|
||||
var series = episode.Series;
|
||||
var imageResult = new List<RemoteImageInfo>();
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
|
||||
{
|
||||
// Process images
|
||||
try
|
||||
{
|
||||
var episodeInfo = new EpisodeInfo
|
||||
{
|
||||
IndexNumber = episode.IndexNumber.Value,
|
||||
ParentIndexNumber = episode.ParentIndexNumber.Value,
|
||||
SeriesProviderIds = series.ProviderIds,
|
||||
SeriesDisplayOrder = series.DisplayOrder
|
||||
};
|
||||
string episodeTvdbId = await _tvdbClientManager
|
||||
.GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(episodeTvdbId))
|
||||
{
|
||||
_logger.LogError(
|
||||
"Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
|
||||
episodeInfo.ParentIndexNumber,
|
||||
episodeInfo.IndexNumber,
|
||||
series.GetProviderId(MetadataProviders.Tvdb));
|
||||
return imageResult;
|
||||
}
|
||||
|
||||
var episodeResult =
|
||||
await _tvdbClientManager
|
||||
.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var image = GetImageInfo(episodeResult.Data);
|
||||
if (image != null)
|
||||
{
|
||||
imageResult.Add(image);
|
||||
}
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProviders.Tvdb));
|
||||
}
|
||||
}
|
||||
|
||||
return imageResult;
|
||||
}
|
||||
|
||||
private RemoteImageInfo GetImageInfo(EpisodeRecord episode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(episode.Filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RemoteImageInfo
|
||||
{
|
||||
Width = Convert.ToInt32(episode.ThumbWidth),
|
||||
Height = Convert.ToInt32(episode.ThumbHeight),
|
||||
ProviderName = Name,
|
||||
Url = TvdbUtils.BannerUrl + episode.Filename,
|
||||
Type = ImageType.Primary
|
||||
};
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Class RemoteEpisodeProvider
|
||||
/// </summary>
|
||||
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<RemoteSearchResult>();
|
||||
|
||||
// Either an episode number or date must be provided; and the dictionary of provider ids must be valid
|
||||
if ((searchInfo.IndexNumber == null && searchInfo.PremiereDate == null)
|
||||
|| !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!metadataResult.HasMetadata)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
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 string Name => "TheTVDB";
|
||||
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Episode>
|
||||
{
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) &&
|
||||
(searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue))
|
||||
{
|
||||
result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Episode>
|
||||
{
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
string seriesTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb);
|
||||
string episodeTvdbId = null;
|
||||
try
|
||||
{
|
||||
episodeTvdbId = await _tvdbClientManager
|
||||
.GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(episodeTvdbId))
|
||||
{
|
||||
_logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
|
||||
searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
|
||||
return result;
|
||||
}
|
||||
|
||||
var episodeResult = await _tvdbClientManager.GetEpisodesAsync(
|
||||
Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
result = MapEpisodeToResult(searchInfo, episodeResult.Data);
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode)
|
||||
{
|
||||
var result = new MetadataResult<Episode>
|
||||
{
|
||||
HasMetadata = true,
|
||||
Item = new Episode
|
||||
{
|
||||
IndexNumber = id.IndexNumber,
|
||||
ParentIndexNumber = id.ParentIndexNumber,
|
||||
IndexNumberEnd = id.IndexNumberEnd,
|
||||
AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode,
|
||||
AirsAfterSeasonNumber = episode.AirsAfterSeason,
|
||||
AirsBeforeSeasonNumber = episode.AirsBeforeSeason,
|
||||
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);
|
||||
|
||||
if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber);
|
||||
item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason;
|
||||
}
|
||||
else if (episode.AiredEpisodeNumber.HasValue)
|
||||
{
|
||||
item.IndexNumber = episode.AiredEpisodeNumber;
|
||||
}
|
||||
else if (episode.AiredSeason.HasValue)
|
||||
{
|
||||
item.ParentIndexNumber = episode.AiredSeason;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(episode.FirstAired, out var date))
|
||||
{
|
||||
// dates from tvdb are UTC but without offset or Z
|
||||
item.PremiereDate = date;
|
||||
item.ProductionYear = date.Year;
|
||||
}
|
||||
|
||||
foreach (var director in episode.Directors)
|
||||
{
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = director,
|
||||
Type = PersonType.Director
|
||||
});
|
||||
}
|
||||
|
||||
// GuestStars is a weird list of names and roles
|
||||
// Example:
|
||||
// 1: Some Actor (Role1
|
||||
// 2: Role2
|
||||
// 3: Role3)
|
||||
// 4: Another Actor (Role1
|
||||
// ...
|
||||
for (var i = 0; i < episode.GuestStars.Length; ++i)
|
||||
{
|
||||
var currentActor = episode.GuestStars[i];
|
||||
var roleStartIndex = currentActor.IndexOf('(');
|
||||
|
||||
if (roleStartIndex == -1)
|
||||
{
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Type = PersonType.GuestStar,
|
||||
Name = currentActor,
|
||||
Role = string.Empty
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var roles = new List<string> {currentActor.Substring(roleStartIndex + 1)};
|
||||
|
||||
// Fetch all roles
|
||||
for (var j = i + 1; j < episode.GuestStars.Length; ++j)
|
||||
{
|
||||
var currentRole = episode.GuestStars[j];
|
||||
var roleEndIndex = currentRole.IndexOf(')');
|
||||
|
||||
if (roleEndIndex == -1)
|
||||
{
|
||||
roles.Add(currentRole);
|
||||
continue;
|
||||
}
|
||||
|
||||
roles.Add(currentRole.TrimEnd(')'));
|
||||
// Update the outer index (keep in mind it adds 1 after the iteration)
|
||||
i = j;
|
||||
break;
|
||||
}
|
||||
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Type = PersonType.GuestStar,
|
||||
Name = currentActor.Substring(0, roleStartIndex).Trim(),
|
||||
Role = string.Join(", ", roles)
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var writer in episode.Writers)
|
||||
{
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = writer,
|
||||
Type = PersonType.Writer
|
||||
});
|
||||
}
|
||||
|
||||
result.ResultLanguage = episode.Language.EpisodeName;
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "TheTVDB";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Order => 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item) => item is Person;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
yield return ImageType.Primary;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Series).Name },
|
||||
PersonIds = new[] { item.Id },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
}
|
||||
|
||||
}).Cast<Series>()
|
||||
.Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds))
|
||||
.ToList();
|
||||
|
||||
var infos = (await Task.WhenAll(seriesWithPerson.Select(async i =>
|
||||
await GetImageFromSeriesData(i, item.Name, cancellationToken).ConfigureAwait(false)))
|
||||
.ConfigureAwait(false))
|
||||
.Where(i => i != null)
|
||||
.Take(1);
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken)
|
||||
{
|
||||
var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.Tvdb));
|
||||
|
||||
try
|
||||
{
|
||||
var actorsResult = await _tvdbClientManager
|
||||
.GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var actor = actorsResult.Data.FirstOrDefault(a =>
|
||||
string.Equals(a.Name, personName, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrEmpty(a.Image));
|
||||
if (actor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RemoteImageInfo
|
||||
{
|
||||
Url = TvdbUtils.BannerUrl + actor.Image,
|
||||
Type = ImageType.Primary,
|
||||
ProviderName = Name
|
||||
};
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}", personName, tvdbId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
using RatingType = MediaBrowser.Model.Dto.RatingType;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
public string Name => ProviderName;
|
||||
|
||||
public static string ProviderName => "TheTVDB";
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Season;
|
||||
}
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new List<ImageType>
|
||||
{
|
||||
ImageType.Primary,
|
||||
ImageType.Banner,
|
||||
ImageType.Backdrop
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var season = (Season)item;
|
||||
var series = season.Series;
|
||||
|
||||
if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
|
||||
{
|
||||
return new RemoteImageInfo[] { };
|
||||
}
|
||||
|
||||
var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProviders.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 imageQuery = new ImagesQuery
|
||||
{
|
||||
KeyType = keyType,
|
||||
SubKey = seasonNumber.ToString()
|
||||
};
|
||||
try
|
||||
{
|
||||
var imageResults = await _tvdbClientManager
|
||||
.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false);
|
||||
remoteImages.AddRange(GetImages(imageResults.Data, language));
|
||||
}
|
||||
catch (TvDbServerException)
|
||||
{
|
||||
_logger.LogDebug("No images of type {KeyType} found for series {TvdbId}", keyType, tvdbId);
|
||||
}
|
||||
}
|
||||
|
||||
return remoteImages;
|
||||
}
|
||||
|
||||
private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
|
||||
{
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data;
|
||||
foreach (Image image in images)
|
||||
{
|
||||
var imageInfo = new RemoteImageInfo
|
||||
{
|
||||
RatingType = RatingType.Score,
|
||||
CommunityRating = (double?)image.RatingsInfo.Average,
|
||||
VoteCount = image.RatingsInfo.Count,
|
||||
Url = TvdbUtils.BannerUrl + image.FileName,
|
||||
ProviderName = ProviderName,
|
||||
Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
|
||||
ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
|
||||
};
|
||||
|
||||
var resolution = image.Resolution.Split('x');
|
||||
if (resolution.Length == 2)
|
||||
{
|
||||
imageInfo.Width = Convert.ToInt32(resolution[0]);
|
||||
imageInfo.Height = Convert.ToInt32(resolution[1]);
|
||||
}
|
||||
|
||||
imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
|
||||
list.Add(imageInfo);
|
||||
}
|
||||
var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return list.OrderByDescending(i =>
|
||||
{
|
||||
if (string.Equals(preferredLanguage, 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);
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
using RatingType = MediaBrowser.Model.Dto.RatingType;
|
||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
public string Name => ProviderName;
|
||||
|
||||
public static string ProviderName => "TheTVDB";
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Series;
|
||||
}
|
||||
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new List<ImageType>
|
||||
{
|
||||
ImageType.Primary,
|
||||
ImageType.Banner,
|
||||
ImageType.Backdrop
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TvdbSeriesProvider.IsValidSeries(item.ProviderIds))
|
||||
{
|
||||
return Array.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
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 imageQuery = new ImagesQuery
|
||||
{
|
||||
KeyType = keyType
|
||||
};
|
||||
try
|
||||
{
|
||||
var imageResults =
|
||||
await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
remoteImages.AddRange(GetImages(imageResults.Data, language));
|
||||
}
|
||||
catch (TvDbServerException)
|
||||
{
|
||||
_logger.LogDebug("No images of type {KeyType} exist for series {TvDbId}", keyType,
|
||||
tvdbId);
|
||||
}
|
||||
}
|
||||
return remoteImages;
|
||||
}
|
||||
|
||||
private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
|
||||
{
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data;
|
||||
|
||||
foreach (Image image in images)
|
||||
{
|
||||
var imageInfo = new RemoteImageInfo
|
||||
{
|
||||
RatingType = RatingType.Score,
|
||||
CommunityRating = (double?)image.RatingsInfo.Average,
|
||||
VoteCount = image.RatingsInfo.Count,
|
||||
Url = TvdbUtils.BannerUrl + image.FileName,
|
||||
ProviderName = Name,
|
||||
Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
|
||||
ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
|
||||
};
|
||||
|
||||
var resolution = image.Resolution.Split('x');
|
||||
if (resolution.Length == 2)
|
||||
{
|
||||
imageInfo.Width = Convert.ToInt32(resolution[0]);
|
||||
imageInfo.Height = Convert.ToInt32(resolution[1]);
|
||||
}
|
||||
|
||||
imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
|
||||
list.Add(imageInfo);
|
||||
}
|
||||
var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return list.OrderByDescending(i =>
|
||||
{
|
||||
if (string.Equals(preferredLanguage, 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);
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TvDbSharper;
|
||||
using TvDbSharper.Dto;
|
||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||
|
||||
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 ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly TvdbClientManager _tvdbClientManager;
|
||||
|
||||
public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_localizationManager = localizationManager;
|
||||
Current = this;
|
||||
_tvdbClientManager = tvdbClientManager;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsValidSeries(searchInfo.ProviderIds))
|
||||
{
|
||||
var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (metadata.HasMetadata)
|
||||
{
|
||||
return new List<RemoteSearchResult>
|
||||
{
|
||||
new RemoteSearchResult
|
||||
{
|
||||
Name = metadata.Item.Name,
|
||||
PremiereDate = metadata.Item.PremiereDate,
|
||||
ProductionYear = metadata.Item.ProductionYear,
|
||||
ProviderIds = metadata.Item.ProviderIds,
|
||||
SearchProviderName = Name
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Series>
|
||||
{
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
if (!IsValidSeries(itemId.ProviderIds))
|
||||
{
|
||||
result.QueriedById = false;
|
||||
await Identify(itemId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsValidSeries(itemId.ProviderIds))
|
||||
{
|
||||
result.Item = new Series();
|
||||
result.HasMetadata = true;
|
||||
|
||||
await FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
|
||||
{
|
||||
var series = result.Item;
|
||||
|
||||
if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId))
|
||||
{
|
||||
series.SetProviderId(MetadataProviders.Tvdb, tvdbId);
|
||||
}
|
||||
|
||||
if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
series.SetProviderId(MetadataProviders.Imdb, imdbId);
|
||||
tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProviders.Imdb.ToString(), metadataLanguage,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It))
|
||||
{
|
||||
series.SetProviderId(MetadataProviders.Zap2It, zap2It);
|
||||
tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProviders.Zap2It.ToString(), metadataLanguage,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var seriesResult =
|
||||
await _tvdbClientManager
|
||||
.GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
MapSeriesToResult(result, seriesResult.Data, metadataLanguage);
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve series with id {TvdbId}", tvdbId);
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
result.ResetPeople();
|
||||
|
||||
try
|
||||
{
|
||||
var actorsResult = await _tvdbClientManager
|
||||
.GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
MapActorsToResult(result, actorsResult.Data);
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve actors for series {TvdbId}", tvdbId);
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id);
|
||||
}
|
||||
|
||||
return result?.Data.First().Id.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a dictionary of provider IDs includes an entry for a valid TV metadata provider.
|
||||
/// </summary>
|
||||
/// <param name="seriesProviderIds">The dictionary to check.</param>
|
||||
/// <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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the series.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="year">The year.</param>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.String}.</returns>
|
||||
private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
var parsedName = _libraryManager.ParseName(name);
|
||||
var nameWithoutYear = parsedName.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results = await FindSeriesInternal(nameWithoutYear, language, 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 async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
|
||||
{
|
||||
var comparableName = GetComparableName(name);
|
||||
var list = new List<Tuple<List<string>, RemoteSearchResult>>();
|
||||
TvDbResponse<SeriesSearchResult[]> result;
|
||||
try
|
||||
{
|
||||
result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "No series results found for {Name}", comparableName);
|
||||
return new List<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
foreach (var seriesSearchResult in result.Data)
|
||||
{
|
||||
var tvdbTitles = new List<string>
|
||||
{
|
||||
GetComparableName(seriesSearchResult.SeriesName)
|
||||
};
|
||||
tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName));
|
||||
|
||||
DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired);
|
||||
var remoteSearchResult = new RemoteSearchResult
|
||||
{
|
||||
Name = tvdbTitles.FirstOrDefault(),
|
||||
ProductionYear = firstAired.Year,
|
||||
SearchProviderName = Name,
|
||||
ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
|
||||
|
||||
};
|
||||
try
|
||||
{
|
||||
var seriesSesult =
|
||||
await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSesult.Data.ImdbId);
|
||||
remoteSearchResult.SetProviderId(MetadataProviders.Zap2It, seriesSesult.Data.Zap2itId);
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id);
|
||||
}
|
||||
|
||||
remoteSearchResult.SetProviderId(MetadataProviders.Tvdb, seriesSearchResult.Id.ToString());
|
||||
list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
|
||||
}
|
||||
|
||||
return list
|
||||
.OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
|
||||
.ThenBy(i => list.IndexOf(i))
|
||||
.Select(i => i.Item2)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetComparableName(string name)
|
||||
{
|
||||
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+", " ");
|
||||
}
|
||||
|
||||
private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
|
||||
{
|
||||
Series series = result.Item;
|
||||
series.SetProviderId(MetadataProviders.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);
|
||||
if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus))
|
||||
{
|
||||
series.Status = seriesStatus;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(tvdbSeries.FirstAired, out var date))
|
||||
{
|
||||
// dates from tvdb are UTC but without offset or Z
|
||||
series.PremiereDate = date;
|
||||
series.ProductionYear = date.Year;
|
||||
}
|
||||
|
||||
series.RunTimeTicks = TimeSpan.FromMinutes(Convert.ToDouble(tvdbSeries.Runtime)).Ticks;
|
||||
foreach (var genre in tvdbSeries.Genre)
|
||||
{
|
||||
series.AddGenre(genre);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tvdbSeries.Network))
|
||||
{
|
||||
series.AddStudio(tvdbSeries.Network);
|
||||
}
|
||||
|
||||
if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended)
|
||||
{
|
||||
try
|
||||
{
|
||||
var episodeSummary = _tvdbClientManager
|
||||
.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data;
|
||||
var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max();
|
||||
var episodeQuery = new EpisodeQuery
|
||||
{
|
||||
AiredSeason = maxSeasonNumber
|
||||
};
|
||||
var episodesPage =
|
||||
_tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data;
|
||||
result.Item.EndDate = episodesPage.Select(e =>
|
||||
{
|
||||
DateTime.TryParse(e.FirstAired, out var firstAired);
|
||||
return firstAired;
|
||||
}).Max();
|
||||
}
|
||||
catch (TvDbServerException e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to find series end date for series {TvdbId}", tvdbSeries.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors)
|
||||
{
|
||||
foreach (Actor actor in actors)
|
||||
{
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Type = PersonType.Actor,
|
||||
Name = (actor.Name ?? string.Empty).Trim(),
|
||||
Role = actor.Role,
|
||||
ImageUrl = TvdbUtils.BannerUrl + actor.Image,
|
||||
SortOrder = actor.SortOrder
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(personInfo.Name))
|
||||
{
|
||||
result.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => "TheTVDB";
|
||||
|
||||
public async Task Identify(SeriesInfo info)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var entry = srch.FirstOrDefault();
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
var id = entry.GetProviderId(MetadataProviders.Tvdb);
|
||||
info.SetProviderId(MetadataProviders.Tvdb, id);
|
||||
}
|
||||
}
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
return _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url,
|
||||
BufferContent = false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.TheTvdb
|
||||
{
|
||||
public static class TvdbUtils
|
||||
{
|
||||
public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
|
||||
public const string TvdbBaseUrl = "https://www.thetvdb.com/";
|
||||
public const string BannerUrl = TvdbBaseUrl + "banners/";
|
||||
|
||||
public static ImageType GetImageTypeFromKeyType(string keyType)
|
||||
{
|
||||
switch (keyType.ToLowerInvariant())
|
||||
{
|
||||
case "poster":
|
||||
case "season": return ImageType.Primary;
|
||||
case "series":
|
||||
case "seasonwide": return ImageType.Banner;
|
||||
case "fanart": return ImageType.Backdrop;
|
||||
default: throw new ArgumentException($"Invalid or unknown keytype: {keyType}", nameof(keyType));
|
||||
}
|
||||
}
|
||||
|
||||
public static string NormalizeLanguage(string language)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// pt-br is just pt to tvdb
|
||||
return language.Split('-')[0].ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user