aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Plugins/TheTvdb
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers/Plugins/TheTvdb')
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs284
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs123
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs256
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs115
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs155
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs152
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs441
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs36
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();
+ }
+ }
+}