diff options
Diffstat (limited to 'MediaBrowser.Providers/Plugins/TheTvdb')
8 files changed, 1562 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs new file mode 100644 index 000000000..b73834155 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -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; + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs new file mode 100644 index 000000000..6118a9c53 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -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 + }); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs new file mode 100644 index 000000000..08c2a74d2 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -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; + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs new file mode 100644 index 000000000..c1cdc90e9 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -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 + }); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs new file mode 100644 index 000000000..a5d183df7 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -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 + }); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs new file mode 100644 index 000000000..1bad60756 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -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 + }); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs new file mode 100644 index 000000000..f6cd249f5 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -0,0 +1,441 @@ +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; + } + + if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime)) + { + series.RunTimeTicks = TimeSpan.FromMinutes(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 + }); + } + } +} diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs new file mode 100644 index 000000000..79d879aa1 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs @@ -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(); + } + } +} |
