diff --git a/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs index 7225fac..d112aa0 100644 --- a/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Plugins; namespace Jellyfin.Plugin.Tvdb.Configuration { @@ -8,8 +8,16 @@ namespace Jellyfin.Plugin.Tvdb.Configuration public class PluginConfiguration : BasePluginConfiguration { /// - /// Gets or sets the tvdb api key. + /// Gets the tvdb api key for project. /// - public string ApiKey { get; set; } = "OG4V3YJ3FAP7FP2K"; + public const string ProjectApiKey = ""; + + /// + /// Gets or sets the tvdb api key for user. + /// + /// + /// This is the subscriber's pin. + /// + public string ApiKey { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/Jellyfin.Plugin.Tvdb/Configuration/config.html b/Jellyfin.Plugin.Tvdb/Configuration/config.html index 46d91b6..59696f8 100644 --- a/Jellyfin.Plugin.Tvdb/Configuration/config.html +++ b/Jellyfin.Plugin.Tvdb/Configuration/config.html @@ -1,4 +1,4 @@ - + TheTVDB @@ -18,7 +18,7 @@

TheTVDB Settings:

- TheTVDB Api Key + TheTVDB Api Key from Subscriptions.

diff --git a/Jellyfin.Plugin.Tvdb/Jellyfin.Plugin.Tvdb.csproj b/Jellyfin.Plugin.Tvdb/Jellyfin.Plugin.Tvdb.csproj index c84c689..c42de24 100644 --- a/Jellyfin.Plugin.Tvdb/Jellyfin.Plugin.Tvdb.csproj +++ b/Jellyfin.Plugin.Tvdb/Jellyfin.Plugin.Tvdb.csproj @@ -12,7 +12,11 @@ + + + + @@ -21,7 +25,7 @@ - + diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeImageProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeImageProvider.cs index 70e93bb..a7563f2 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeImageProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeImageProvider.cs @@ -11,8 +11,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; namespace Jellyfin.Plugin.Tvdb.Providers { @@ -95,13 +94,13 @@ await _tvdbClientManager .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken) .ConfigureAwait(false); - var image = GetImageInfo(episodeResult.Data); + var image = GetImageInfo(episodeResult); if (image != null) { imageResult.Add(image); } } - catch (TvDbServerException e) + catch (Exception e) { _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}:{Name}", series.GetProviderId(TvdbPlugin.ProviderId), series.Name); } @@ -116,19 +115,17 @@ public Task GetImageResponse(string url, CancellationToken return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken); } - private RemoteImageInfo? GetImageInfo(EpisodeRecord episode) + private RemoteImageInfo? GetImageInfo(EpisodeExtendedRecord episode) { - if (string.IsNullOrEmpty(episode.Filename)) + if (string.IsNullOrEmpty(episode.Image)) { return null; } return new RemoteImageInfo { - Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture), - Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture), ProviderName = Name, - Url = TvdbUtils.BannerUrl + episode.Filename, + Url = episode.Image, Type = ImageType.Primary }; } diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeProvider.cs index 0f8acf6..413afa4 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeProvider.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -12,8 +14,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; namespace Jellyfin.Plugin.Tvdb.Providers { @@ -124,12 +125,12 @@ private async Task> GetCombinedEpisode(EpisodeInfo info, results.Add(await GetEpisode(tempEpisodeInfo, cancellationToken).ConfigureAwait(false)); } - var result = CombineResults(info, results); + var result = CombineResults(results); return result; } - private MetadataResult CombineResults(EpisodeInfo id, List> results) + private MetadataResult CombineResults(List> results) { // Use first result as baseline var result = results[0]; @@ -158,7 +159,7 @@ private async Task> GetEpisode(EpisodeInfo searchInfo, C QueriedById = true }; - var seriesTvdbId = searchInfo.GetProviderId(TvdbPlugin.ProviderId); + var seriesTvdbId = searchInfo.SeriesProviderIds.FirstOrDefault(x => x.Key == TvdbPlugin.ProviderId).Value; string? episodeTvdbId = null; try { @@ -181,9 +182,9 @@ private async Task> GetEpisode(EpisodeInfo searchInfo, C searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - result = MapEpisodeToResult(searchInfo, episodeResult.Data); + result = MapEpisodeToResult(searchInfo, episodeResult); } - catch (TvDbServerException e) + catch (Exception e) { _logger.LogError( e, @@ -196,7 +197,7 @@ private async Task> GetEpisode(EpisodeInfo searchInfo, C return result; } - private static MetadataResult MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode) + private static MetadataResult MapEpisodeToResult(EpisodeInfo id, EpisodeExtendedRecord episode) { var result = new MetadataResult { @@ -209,116 +210,99 @@ private static MetadataResult MapEpisodeToResult(EpisodeInfo id, Episod AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode, AirsAfterSeasonNumber = episode.AirsAfterSeason, AirsBeforeSeasonNumber = episode.AirsBeforeSeason, - Name = episode.EpisodeName, - Overview = episode.Overview, - CommunityRating = (float?)episode.SiteRating, - OfficialRating = episode.ContentRating, + // Tvdb uses 3 letter code for language (prob ISO 639-2) + // Reverts to OriginalName if no translation is found + Name = episode.Translations.NameTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(id.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Name ?? episode.Name, + Overview = episode.Translations.OverviewTranslations?.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(id.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Overview ?? episode.Overview } }; result.ResetPeople(); var item = result.Item; item.SetProviderId(TvdbPlugin.ProviderId, episode.Id.ToString(CultureInfo.InvariantCulture)); - item.SetProviderId(MetadataProvider.Imdb, episode.ImdbId); + var imdbID = episode.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id; + if (!string.IsNullOrEmpty(imdbID)) + { + item.SetProviderId(MetadataProvider.Imdb, imdbID); + } if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase)) { - item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber, CultureInfo.InvariantCulture); - item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason; + var dvdInfo = episode.Seasons.FirstOrDefault(x => string.Equals(x.Type.Name, "dvd", StringComparison.OrdinalIgnoreCase)); + if (dvdInfo is null) + { + item.IndexNumber = episode.Number; + } + else + { + item.IndexNumber = Convert.ToInt32(dvdInfo.Number, CultureInfo.InvariantCulture); + } + + item.ParentIndexNumber = episode.SeasonNumber; } else if (string.Equals(id.SeriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase)) { - if (episode.AbsoluteNumber.GetValueOrDefault() != 0) + var absoluteInfo = episode.Seasons.FirstOrDefault(x => string.Equals(x.Type.Name, "absolute", StringComparison.OrdinalIgnoreCase)); + if (absoluteInfo is not null) { - item.IndexNumber = episode.AbsoluteNumber; + item.IndexNumber = Convert.ToInt32(absoluteInfo.Number, CultureInfo.InvariantCulture); } } - else if (episode.AiredEpisodeNumber.HasValue) - { - item.IndexNumber = episode.AiredEpisodeNumber; - } - else if (episode.AiredSeason.HasValue) + else { - item.ParentIndexNumber = episode.AiredSeason; + item.IndexNumber = episode.Number; + item.ParentIndexNumber = episode.SeasonNumber; } - if (DateTime.TryParse(episode.FirstAired, out var date)) + if (DateTime.TryParse(episode.Aired, 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) + if (episode.Characters is not null) { - var currentActor = episode.GuestStars[i]; - var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal); - - if (roleStartIndex == -1) + for (var i = 0; i < episode.Characters.Count; ++i) { - result.AddPerson(new PersonInfo + var currentActor = episode.Characters[i]; + if (string.Equals(currentActor.PeopleType, "Actor", StringComparison.OrdinalIgnoreCase)) { - Type = PersonType.GuestStar, - Name = currentActor, - Role = string.Empty - }); - continue; - } - - var roles = new List { 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.Contains(')', StringComparison.Ordinal); - - if (!roleEndIndex) + result.AddPerson(new PersonInfo + { + Type = PersonType.Actor, + Name = currentActor.PersonName, + Role = currentActor.Name + }); + } + else if (string.Equals(currentActor.PeopleType, "Director", StringComparison.OrdinalIgnoreCase)) { - roles.Add(currentRole); - continue; + result.AddPerson(new PersonInfo + { + Type = PersonType.Director, + Name = currentActor.PersonName + }); + } + else if (string.Equals(currentActor.PeopleType, "Writer", StringComparison.OrdinalIgnoreCase)) + { + result.AddPerson(new PersonInfo + { + Type = PersonType.Writer, + Name = currentActor.PersonName + }); + } + else if (string.Equals(currentActor.PeopleType, "Guest Star", StringComparison.OrdinalIgnoreCase)) + { + result.AddPerson(new PersonInfo + { + Type = PersonType.GuestStar, + Name = currentActor.PersonName, + Role = currentActor.Name + }); } - - 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; } diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbMissingEpisodeProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbMissingEpisodeProvider.cs index a430786..769eb4a 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbMissingEpisodeProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbMissingEpisodeProvider.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities.Libraries; using Jellyfin.Data.Events; using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.Dto; @@ -15,8 +16,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; +using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Jellyfin.Plugin.Tvdb.Providers @@ -94,14 +97,9 @@ protected virtual void Dispose(bool disposing) } } - private static bool IsValidEpisode(EpisodeRecord? episodeRecord) + private static bool EpisodeExists(EpisodeBaseRecord episodeRecord, IReadOnlyList existingEpisodes) { - return episodeRecord?.AiredSeason != null && episodeRecord.AiredEpisodeNumber != null; - } - - private static bool EpisodeExists(EpisodeRecord episodeRecord, IReadOnlyList existingEpisodes) - { - return existingEpisodes.Any(ep => ep.ContainsEpisodeNumber(episodeRecord.AiredEpisodeNumber!.Value) && ep.ParentIndexNumber == episodeRecord.AiredSeason); + return existingEpisodes.Any(ep => ep.ContainsEpisodeNumber(episodeRecord.Number) && ep.ParentIndexNumber == episodeRecord.Number); } private bool IsEnabledForLibrary(BaseItem item) @@ -178,8 +176,7 @@ private async Task HandleSeries(Series series) var allEpisodes = await GetAllEpisodes(tvdbId, series.GetPreferredMetadataLanguage()).ConfigureAwait(false); var allSeasons = allEpisodes - .Where(ep => ep.AiredSeason.HasValue) - .Select(ep => ep.AiredSeason!.Value) + .Select(ep => ep.SeasonNumber) .Distinct() .ToList(); @@ -197,12 +194,7 @@ private async Task HandleSeason(Season season) } var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture); - - var query = new EpisodeQuery - { - AiredSeason = season.IndexNumber - }; - var allEpisodes = await GetAllEpisodes(tvdbId, season.GetPreferredMetadataLanguage(), query).ConfigureAwait(false); + var allEpisodes = await GetAllEpisodes(tvdbId, season.GetPreferredMetadataLanguage()).ConfigureAwait(false); var existingEpisodes = season.Children.OfType().ToList(); @@ -292,14 +284,9 @@ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs ite var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture); - var query = new EpisodeQuery - { - AiredSeason = episode.ParentIndexNumber, - AiredEpisode = episode.IndexNumber - }; - var episodeRecords = GetAllEpisodes(tvdbId, episode.GetPreferredMetadataLanguage(), query).GetAwaiter().GetResult(); + var episodeRecords = GetAllEpisodes(tvdbId, episode.GetPreferredMetadataLanguage()).GetAwaiter().GetResult(); - EpisodeRecord? episodeRecord = null; + EpisodeBaseRecord? episodeRecord = null; if (episodeRecords.Count > 0) { episodeRecord = episodeRecords[0]; @@ -309,44 +296,25 @@ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs ite } } - private async Task> GetAllEpisodes(int tvdbId, string acceptedLanguage, EpisodeQuery? episodeQuery = null) + private async Task> GetAllEpisodes(int tvdbId, string acceptedLanguage) { try { // Fetch all episodes for the series - var allEpisodes = new List(); - var page = 1; - while (true) + var seriesInfo = await _tvdbClientManager.GetSeriesEpisodesAsync(tvdbId, acceptedLanguage, "default", CancellationToken.None).ConfigureAwait(false); + var allEpisodes = seriesInfo.Episodes; + if (allEpisodes is null || !allEpisodes.Any()) { - episodeQuery ??= new EpisodeQuery(); - var episodes = await _tvdbClientManager.GetEpisodesPageAsync( - tvdbId, - page, - episodeQuery, - acceptedLanguage, - CancellationToken.None).ConfigureAwait(false); - - if (episodes.Data == null) - { - _logger.LogWarning("Unable to get episodes from TVDB: Episode Query returned null for TVDB Id: {TvdbId}", tvdbId); - return Array.Empty(); - } - - allEpisodes.AddRange(episodes.Data); - if (!episodes.Links.Next.HasValue) - { - break; - } - - page = episodes.Links.Next.Value; + _logger.LogWarning("Unable to get episodes from TVDB: Episode Query returned null for TVDB Id: {TvdbId}", tvdbId); + return Array.Empty(); } return allEpisodes; } - catch (TvDbServerException ex) + catch (Exception ex) { _logger.LogWarning(ex, "Unable to get episodes from TVDB"); - return Array.Empty(); + return Array.Empty(); } } @@ -362,26 +330,21 @@ private IEnumerable AddMissingSeasons(Series series, List existi private void AddMissingEpisodes( Dictionary> existingEpisodes, - IReadOnlyList allEpisodeRecords, + IReadOnlyList allEpisodeRecords, IReadOnlyList existingSeasons) { for (var i = 0; i < allEpisodeRecords.Count; i++) { var episodeRecord = allEpisodeRecords[i]; - // tvdb has a lot of bad data? - if (!IsValidEpisode(episodeRecord)) - { - continue; - } // skip if it exists already - if (existingEpisodes.TryGetValue(episodeRecord.AiredSeason!.Value, out var episodes) + if (existingEpisodes.TryGetValue(episodeRecord.SeasonNumber, out var episodes) && EpisodeExists(episodeRecord, episodes)) { continue; } - var existingSeason = existingSeasons.First(season => season.IndexNumber.HasValue && season.IndexNumber.Value == episodeRecord.AiredSeason); + var existingSeason = existingSeasons.First(season => season.IndexNumber.HasValue && season.IndexNumber.Value == episodeRecord.SeasonNumber); AddVirtualEpisode(episodeRecord, existingSeason); } @@ -422,10 +385,9 @@ private Season AddVirtualSeason(int season, Series series) return newSeason; } - private void AddVirtualEpisode(EpisodeRecord? episode, Season? season) + private void AddVirtualEpisode(EpisodeBaseRecord? episode, Season? season) { - // tvdb has a lot of bad data? - if (!IsValidEpisode(episode) || season == null) + if (season == null) { return; } @@ -433,11 +395,11 @@ private void AddVirtualEpisode(EpisodeRecord? episode, Season? season) // Put as much metadata into it as possible var newEpisode = new Episode { - Name = episode!.EpisodeName, - IndexNumber = episode.AiredEpisodeNumber!.Value, - ParentIndexNumber = episode.AiredSeason!.Value, + Name = episode!.Name, + IndexNumber = episode.Number, + ParentIndexNumber = episode.SeasonNumber, Id = _libraryManager.GetNewItemId( - season.Series.Id + episode.AiredSeason.Value.ToString(CultureInfo.InvariantCulture) + "Episode " + episode.AiredEpisodeNumber, + season.Series.Id + episode.SeasonNumber.ToString(CultureInfo.InvariantCulture) + "Episode " + episode.Number, typeof(Episode)), IsVirtualItem = true, SeasonId = season.Id, @@ -446,14 +408,12 @@ private void AddVirtualEpisode(EpisodeRecord? episode, Season? season) AirsAfterSeasonNumber = episode.AirsAfterSeason, AirsBeforeSeasonNumber = episode.AirsBeforeSeason, Overview = episode.Overview, - CommunityRating = (float?)episode.SiteRating, - OfficialRating = episode.ContentRating, SeriesName = season.Series.Name, SeriesPresentationUniqueKey = season.SeriesPresentationUniqueKey, SeasonName = season.Name, DateLastSaved = DateTime.UtcNow }; - if (DateTime.TryParse(episode!.FirstAired, out var premiereDate)) + if (DateTime.TryParse(episode!.Aired, out var premiereDate)) { newEpisode.PremiereDate = premiereDate; } @@ -464,8 +424,8 @@ private void AddVirtualEpisode(EpisodeRecord? episode, Season? season) _logger.LogDebug( "Creating virtual episode {0} {1}x{2}", season.Series.Name, - episode.AiredSeason, - episode.AiredEpisodeNumber); + episode.SeasonNumber, + episode.Number); season.AddChild(newEpisode); } diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbPersonImageProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbPersonImageProvider.cs index f473b85..82b90a1 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbPersonImageProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbPersonImageProvider.cs @@ -15,7 +15,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; +using Tvdb.Sdk; namespace Jellyfin.Plugin.Tvdb.Providers { @@ -98,24 +98,26 @@ public Task GetImageResponse(string url, CancellationToken try { var actorsResult = await _tvdbClientManager - .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken) + .GetSeriesExtendedByIdAsync(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) + var character = actorsResult.Characters.FirstOrDefault(i => string.Equals(i.PersonName, personName, StringComparison.OrdinalIgnoreCase)); + + if (character == null) { return null; } + var actor = await _tvdbClientManager + .GetActorAsync(character.PeopleId, series.GetPreferredMetadataCountryCode(), cancellationToken) + .ConfigureAwait(false); return new RemoteImageInfo { - Url = TvdbUtils.BannerUrl + actor.Image, + Url = actor.Image, Type = ImageType.Primary, ProviderName = Name }; } - catch (TvDbServerException e) + catch (Exception e) { _logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}:{Name}", personName, tvdbId, series.Name); return null; diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs index 630952d..d5a86db 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs @@ -12,8 +12,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; using RatingType = MediaBrowser.Model.Dto.RatingType; namespace Jellyfin.Plugin.Tvdb.Providers @@ -72,45 +71,23 @@ public async Task> GetImages(BaseItem item, Cancell var seasonNumber = season.IndexNumber.Value; var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List(); + var seriesInfo = await _tvdbClientManager.GetSeriesExtendedByIdAsync(tvdbId, language, cancellationToken, small: true).ConfigureAwait(false); + var seasonTvdbId = seriesInfo.Seasons.FirstOrDefault(s => s.Number == seasonNumber)?.Id; - var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false); - await foreach (var keyType in keyTypes) + var seasonInfo = await _tvdbClientManager.GetSeasonByIdAsync(Convert.ToInt32(seasonTvdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false); + var seasonImages = seasonInfo.Artwork; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result; + var artworkTypes = _tvdbClientManager.GetArtworkTypeAsync(CancellationToken.None).Result; + + foreach (var image in seasonImages) { - var imageQuery = new ImagesQuery - { - KeyType = keyType, - SubKey = seasonNumber.ToString(CultureInfo.InvariantCulture) - }; + ImageType type; + // Checks if valid image type, if not, skip try { - var imageResults = await _tvdbClientManager - .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false); - remoteImages.AddRange(GetImages(imageResults.Data, imageQuery.SubKey, language)); + type = TvdbUtils.GetImageTypeFromKeyType(artworkTypes.FirstOrDefault(x => x.Id == image.Type && string.Equals(x.RecordType, "season", StringComparison.OrdinalIgnoreCase))?.Name); } - catch (TvDbServerException) - { - _logger.LogDebug("No images of type {KeyType} found for series {TvdbId}:{Name}", keyType, tvdbId, item.Name); - } - } - - return remoteImages; - } - - /// - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken); - } - - private IEnumerable GetImages(Image[] images, string seasonNumber, string preferredLanguage) - { - var list = new List(); - // any languages with null ids are ignored - var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data.Where(x => x.Id.HasValue).ToArray(); - foreach (Image image in images) - { - // The API returns everything that contains the subkey eg. 2 matches 20, 21, 22, 23 etc. - if (!string.Equals(image.SubKey, seasonNumber, StringComparison.Ordinal)) + catch (Exception) { continue; } @@ -118,50 +95,43 @@ private IEnumerable GetImages(Image[] images, string seasonNumb var imageInfo = new RemoteImageInfo { RatingType = RatingType.Score, - CommunityRating = (double?)image.RatingsInfo.Average, - VoteCount = image.RatingsInfo.Count, - Url = TvdbUtils.BannerUrl + image.FileName, + Url = image.Image, + Width = Convert.ToInt32(image.Width, CultureInfo.InvariantCulture), + Height = Convert.ToInt32(image.Height, CultureInfo.InvariantCulture), + Type = type, ProviderName = Name, - Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, - ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail + ThumbnailUrl = image.Thumbnail }; - var resolution = image.Resolution.Split('x'); - if (resolution.Length == 2) + // Tvdb uses 3 letter code for language (prob ISO 639-2) + var artworkLanguage = languages.FirstOrDefault(lang => string.Equals(lang.Id, image.Language, StringComparison.OrdinalIgnoreCase))?.Id; + if (!string.IsNullOrEmpty(artworkLanguage)) { - imageInfo.Width = Convert.ToInt32(resolution[0], CultureInfo.InvariantCulture); - imageInfo.Height = Convert.ToInt32(resolution[1], CultureInfo.InvariantCulture); + imageInfo.Language = TvdbUtils.NormalizeLanguageToJellyfin(artworkLanguage)?.ToLowerInvariant(); } - imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); - list.Add(imageInfo); + remoteImages.Add(imageInfo); } - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - return list.OrderByDescending(i => + return remoteImages.OrderByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) { - 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); + return 2; + } + else if (!string.IsNullOrEmpty(i.Language)) + { + return 1; + } + + return 0; + }); + } + + /// + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken); } } } diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesImageProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesImageProvider.cs index d1a1d5b..6565df1 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesImageProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesImageProvider.cs @@ -11,8 +11,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; using RatingType = MediaBrowser.Model.Dto.RatingType; using Series = MediaBrowser.Controller.Entities.TV.Series; @@ -68,89 +67,56 @@ public async Task> GetImages(BaseItem item, Cancell var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List(); var tvdbId = Convert.ToInt32(item.GetProviderId(TvdbPlugin.ProviderId), CultureInfo.InvariantCulture); - var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken) - .ConfigureAwait(false); - await foreach (KeyType keyType in allowedKeyTypes) + var seriesInfo = await _tvdbClientManager.GetSeriesImagesAsync(tvdbId, language, cancellationToken).ConfigureAwait(false); + var seriesImages = seriesInfo.Artworks; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result; + var artworkTypes = _tvdbClientManager.GetArtworkTypeAsync(CancellationToken.None).Result; + foreach (var image in seriesImages) { - var imageQuery = new ImagesQuery - { - KeyType = keyType - }; + ImageType type; + // Checks if valid image type, if not, skip try { - var imageResults = - await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) - .ConfigureAwait(false); - - remoteImages.AddRange(GetImages(imageResults.Data, language)); + type = TvdbUtils.GetImageTypeFromKeyType(artworkTypes.FirstOrDefault(x => x.Id == image.Type && string.Equals(x.RecordType, "series", StringComparison.OrdinalIgnoreCase))?.Name); } - catch (TvDbServerException) + catch (Exception) { - _logger.LogDebug( - "No images of type {KeyType} exist for series {TvDbId}:{Name}", - keyType, - tvdbId, - item.Name); + continue; } - } - - return remoteImages; - } - - private IEnumerable GetImages(Image[] images, string preferredLanguage) - { - var list = new List(); - 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, + Url = image.Image, + Width = Convert.ToInt32(image.Width, CultureInfo.InvariantCulture), + Height = Convert.ToInt32(image.Height, CultureInfo.InvariantCulture), + Type = type, ProviderName = Name, - Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation, - ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail + ThumbnailUrl = image.Thumbnail }; - - var resolution = image.Resolution.Split('x'); - if (resolution.Length == 2) + // TVDb uses 3 character language + var imageLanguage = languages.FirstOrDefault(lang => string.Equals(lang.Id, image.Language, StringComparison.OrdinalIgnoreCase))?.Id; + if (!string.IsNullOrEmpty(imageLanguage)) { - imageInfo.Width = Convert.ToInt32(resolution[0], CultureInfo.InvariantCulture); - imageInfo.Height = Convert.ToInt32(resolution[1], CultureInfo.InvariantCulture); + imageInfo.Language = TvdbUtils.NormalizeLanguageToJellyfin(imageLanguage)?.ToLowerInvariant(); } - imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType); - list.Add(imageInfo); + remoteImages.Add(imageInfo); } - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - return list.OrderByDescending(i => + return remoteImages.OrderByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) { - 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 2; + } + else if (!string.IsNullOrEmpty(i.Language)) + { + return 1; + } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0); + return 0; + }); } /// diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs index 1571177..4c3a039 100644 --- a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs +++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs @@ -14,8 +14,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; -using TvDbSharper; -using TvDbSharper.Dto; +using Tvdb.Sdk; using Series = MediaBrowser.Controller.Entities.TV.Series; namespace Jellyfin.Plugin.Tvdb.Providers @@ -116,7 +115,6 @@ private async Task> FetchSeriesSearchResult(Seri { tvdbId = await GetSeriesByRemoteId( imdbId, - MetadataProvider.Imdb.ToString(), seriesInfo.MetadataLanguage, seriesInfo.Name, cancellationToken).ConfigureAwait(false); @@ -130,7 +128,19 @@ private async Task> FetchSeriesSearchResult(Seri { tvdbId = await GetSeriesByRemoteId( zap2ItId, - MetadataProvider.Zap2It.ToString(), + seriesInfo.MetadataLanguage, + seriesInfo.Name, + cancellationToken).ConfigureAwait(false); + } + } + + if (string.IsNullOrEmpty(tvdbId)) + { + var tmdbId = seriesInfo.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(tmdbId)) + { + tvdbId = await GetSeriesByRemoteId( + tmdbId, seriesInfo.MetadataLanguage, seriesInfo.Name, cancellationToken).ConfigureAwait(false); @@ -141,25 +151,25 @@ private async Task> FetchSeriesSearchResult(Seri { var seriesResult = await _tvdbClientManager - .GetSeriesByIdAsync(Convert.ToInt32(tvdbId, CultureInfo.InvariantCulture), seriesInfo.MetadataLanguage, cancellationToken) + .GetSeriesExtendedByIdAsync(Convert.ToInt32(tvdbId, CultureInfo.InvariantCulture), seriesInfo.MetadataLanguage, cancellationToken, small: true) .ConfigureAwait(false); - return new[] { MapSeriesToRemoteSearchResult(seriesResult.Data) }; + return new[] { MapSeriesToRemoteSearchResult(seriesResult) }; } - catch (TvDbServerException e) + catch (Exception e) { _logger.LogError(e, "Failed to retrieve series with id {TvdbId}:{SeriesName}", tvdbId, seriesInfo.Name); return Array.Empty(); } } - private RemoteSearchResult MapSeriesToRemoteSearchResult(TvDbSharper.Dto.Series series) + private RemoteSearchResult MapSeriesToRemoteSearchResult(SeriesExtendedRecord series) { var remoteResult = new RemoteSearchResult { - Name = series.SeriesName, + Name = series.Name, Overview = series.Overview?.Trim() ?? string.Empty, SearchProviderName = Name, - ImageUrl = TvdbUtils.BannerUrl + series.Poster + ImageUrl = series.Image }; if (DateTime.TryParse(series.FirstAired, out var date)) @@ -169,9 +179,10 @@ private RemoteSearchResult MapSeriesToRemoteSearchResult(TvDbSharper.Dto.Series remoteResult.ProductionYear = date.Year; } - if (!string.IsNullOrEmpty(series.ImdbId)) + var imdbID = series.RemoteIds.FirstOrDefault(x => x.SourceName == "IMDB")?.Id; + if (!string.IsNullOrEmpty(imdbID)) { - remoteResult.SetProviderId(MetadataProvider.Imdb, series.ImdbId); + remoteResult.SetProviderId(MetadataProvider.Imdb, imdbID); } remoteResult.SetProviderId(MetadataProvider.Tvdb, series.Id.ToString(CultureInfo.InvariantCulture)); @@ -198,7 +209,6 @@ private async Task FetchSeriesMetadata( series.SetProviderId(MetadataProvider.Imdb, imdbId); tvdbId = await GetSeriesByRemoteId( imdbId, - MetadataProvider.Imdb.ToString(), metadataLanguage, info.Name, cancellationToken).ConfigureAwait(false); @@ -209,7 +219,16 @@ private async Task FetchSeriesMetadata( series.SetProviderId(MetadataProvider.Zap2It, zap2It); tvdbId = await GetSeriesByRemoteId( zap2It, - MetadataProvider.Zap2It.ToString(), + metadataLanguage, + info.Name, + cancellationToken).ConfigureAwait(false); + } + + if (seriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var tmdbId) && !string.IsNullOrEmpty(tmdbId)) + { + series.SetProviderId(MetadataProvider.Tmdb, tmdbId); + tvdbId = await GetSeriesByRemoteId( + tmdbId, metadataLanguage, info.Name, cancellationToken).ConfigureAwait(false); @@ -219,55 +238,47 @@ private async Task FetchSeriesMetadata( { var seriesResult = await _tvdbClientManager - .GetSeriesByIdAsync(Convert.ToInt32(tvdbId, CultureInfo.InvariantCulture), metadataLanguage, cancellationToken) + .GetSeriesExtendedByIdAsync(Convert.ToInt32(tvdbId, CultureInfo.InvariantCulture), metadataLanguage, cancellationToken, Meta4.Translations, false) .ConfigureAwait(false); - await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false); - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve series with id {TvdbId}:{SeriesName}", tvdbId, info.Name); - return; - } + MapSeriesToResult(result, seriesResult, info); - cancellationToken.ThrowIfCancellationRequested(); + result.ResetPeople(); - result.ResetPeople(); + List people = new List(); + if (seriesResult.Characters is not null) + { + foreach (Character character in seriesResult.Characters) + { + people.Add(character); + } - try - { - var actorsResult = await _tvdbClientManager - .GetActorsAsync(Convert.ToInt32(tvdbId, CultureInfo.InvariantCulture), metadataLanguage, cancellationToken).ConfigureAwait(false); - MapActorsToResult(result, actorsResult.Data); + MapActorsToResult(result, people); + } + else + { + _logger.LogError("Failed to retrieve actors for series {TvdbId}:{SeriesName}", tvdbId, info.Name); + } } - catch (TvDbServerException e) + catch (Exception e) { - _logger.LogError(e, "Failed to retrieve actors for series {TvdbId}:{SeriesName}", tvdbId, info.Name); + _logger.LogError(e, "Failed to retrieve series with id {TvdbId}:{SeriesName}", tvdbId, info.Name); + return; } } - private async Task GetSeriesByRemoteId(string id, string idType, string language, string seriesName, CancellationToken cancellationToken) + private async Task GetSeriesByRemoteId(string id, string language, string seriesName, CancellationToken cancellationToken) { - TvDbResponse? result = null; - - try - { - if (string.Equals(idType, MetadataProvider.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) - { - result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) - .ConfigureAwait(false); - } - else - { - result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) + var result = await _tvdbClientManager.GetSeriesByRemoteIdAsync(id, language, cancellationToken) .ConfigureAwait(false); - } - } - catch (TvDbServerException e) + var resultData = result; + + if (resultData == null || resultData.Count == 0) { - _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}:{SeriesName}", id, seriesName); + _logger.LogWarning("TvdbSearch: No series found for id: {0}", id); + return null; } - return result?.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); + return resultData[0].Series.Id.ToString(CultureInfo.InvariantCulture); } /// @@ -301,28 +312,31 @@ private async Task> FindSeriesInternal(string name, str var comparableName = GetComparableName(parsedName.Name); var list = new List, RemoteSearchResult>>(); - TvDbResponse result; + IReadOnlyList result; try { result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) .ConfigureAwait(false); } - catch (TvDbServerException e) + catch (Exception e) { _logger.LogError(e, "No series results found for {Name}", comparableName); return new List(); } - foreach (var seriesSearchResult in result.Data) + foreach (var seriesSearchResult in result) { var tvdbTitles = new List { - seriesSearchResult.SeriesName + seriesSearchResult.Name }; - tvdbTitles.AddRange(seriesSearchResult.Aliases); + if (seriesSearchResult.Aliases is not null) + { + tvdbTitles.AddRange(seriesSearchResult.Aliases); + } DateTime? firstAired = null; - if (DateTime.TryParse(seriesSearchResult.FirstAired, out var parsedFirstAired)) + if (DateTime.TryParse(seriesSearchResult.First_air_time, out var parsedFirstAired)) { firstAired = parsedFirstAired; } @@ -334,26 +348,42 @@ private async Task> FindSeriesInternal(string name, str SearchProviderName = Name }; - if (!string.IsNullOrEmpty(seriesSearchResult.Poster)) + if (!string.IsNullOrEmpty(seriesSearchResult.Image_url)) { - // Results from their Search endpoints already include the /banners/ part in the url, because reasons... - remoteSearchResult.ImageUrl = TvdbUtils.TvdbBaseUrl + seriesSearchResult.Poster.TrimStart('/'); + remoteSearchResult.ImageUrl = seriesSearchResult.Image_url; } try { - var seriesSesult = - await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) + var seriesResult = + await _tvdbClientManager.GetSeriesExtendedByIdAsync(Convert.ToInt32(seriesSearchResult.Tvdb_id, CultureInfo.InvariantCulture), language, cancellationToken, small: true) .ConfigureAwait(false); - remoteSearchResult.SetProviderId(MetadataProvider.Imdb, seriesSesult.Data.ImdbId); - remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, seriesSesult.Data.Zap2itId); + var imdbId = seriesResult.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + if (!string.IsNullOrEmpty(imdbId)) + { + remoteSearchResult.SetProviderId(MetadataProvider.Imdb, imdbId); + } + + var zap2ItId = seriesResult.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "Zap2It", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + + if (!string.IsNullOrEmpty(zap2ItId)) + { + remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, zap2ItId); + } + + var tmdbId = seriesResult.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "TheMovieDB.com", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + + if (!string.IsNullOrEmpty(tmdbId)) + { + remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, tmdbId); + } } - catch (TvDbServerException e) + catch (Exception e) { - _logger.LogError(e, "Unable to retrieve series with id {TvdbId}:{SeriesName}", seriesSearchResult.Id, seriesSearchResult.SeriesName); + _logger.LogError(e, "Unable to retrieve series with id {TvdbId}:{SeriesName}", seriesSearchResult.Tvdb_id, seriesSearchResult.Name); } - remoteSearchResult.SetProviderId(TvdbPlugin.ProviderId, seriesSearchResult.Id.ToString(CultureInfo.InvariantCulture)); + remoteSearchResult.SetProviderId(TvdbPlugin.ProviderId, seriesSearchResult.Tvdb_id); list.Add(new Tuple, RemoteSearchResult>(tvdbTitles, remoteSearchResult)); } @@ -386,21 +416,20 @@ private static string GetComparableName(string name) return name.Trim(); } - private static void MapActorsToResult(MetadataResult result, IEnumerable actors) + private static void MapActorsToResult(MetadataResult result, IEnumerable actors) { - foreach (Actor actor in actors) + foreach (Character actor in actors) { var personInfo = new PersonInfo { Type = PersonType.Actor, - Name = (actor.Name ?? string.Empty).Trim(), - Role = actor.Role, - SortOrder = actor.SortOrder + Name = (actor.PersonName ?? string.Empty).Trim(), + Role = actor.Name }; - if (!string.IsNullOrEmpty(actor.Image)) + if (!string.IsNullOrEmpty(actor.PersonImgURL)) { - personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image; + personInfo.ImageUrl = actor.PersonImgURL; } if (!string.IsNullOrWhiteSpace(personInfo.Name)) @@ -432,19 +461,40 @@ private async Task Identify(SeriesInfo info) } } - private async Task MapSeriesToResult(MetadataResult result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) + private static void MapSeriesToResult(MetadataResult result, SeriesExtendedRecord tvdbSeries, SeriesInfo info) { Series series = result.Item; series.SetProviderId(TvdbPlugin.ProviderId, tvdbSeries.Id.ToString(CultureInfo.InvariantCulture)); - series.Name = tvdbSeries.SeriesName; - series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim(); - result.ResultLanguage = metadataLanguage; - series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek); + // Tvdb uses 3 letter code for language (prob ISO 639-2) + // Reverts to OriginalName if no translation is found + series.Name = tvdbSeries.Translations.NameTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(info.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Name ?? tvdbSeries.Name; + series.Overview = tvdbSeries.Translations.OverviewTranslations.FirstOrDefault(x => string.Equals(x.Language, TvdbUtils.NormalizeLanguageToTvdb(info.MetadataLanguage), StringComparison.OrdinalIgnoreCase))?.Overview ?? tvdbSeries.Overview; + series.OriginalTitle = tvdbSeries.Name; + result.ResultLanguage = info.MetadataLanguage; + series.AirDays = TvdbUtils.GetAirDays(tvdbSeries.AirsDays).ToArray(); series.AirTime = tvdbSeries.AirsTime; - series.CommunityRating = (float?)tvdbSeries.SiteRating; - series.SetProviderId(MetadataProvider.Imdb, tvdbSeries.ImdbId); - series.SetProviderId(MetadataProvider.Zap2It, tvdbSeries.Zap2itId); - if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus)) + // series.CommunityRating = (float?)tvdbSeries.SiteRating; + // Attempts to default to USA if not found + series.OfficialRating = tvdbSeries.ContentRatings.FirstOrDefault(x => string.Equals(x.Country, TvdbCultureInfo.GetCountryInfo(info.MetadataCountryCode)?.ThreeLetterISORegionName, StringComparison.OrdinalIgnoreCase))?.Name ?? tvdbSeries.ContentRatings.FirstOrDefault(x => string.Equals(x.Country, "usa", StringComparison.OrdinalIgnoreCase))?.Name; + var imdbId = tvdbSeries.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + var zap2ItId = tvdbSeries.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "Zap2It", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + var tmdbId = tvdbSeries.RemoteIds.FirstOrDefault(x => string.Equals(x.SourceName, "TheMovieDB.com", StringComparison.OrdinalIgnoreCase))?.Id.ToString(); + if (!string.IsNullOrEmpty(imdbId)) + { + series.SetProviderId(MetadataProvider.Imdb, imdbId); + } + + if (!string.IsNullOrEmpty(zap2ItId)) + { + series.SetProviderId(MetadataProvider.Zap2It, zap2ItId); + } + + if (!string.IsNullOrEmpty(tmdbId)) + { + series.SetProviderId(MetadataProvider.Tmdb, tmdbId); + } + + if (Enum.TryParse(tvdbSeries.Status.Name, true, out SeriesStatus seriesStatus)) { series.Status = seriesStatus; } @@ -456,44 +506,26 @@ private async Task MapSeriesToResult(MetadataResult result, TvDbSharper. series.ProductionYear = date.Year; } - if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime)) + if (tvdbSeries.AverageRuntime is not null) { - series.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; + series.RunTimeTicks = TimeSpan.FromMinutes(tvdbSeries.AverageRuntime.Value).Ticks; } - foreach (var genre in tvdbSeries.Genre) + foreach (var genre in tvdbSeries.Genres) { - series.AddGenre(genre); + series.AddGenre(genre.Name); } - if (!string.IsNullOrEmpty(tvdbSeries.Network)) + if (tvdbSeries.OriginalNetwork is not null) { - series.AddStudio(tvdbSeries.Network); + series.AddStudio(tvdbSeries.OriginalNetwork.Name); } if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended) { - try - { - var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false); - - if (episodeSummary.Data.AiredSeasons.Length != 0) - { - var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture)); - var episodeQuery = new EpisodeQuery - { - AiredSeason = maxSeasonNumber - }; - var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false); - - result.Item.EndDate = episodesPage.Data - .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null) - .Max(); - } - } - catch (TvDbServerException e) + if (tvdbSeries.Seasons.Count != 0) { - _logger.LogError(e, "Failed to find series end date for series {TvdbId}:{SeriesName}", tvdbSeries.Id, tvdbSeries?.SeriesName ?? result.Item?.Name); + result.Item.EndDate = DateTime.ParseExact(tvdbSeries.LastAired, "yyyy-mm-dd", CultureInfo.InvariantCulture); } } } diff --git a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs index f3b0370..1a87780 100644 --- a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs +++ b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs @@ -1,466 +1,443 @@ -using System; -using System.Collections.Concurrent; +using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; +using System.Net; using System.Net.Http; -using System.Reflection; -using System.Runtime.CompilerServices; +using System.Net.Http.Headers; +using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; +using Jellyfin.Plugin.Tvdb.Configuration; +using MediaBrowser.Common; using MediaBrowser.Controller.Providers; -using Microsoft.Extensions.Caching.Memory; -using TvDbSharper; -using TvDbSharper.Dto; +using Microsoft.Extensions.DependencyInjection; +using Tvdb.Sdk; -namespace Jellyfin.Plugin.Tvdb +namespace Jellyfin.Plugin.Tvdb; + +/// +/// Tvdb client manager. +/// +public class TvdbClientManager { + private const string TvdbHttpClient = "TvdbHttpClient"; + private static readonly SemaphoreSlim _tokenUpdateLock = new SemaphoreSlim(1, 1); + + private readonly IHttpClientFactory _httpClientFactory; + private readonly IServiceProvider _serviceProvider; + private readonly SdkClientSettings _sdkClientSettings; + + private DateTime _tokenUpdatedAt; + /// - /// Tvdb client manager. + /// Initializes a new instance of the class. /// - public class TvdbClientManager + /// Instance of the interface. + public TvdbClientManager(IApplicationHost applicationHost) { - private const string DefaultLanguage = "en"; - - private readonly IMemoryCache _cache; - private readonly IHttpClientFactory _httpClientFactory; - - /// - /// TvDbClients per language. - /// - private readonly ConcurrentDictionary _tvDbClients = new ConcurrentDictionary(); - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public TvdbClientManager(IMemoryCache memoryCache, IHttpClientFactory httpClientFactory) - { - _cache = memoryCache; - _httpClientFactory = httpClientFactory; - } + _serviceProvider = ConfigureService(applicationHost); + _httpClientFactory = _serviceProvider.GetRequiredService(); + _sdkClientSettings = _serviceProvider.GetRequiredService(); - private static string? ApiKey => TvdbPlugin.Instance?.Configuration.ApiKey; - - private async Task GetTvDbClient(string language) - { - var normalizedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; + _tokenUpdatedAt = DateTime.MinValue; + } - var tvDbClientInfo = _tvDbClients.GetOrAdd(normalizedLanguage, key => new TvDbClientInfo(_httpClientFactory, key)); + private static string? UserPin => TvdbPlugin.Instance?.Configuration.ApiKey; - var tvDbClient = tvDbClientInfo.Client; + /// + /// Logs in or refresh login to the tvdb api when needed. + /// + private async Task LoginAsync() + { + var loginClient = _serviceProvider.GetRequiredService(); + if (string.IsNullOrEmpty(UserPin)) + { + throw new InvalidOperationException("Subscriber PIN not set"); + } - // First time authenticating if the token was never updated or if it's empty in the client - if (tvDbClientInfo.TokenUpdatedAt == DateTime.MinValue || string.IsNullOrEmpty(tvDbClient.Authentication.Token)) + // Ensure we have a recent token. + if (IsTokenInvalid()) + { + await _tokenUpdateLock.WaitAsync().ConfigureAwait(false); + try { - await tvDbClientInfo.TokenUpdateLock.WaitAsync().ConfigureAwait(false); - - try + if (IsTokenInvalid()) { - if (string.IsNullOrEmpty(tvDbClient.Authentication.Token)) + var loginResponse = await loginClient.LoginAsync(new Body { - await tvDbClient.Authentication.AuthenticateAsync(ApiKey).ConfigureAwait(false); - tvDbClientInfo.TokenUpdatedAt = DateTime.UtcNow; - } - } - finally - { - tvDbClientInfo.TokenUpdateLock.Release(); + Apikey = PluginConfiguration.ProjectApiKey, + Pin = UserPin + }).ConfigureAwait(false); + + _tokenUpdatedAt = DateTime.UtcNow; + _sdkClientSettings.AccessToken = loginResponse.Data.Token; } } - - // Refresh if necessary - if (tvDbClientInfo.TokenUpdatedAt < DateTime.UtcNow.Subtract(TimeSpan.FromHours(20))) + finally { - await tvDbClientInfo.TokenUpdateLock.WaitAsync().ConfigureAwait(false); - - try - { - if (tvDbClientInfo.TokenUpdatedAt < DateTime.UtcNow.Subtract(TimeSpan.FromHours(20))) - { - try - { - await tvDbClient.Authentication.RefreshTokenAsync().ConfigureAwait(false); - } - catch - { - await tvDbClient.Authentication.AuthenticateAsync(ApiKey).ConfigureAwait(false); - } - - tvDbClientInfo.TokenUpdatedAt = DateTime.UtcNow; - } - } - finally - { - tvDbClientInfo.TokenUpdateLock.Release(); - } + _tokenUpdateLock.Release(); } - - return tvDbClient; } - /// - /// Get series by name. - /// - /// Series name. - /// Metadata language. - /// Cancellation token. - /// The series search result. - public Task> GetSeriesByNameAsync( - string name, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", name, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken)); - } + return; - /// - /// Get series by id. - /// - /// The series tvdb id. - /// Metadata language. - /// Cancellation token. - /// The series response. - public Task> GetSeriesByIdAsync( - int tvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", tvdbId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetAsync(tvdbId, cancellationToken)); - } + bool IsTokenInvalid() => + _tokenUpdatedAt == DateTime.MinValue + || string.IsNullOrEmpty(_sdkClientSettings.AccessToken) + || _tokenUpdatedAt < DateTime.UtcNow.Subtract(TimeSpan.FromDays(25)); + } - /// - /// Get episode record. - /// - /// The episode tvdb id. - /// Metadata language. - /// Cancellation token. - /// The episode record. - public Task> GetEpisodesAsync( - int episodeTvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("episode", episodeTvdbId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); - } + /// + /// Get series by name. + /// + /// Series name. + /// Metadata language. + /// Cancellation token. + /// The series search result. + public async Task> GetSeriesByNameAsync( + string name, + string language, + CancellationToken cancellationToken) + { + var searchClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var searchResult = await searchClient.GetSearchResultsAsync(query: name, type: "series", limit: 5, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return searchResult.Data; + } - /// - /// Get series by imdb. - /// - /// The imdb id. - /// Metadata language. - /// Cancellation token. - /// The series search result. - public Task> GetSeriesByImdbIdAsync( - string imdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", imdbId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); - } + /// + /// Get series by id. + /// + /// The series tvdb id. + /// Metadata language. + /// Cancellation token. + /// The series response. + public async Task GetSeriesByIdAsync( + int tvdbId, + string language, + CancellationToken cancellationToken) + { + var seriesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var seriesResult = await seriesClient.GetSeriesBaseAsync(id: tvdbId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return seriesResult.Data; + } - /// - /// Get series by zap2it id. - /// - /// Zap2it id. - /// Metadata language. - /// Cancellation token. - /// The series search result. - public Task> GetSeriesByZap2ItIdAsync( - string zap2ItId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("series", zap2ItId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); - } + /// + /// Get series by id. + /// + /// The series tvdb id. + /// Metadata language. + /// Cancellation token. + /// episodes or translations. + /// Payload size. True for smaller payload. + /// The series response. + public async Task GetSeriesExtendedByIdAsync( + int tvdbId, + string language, + CancellationToken cancellationToken, + Meta4? meta = null, + bool? small = null) + { + var seriesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var seriesResult = await seriesClient.GetSeriesExtendedAsync(id: tvdbId, meta: meta, @short: small, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return seriesResult.Data; + } - /// - /// Get actors by tvdb id. - /// - /// Tvdb id. - /// Metadata language. - /// Cancellation token. - /// The actors attached to the id. - public Task> GetActorsAsync( - int tvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("actors", tvdbId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken)); - } + /// + /// Get all episodes of series. + /// + /// The series tvdb id. + /// Metadata language. + /// Season type: default, dvd, absolute etc. + /// Cancellation token. + /// All episodes of series. + public async Task GetSeriesEpisodesAsync( + int tvdbId, + string language, + string seasonType, + CancellationToken cancellationToken) + { + var seriesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var seriesResult = await seriesClient.GetSeriesEpisodesAsync(id: tvdbId, season_type: seasonType, cancellationToken: cancellationToken, page: 0) + .ConfigureAwait(false); + return seriesResult.Data; + } - /// - /// Get images by tvdb id. - /// - /// Tvdb id. - /// The image query. - /// Metadata language. - /// Cancellation token. - /// The images attached to the id. - public Task> GetImagesAsync( - int tvdbId, - ImagesQuery imageQuery, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("images", tvdbId, language, imageQuery); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken)); - } + /// + /// Get Season record. + /// + /// The season tvdb id. + /// Metadata language. + /// Cancellation token. + /// The episode record. + public async Task GetSeasonByIdAsync( + int seasonTvdbId, + string language, + CancellationToken cancellationToken) + { + var seasonClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var seasonResult = await seasonClient.GetSeasonExtendedAsync(id: seasonTvdbId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return seasonResult.Data; + } - /// - /// Get all tvdb languages. - /// - /// Cancellation token. - /// All tvdb languages. - public Task> GetLanguagesAsync(CancellationToken cancellationToken) - { - return TryGetValue("languages", string.Empty, tvDbClient => tvDbClient.Languages.GetAllAsync(cancellationToken)); - } + /// + /// Get episode record. + /// + /// The episode tvdb id. + /// Metadata language. + /// Cancellation token. + /// The episode record. + public async Task GetEpisodesAsync( + int episodeTvdbId, + string language, + CancellationToken cancellationToken) + { + var episodeClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var episodeResult = await episodeClient.GetEpisodeExtendedAsync(id: episodeTvdbId, meta: Meta.Translations, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return episodeResult.Data; + } - /// - /// Get series episode summary. - /// - /// Tvdb id. - /// Metadata language. - /// Cancellation token. - /// The episode summary. - public Task> GetSeriesEpisodeSummaryAsync( - int tvdbId, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken)); - } + /// + /// Get series by remoteId. + /// + /// The remote id. Supported RemoteIds are: IMDB, TMDB, Zap2It, TV Maze and EIDR. + /// Metadata language. + /// Cancellation token. + /// The series search result. + public async Task> GetSeriesByRemoteIdAsync( + string remoteId, + string language, + CancellationToken cancellationToken) + { + var searchClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var searchResult = await searchClient.GetSearchResultsByRemoteIdAsync(remoteId: remoteId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return searchResult.Data; + } - /// - /// Gets a page of episodes. - /// - /// Tvdb series id. - /// Episode page. - /// Episode query. - /// Metadata language. - /// Cancellation token. - /// The page of episodes. - public Task> GetEpisodesPageAsync( - int tvdbId, - int page, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) - { - var cacheKey = GenerateKey(language, tvdbId, episodeQuery, page); - return TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken)); - } + /// + /// Get actors by tvdb id. + /// + /// People Tvdb id. + /// Metadata language. + /// Cancellation token. + /// The actors attached to the id. + public async Task GetActorAsync( + int tvdbId, + string language, + CancellationToken cancellationToken) + { + var peopleClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var peopleResult = await peopleClient.GetPeopleBaseAsync(id: tvdbId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return peopleResult.Data; + } - /// - /// Get an episode's tvdb id. - /// - /// Episode search info. - /// Metadata language. - /// Cancellation token. - /// The tvdb id. - public Task GetEpisodeTvdbId( - EpisodeInfo searchInfo, - string language, - CancellationToken cancellationToken) - { - searchInfo.SeriesProviderIds.TryGetValue(TvdbPlugin.ProviderId, out var seriesTvdbId); + /// + /// Get image by image tvdb id. + /// + /// Tvdb id. + /// Metadata language. + /// Cancellation token. + /// The images attached to the id. + public async Task GetImageAsync( + int imageTvdbId, + string language, + CancellationToken cancellationToken) + { + var artworkClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var artworkResult = await artworkClient.GetArtworkExtendedAsync(id: imageTvdbId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return artworkResult.Data; + } - var episodeQuery = new EpisodeQuery(); + /// + /// Get image by series tvdb id. + /// + /// Tvdb id. + /// Metadata language. + /// Cancellation token. + /// The images attached to the id. + public async Task GetSeriesImagesAsync( + int tvdbId, + string language, + CancellationToken cancellationToken) + { + var seriesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var seriesResult = await seriesClient.GetSeriesArtworksAsync(id: tvdbId, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return seriesResult.Data; + } - // 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", CultureInfo.InvariantCulture); - } + /// + /// Get all tvdb languages. + /// + /// Cancellation token. + /// All tvdb languages. + public async Task> GetLanguagesAsync(CancellationToken cancellationToken) + { + var languagesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var languagesResult = await languagesClient.GetAllLanguagesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + return languagesResult.Data; + } - return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken); - } + /// + /// Gets all tvdb artwork types. + /// + /// Cancellation Token. + /// All tvdb artwork types. + public async Task> GetArtworkTypeAsync(CancellationToken cancellationToken) + { + var artworkTypesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + var artworkTypesResult = await artworkTypesClient.GetAllArtworkTypesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + return artworkTypesResult.Data; + } - /// - /// Get an episode's tvdb id. - /// - /// The series tvdb id. - /// Episode query. - /// Metadata language. - /// Cancellation token. - /// The tvdb id. - public async Task GetEpisodeTvdbId( - int seriesTvdbId, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) + /// + /// Get an episode's tvdb id. + /// + /// Episode search info. + /// Metadata language. + /// Cancellation token. + /// The tvdb id. + public async Task GetEpisodeTvdbId( + EpisodeInfo searchInfo, + string language, + CancellationToken cancellationToken) + { + var seriesClient = _serviceProvider.GetRequiredService(); + await LoginAsync().ConfigureAwait(false); + searchInfo.SeriesProviderIds.TryGetValue(TvdbPlugin.ProviderId, out var seriesTvdbId); + int? episodeNumber = null; + int? seasonNumber = null; + string? airDate = null; + bool special = false; + // Prefer SxE over premiere date as it is more robust + if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue) { - var episodePage = - await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken) - .ConfigureAwait(false); - return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); - } + switch (searchInfo.SeriesDisplayOrder) + { + case "dvd": + episodeNumber = searchInfo.IndexNumber.Value; + seasonNumber = searchInfo.ParentIndexNumber.Value; + break; + case "absolute": + if (searchInfo.ParentIndexNumber.Value == 0) // check if special + { + special = true; + seasonNumber = 0; + } + else + { + seasonNumber = 1; // absolute order is always season 1 + } - /// - /// Get episode page. - /// - /// Tvdb series id. - /// Episode query. - /// Metadata language. - /// Cancellation token. - /// The page of episodes. - public Task> GetEpisodesPageAsync( - int tvdbId, - EpisodeQuery episodeQuery, - string language, - CancellationToken cancellationToken) + episodeNumber = searchInfo.IndexNumber.Value; + break; + default: + // aired order + episodeNumber = searchInfo.IndexNumber.Value; + seasonNumber = searchInfo.ParentIndexNumber.Value; + break; + } + } + else if (searchInfo.PremiereDate.HasValue) { - return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); + // tvdb expects yyyy-mm-dd format + airDate = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } - /// - /// Get image key types for series. - /// - /// Tvdb series id. - /// Metadata language. - /// Cancellation token. - /// The image key types. - public async IAsyncEnumerable GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) + Response56 seriesResponse; + if (!special) { - // Images summary is language agnostic - var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); - var imagesSummary = await TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); - - if (imagesSummary.Data.Fanart > 0) + switch (searchInfo.SeriesDisplayOrder) { - yield return KeyType.Fanart; - } - - if (imagesSummary.Data.Series > 0) - { - yield return KeyType.Series; - } - - if (imagesSummary.Data.Poster > 0) - { - yield return KeyType.Poster; + case "dvd": + case "absolute": + seriesResponse = await seriesClient.GetSeriesEpisodesAsync(page: 0, id: Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), season_type: searchInfo.SeriesDisplayOrder, season: seasonNumber, episodeNumber: episodeNumber, airDate: airDate, cancellationToken: cancellationToken).ConfigureAwait(false); + break; + default: + seriesResponse = await seriesClient.GetSeriesEpisodesAsync(page: 0, id: Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), season_type: "default", season: seasonNumber, episodeNumber: episodeNumber, airDate: airDate, cancellationToken: cancellationToken).ConfigureAwait(false); + break; } } - - /// - /// Get image key types for season. - /// - /// Tvdb series id. - /// Metadata language. - /// Cancellation token. - /// The image key types. - public async IAsyncEnumerable GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) + else // when special use default order { - // Images summary is language agnostic - var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); - var imagesSummary = await TryGetValue(cacheKey, language, tvDbClient => tvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); - - if (imagesSummary.Data.Season > 0) - { - yield return KeyType.Season; - } + seriesResponse = await seriesClient.GetSeriesEpisodesAsync(page: 0, id: Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), season_type: "default", season: seasonNumber, episodeNumber: episodeNumber, airDate: airDate, cancellationToken: cancellationToken).ConfigureAwait(false); + } - if (imagesSummary.Data.Fanart > 0) - { - yield return KeyType.Fanart; - } + Data2 seriesData = seriesResponse.Data; - if (imagesSummary.Data.SeasonWide > 0) - { - yield return KeyType.Seasonwide; - } + if (seriesData == null || seriesData.Episodes == null || seriesData.Episodes.Count == 0) + { + return null; } - - private static string GenerateKey(params object[] objects) + else { - 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; + return seriesData.Episodes[0].Id.ToString(CultureInfo.InvariantCulture); } + } - private Task TryGetValue(string key, string language, Func> resultFactory) - { - return _cache.GetOrCreateAsync(key, async entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); + /// + /// Create an independent ServiceProvider because registering HttpClients directly into Jellyfin + /// causes issues upstream. + /// + /// Instance of the . + /// The service provider. + private ServiceProvider ConfigureService(IApplicationHost applicationHost) + { + var productHeader = ProductInfoHeaderValue.Parse(applicationHost.ApplicationUserAgent); - var tvDbClient = await GetTvDbClient(language).ConfigureAwait(false); + var assembly = typeof(TvdbPlugin).Assembly.GetName(); + var pluginHeader = new ProductInfoHeaderValue( + assembly.Name!.Replace(' ', '-').Replace('.', '-'), + assembly.Version!.ToString(3)); - var result = await resultFactory.Invoke(tvDbClient).ConfigureAwait(false); + var contactHeader = new ProductInfoHeaderValue($"({applicationHost.ApplicationUserAgentAddress})"); - return result; - }); - } + var services = new ServiceCollection(); - private class TvDbClientInfo - { - public TvDbClientInfo(IHttpClientFactory httpClientFactory, string language) + services.AddSingleton(); + services.AddHttpClient(TvdbHttpClient, c => { - Client = new TvDbClient(httpClientFactory.CreateClient(NamedClient.Default)) - { - AcceptedLanguage = language - }; - - TokenUpdateLock = new SemaphoreSlim(1, 1); - TokenUpdatedAt = DateTime.MinValue; - } - - public TvDbClient Client { get; } - - public SemaphoreSlim TokenUpdateLock { get; } + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.UserAgent.Add(pluginHeader); + c.DefaultRequestHeaders.UserAgent.Add(contactHeader); + }) + .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 + }); - public DateTime TokenUpdatedAt { get; set; } - } + services.AddTransient(_ => new LoginClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new SearchClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new SeriesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new SeasonsClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new EpisodesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new PeopleClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new ArtworkClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new Artwork_TypesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + services.AddTransient(_ => new LanguagesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient))); + + return services.BuildServiceProvider(); } } diff --git a/Jellyfin.Plugin.Tvdb/TvdbCultureInfo.cs b/Jellyfin.Plugin.Tvdb/TvdbCultureInfo.cs new file mode 100644 index 0000000..f28158b --- /dev/null +++ b/Jellyfin.Plugin.Tvdb/TvdbCultureInfo.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml.Schema; +using Jellyfin.Extensions; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Plugin.Tvdb +{ + /// + /// Tvdb culture info. + /// + public static class TvdbCultureInfo + { + private const string _cultureInfo = "Jellyfin.Plugin.Tvdb.iso6392.txt"; + private const string _countryInfo = "Jellyfin.Plugin.Tvdb.countries.json"; + private static readonly Assembly _assembly = typeof(TvdbCultureInfo).Assembly; + private static List _cultures = new List(); + private static List _countries = new List(); + + static TvdbCultureInfo() + { + LoadCultureInfo(); + LoadCountryInfo(); + } + + /// + /// Loads culture info from embedded resource. + /// + private static void LoadCultureInfo() + { + List cultureList = new List(); + using var stream = _assembly.GetManifestResourceStream(_cultureInfo) ?? throw new InvalidOperationException($"Invalid resource path: '{_cultureInfo}'"); + using var reader = new StreamReader(stream); + foreach (var line in reader.ReadAllLines()) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var parts = line.Split('|'); + + if (parts.Length == 5) + { + string name = parts[3]; + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + string twoCharName = parts[2]; + if (string.IsNullOrWhiteSpace(twoCharName)) + { + continue; + } + + string[] threeletterNames; + if (string.IsNullOrWhiteSpace(parts[1])) + { + threeletterNames = new[] { parts[0] }; + } + else + { + threeletterNames = new[] { parts[0], parts[1] }; + } + + cultureList.Add(new CultureDto(name, name, twoCharName, threeletterNames)); + } + } + + _cultures = cultureList; + } + + /// + /// Loads country info from embedded resource. + /// + private static void LoadCountryInfo() + { + using var stream = _assembly.GetManifestResourceStream(_countryInfo) ?? throw new InvalidOperationException($"Invalid resource path: '{_countryInfo}'"); + using var reader = new StreamReader(stream); + _countries = JsonSerializer.Deserialize>(reader.ReadToEnd(), JsonDefaults.Options) ?? throw new InvalidOperationException($"Resource contains invalid data: '{_countryInfo}'"); + } + + /// + /// Gets the cultureinfo for the given language. + /// + /// Language. + /// CultureInfo. + public static CultureDto? GetCultureInfo(string language) + { + for (var i = 0; i < _cultures.Count; i++) + { + var culture = _cultures[i]; + if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) + || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) + { + return culture; + } + } + + return default; + } + + /// + /// Gets the CountryInfo for the given country. + /// + /// Country. + /// CountryInfo. + public static CountryInfo? GetCountryInfo(string country) + { + for (var i = 0; i < _countries.Count; i++) + { + var countryInfo = _countries[i]; + if (country.Equals(countryInfo.Name, StringComparison.OrdinalIgnoreCase) + || country.Equals(countryInfo.TwoLetterISORegionName, StringComparison.OrdinalIgnoreCase) + || country.Equals(countryInfo.ThreeLetterISORegionName, StringComparison.OrdinalIgnoreCase)) + { + return countryInfo; + } + } + + return default; + } + } +} diff --git a/Jellyfin.Plugin.Tvdb/TvdbUtils.cs b/Jellyfin.Plugin.Tvdb/TvdbUtils.cs index f4dbabf..94400c9 100644 --- a/Jellyfin.Plugin.Tvdb/TvdbUtils.cs +++ b/Jellyfin.Plugin.Tvdb/TvdbUtils.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using MediaBrowser.Model.Entities; +using Tvdb.Sdk; namespace Jellyfin.Plugin.Tvdb { @@ -13,28 +15,27 @@ public static class TvdbUtils /// public const string TvdbBaseUrl = "https://www.thetvdb.com/"; - /// - /// Base url for banners. - /// - public const string BannerUrl = TvdbBaseUrl + "banners/"; - /// /// Get image type from key type. /// /// Key type. /// Image type. /// Unknown key type. - public static ImageType GetImageTypeFromKeyType(string keyType) + public static ImageType GetImageTypeFromKeyType(string? keyType) { - switch (keyType.ToLowerInvariant()) + if (!string.IsNullOrEmpty(keyType)) { - 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)); + switch (keyType.ToLowerInvariant()) + { + case "poster": return ImageType.Primary; + case "banner": return ImageType.Banner; + case "background": return ImageType.Backdrop; + case "clearlogo": return ImageType.Logo; + default: throw new ArgumentException($"Invalid or unknown keytype: {keyType}", nameof(keyType)); + } } + + throw new ArgumentException("Null keytype"); } /// @@ -42,15 +43,98 @@ public static ImageType GetImageTypeFromKeyType(string keyType) /// /// Language. /// Normalized language. - public static string? NormalizeLanguage(string? language) + public static string? NormalizeLanguageToTvdb(string? language) { if (string.IsNullOrWhiteSpace(language)) { return null; } - // pt-br is just pt to tvdb - return language.Split('-')[0].ToLowerInvariant(); + // Unique case for zh-TW + if (string.Equals(language, "zh-TW", StringComparison.OrdinalIgnoreCase)) + { + return "zhtw"; + } + + // Unique case for pt-BR + if (string.Equals(language, "pt-br", StringComparison.OrdinalIgnoreCase)) + { + return "pt"; + } + + // to (ISO 639-2) + return TvdbCultureInfo.GetCultureInfo(language)?.ThreeLetterISOLanguageName; + } + + /// + /// Normalize language to jellyfin format. + /// + /// Language. + /// Normalized language. + public static string? NormalizeLanguageToJellyfin(string? language) + { + if (string.IsNullOrWhiteSpace(language)) + { + return null; + } + + // Unique case for zhtw + if (string.Equals(language, "zhtw", StringComparison.OrdinalIgnoreCase)) + { + return "zh-TW"; + } + + // Unique case for pt + if (string.Equals(language, "pt", StringComparison.OrdinalIgnoreCase)) + { + return "pt-BR"; + } + + // to (ISO 639-1) + return TvdbCultureInfo.GetCultureInfo(language)?.TwoLetterISOLanguageName; + } + + /// + /// Converts SeriesAirsDays to DayOfWeek array. + /// + /// SeriesAirDays. + /// List{DayOfWeek}. + public static IEnumerable GetAirDays(SeriesAirsDays seriesAirsDays) + { + if (seriesAirsDays.Sunday) + { + yield return DayOfWeek.Sunday; + } + + if (seriesAirsDays.Monday) + { + yield return DayOfWeek.Monday; + } + + if (seriesAirsDays.Tuesday) + { + yield return DayOfWeek.Tuesday; + } + + if (seriesAirsDays.Wednesday) + { + yield return DayOfWeek.Wednesday; + } + + if (seriesAirsDays.Thursday) + { + yield return DayOfWeek.Thursday; + } + + if (seriesAirsDays.Friday) + { + yield return DayOfWeek.Friday; + } + + if (seriesAirsDays.Saturday) + { + yield return DayOfWeek.Saturday; + } } } -} \ No newline at end of file +} diff --git a/Jellyfin.Plugin.Tvdb/countries.json b/Jellyfin.Plugin.Tvdb/countries.json new file mode 100644 index 0000000..22ffc5e --- /dev/null +++ b/Jellyfin.Plugin.Tvdb/countries.json @@ -0,0 +1,836 @@ +[ + { + "DisplayName": "Afghanistan", + "Name": "AF", + "ThreeLetterISORegionName": "AFG", + "TwoLetterISORegionName": "AF" + }, + { + "DisplayName": "Albania", + "Name": "AL", + "ThreeLetterISORegionName": "ALB", + "TwoLetterISORegionName": "AL" + }, + { + "DisplayName": "Algeria", + "Name": "DZ", + "ThreeLetterISORegionName": "DZA", + "TwoLetterISORegionName": "DZ" + }, + { + "DisplayName": "Argentina", + "Name": "AR", + "ThreeLetterISORegionName": "ARG", + "TwoLetterISORegionName": "AR" + }, + { + "DisplayName": "Armenia", + "Name": "AM", + "ThreeLetterISORegionName": "ARM", + "TwoLetterISORegionName": "AM" + }, + { + "DisplayName": "Australia", + "Name": "AU", + "ThreeLetterISORegionName": "AUS", + "TwoLetterISORegionName": "AU" + }, + { + "DisplayName": "Austria", + "Name": "AT", + "ThreeLetterISORegionName": "AUT", + "TwoLetterISORegionName": "AT" + }, + { + "DisplayName": "Azerbaijan", + "Name": "AZ", + "ThreeLetterISORegionName": "AZE", + "TwoLetterISORegionName": "AZ" + }, + { + "DisplayName": "Bahrain", + "Name": "BH", + "ThreeLetterISORegionName": "BHR", + "TwoLetterISORegionName": "BH" + }, + { + "DisplayName": "Bangladesh", + "Name": "BD", + "ThreeLetterISORegionName": "BGD", + "TwoLetterISORegionName": "BD" + }, + { + "DisplayName": "Belarus", + "Name": "BY", + "ThreeLetterISORegionName": "BLR", + "TwoLetterISORegionName": "BY" + }, + { + "DisplayName": "Belgium", + "Name": "BE", + "ThreeLetterISORegionName": "BEL", + "TwoLetterISORegionName": "BE" + }, + { + "DisplayName": "Belize", + "Name": "BZ", + "ThreeLetterISORegionName": "BLZ", + "TwoLetterISORegionName": "BZ" + }, + { + "DisplayName": "Bolivarian Republic of Venezuela", + "Name": "VE", + "ThreeLetterISORegionName": "VEN", + "TwoLetterISORegionName": "VE" + }, + { + "DisplayName": "Bolivia", + "Name": "BO", + "ThreeLetterISORegionName": "BOL", + "TwoLetterISORegionName": "BO" + }, + { + "DisplayName": "Bosnia and Herzegovina", + "Name": "BA", + "ThreeLetterISORegionName": "BIH", + "TwoLetterISORegionName": "BA" + }, + { + "DisplayName": "Botswana", + "Name": "BW", + "ThreeLetterISORegionName": "BWA", + "TwoLetterISORegionName": "BW" + }, + { + "DisplayName": "Brazil", + "Name": "BR", + "ThreeLetterISORegionName": "BRA", + "TwoLetterISORegionName": "BR" + }, + { + "DisplayName": "Brunei Darussalam", + "Name": "BN", + "ThreeLetterISORegionName": "BRN", + "TwoLetterISORegionName": "BN" + }, + { + "DisplayName": "Bulgaria", + "Name": "BG", + "ThreeLetterISORegionName": "BGR", + "TwoLetterISORegionName": "BG" + }, + { + "DisplayName": "Cambodia", + "Name": "KH", + "ThreeLetterISORegionName": "KHM", + "TwoLetterISORegionName": "KH" + }, + { + "DisplayName": "Cameroon", + "Name": "CM", + "ThreeLetterISORegionName": "CMR", + "TwoLetterISORegionName": "CM" + }, + { + "DisplayName": "Canada", + "Name": "CA", + "ThreeLetterISORegionName": "CAN", + "TwoLetterISORegionName": "CA" + }, + { + "DisplayName": "Caribbean", + "Name": "029", + "ThreeLetterISORegionName": "029", + "TwoLetterISORegionName": "029" + }, + { + "DisplayName": "Chile", + "Name": "CL", + "ThreeLetterISORegionName": "CHL", + "TwoLetterISORegionName": "CL" + }, + { + "DisplayName": "Colombia", + "Name": "CO", + "ThreeLetterISORegionName": "COL", + "TwoLetterISORegionName": "CO" + }, + { + "DisplayName": "Congo [DRC]", + "Name": "CD", + "ThreeLetterISORegionName": "COD", + "TwoLetterISORegionName": "CD" + }, + { + "DisplayName": "Costa Rica", + "Name": "CR", + "ThreeLetterISORegionName": "CRI", + "TwoLetterISORegionName": "CR" + }, + { + "DisplayName": "Croatia", + "Name": "HR", + "ThreeLetterISORegionName": "HRV", + "TwoLetterISORegionName": "HR" + }, + { + "DisplayName": "Czech Republic", + "Name": "CZ", + "ThreeLetterISORegionName": "CZE", + "TwoLetterISORegionName": "CZ" + }, + { + "DisplayName": "Denmark", + "Name": "DK", + "ThreeLetterISORegionName": "DNK", + "TwoLetterISORegionName": "DK" + }, + { + "DisplayName": "Dominican Republic", + "Name": "DO", + "ThreeLetterISORegionName": "DOM", + "TwoLetterISORegionName": "DO" + }, + { + "DisplayName": "Ecuador", + "Name": "EC", + "ThreeLetterISORegionName": "ECU", + "TwoLetterISORegionName": "EC" + }, + { + "DisplayName": "Egypt", + "Name": "EG", + "ThreeLetterISORegionName": "EGY", + "TwoLetterISORegionName": "EG" + }, + { + "DisplayName": "El Salvador", + "Name": "SV", + "ThreeLetterISORegionName": "SLV", + "TwoLetterISORegionName": "SV" + }, + { + "DisplayName": "Eritrea", + "Name": "ER", + "ThreeLetterISORegionName": "ERI", + "TwoLetterISORegionName": "ER" + }, + { + "DisplayName": "Estonia", + "Name": "EE", + "ThreeLetterISORegionName": "EST", + "TwoLetterISORegionName": "EE" + }, + { + "DisplayName": "Ethiopia", + "Name": "ET", + "ThreeLetterISORegionName": "ETH", + "TwoLetterISORegionName": "ET" + }, + { + "DisplayName": "Faroe Islands", + "Name": "FO", + "ThreeLetterISORegionName": "FRO", + "TwoLetterISORegionName": "FO" + }, + { + "DisplayName": "Finland", + "Name": "FI", + "ThreeLetterISORegionName": "FIN", + "TwoLetterISORegionName": "FI" + }, + { + "DisplayName": "France", + "Name": "FR", + "ThreeLetterISORegionName": "FRA", + "TwoLetterISORegionName": "FR" + }, + { + "DisplayName": "Georgia", + "Name": "GE", + "ThreeLetterISORegionName": "GEO", + "TwoLetterISORegionName": "GE" + }, + { + "DisplayName": "Germany", + "Name": "DE", + "ThreeLetterISORegionName": "DEU", + "TwoLetterISORegionName": "DE" + }, + { + "DisplayName": "Greece", + "Name": "GR", + "ThreeLetterISORegionName": "GRC", + "TwoLetterISORegionName": "GR" + }, + { + "DisplayName": "Greenland", + "Name": "GL", + "ThreeLetterISORegionName": "GRL", + "TwoLetterISORegionName": "GL" + }, + { + "DisplayName": "Guatemala", + "Name": "GT", + "ThreeLetterISORegionName": "GTM", + "TwoLetterISORegionName": "GT" + }, + { + "DisplayName": "Haiti", + "Name": "HT", + "ThreeLetterISORegionName": "HTI", + "TwoLetterISORegionName": "HT" + }, + { + "DisplayName": "Honduras", + "Name": "HN", + "ThreeLetterISORegionName": "HND", + "TwoLetterISORegionName": "HN" + }, + { + "DisplayName": "Hong Kong S.A.R.", + "Name": "HK", + "ThreeLetterISORegionName": "HKG", + "TwoLetterISORegionName": "HK" + }, + { + "DisplayName": "Hungary", + "Name": "HU", + "ThreeLetterISORegionName": "HUN", + "TwoLetterISORegionName": "HU" + }, + { + "DisplayName": "Iceland", + "Name": "IS", + "ThreeLetterISORegionName": "ISL", + "TwoLetterISORegionName": "IS" + }, + { + "DisplayName": "India", + "Name": "IN", + "ThreeLetterISORegionName": "IND", + "TwoLetterISORegionName": "IN" + }, + { + "DisplayName": "Indonesia", + "Name": "ID", + "ThreeLetterISORegionName": "IDN", + "TwoLetterISORegionName": "ID" + }, + { + "DisplayName": "Iran", + "Name": "IR", + "ThreeLetterISORegionName": "IRN", + "TwoLetterISORegionName": "IR" + }, + { + "DisplayName": "Iraq", + "Name": "IQ", + "ThreeLetterISORegionName": "IRQ", + "TwoLetterISORegionName": "IQ" + }, + { + "DisplayName": "Ireland", + "Name": "IE", + "ThreeLetterISORegionName": "IRL", + "TwoLetterISORegionName": "IE" + }, + { + "DisplayName": "Islamic Republic of Pakistan", + "Name": "PK", + "ThreeLetterISORegionName": "PAK", + "TwoLetterISORegionName": "PK" + }, + { + "DisplayName": "Israel", + "Name": "IL", + "ThreeLetterISORegionName": "ISR", + "TwoLetterISORegionName": "IL" + }, + { + "DisplayName": "Italy", + "Name": "IT", + "ThreeLetterISORegionName": "ITA", + "TwoLetterISORegionName": "IT" + }, + { + "DisplayName": "Ivory Coast", + "Name": "CI", + "ThreeLetterISORegionName": "CIV", + "TwoLetterISORegionName": "CI" + }, + { + "DisplayName": "Jamaica", + "Name": "JM", + "ThreeLetterISORegionName": "JAM", + "TwoLetterISORegionName": "JM" + }, + { + "DisplayName": "Japan", + "Name": "JP", + "ThreeLetterISORegionName": "JPN", + "TwoLetterISORegionName": "JP" + }, + { + "DisplayName": "Jordan", + "Name": "JO", + "ThreeLetterISORegionName": "JOR", + "TwoLetterISORegionName": "JO" + }, + { + "DisplayName": "Kazakhstan", + "Name": "KZ", + "ThreeLetterISORegionName": "KAZ", + "TwoLetterISORegionName": "KZ" + }, + { + "DisplayName": "Kenya", + "Name": "KE", + "ThreeLetterISORegionName": "KEN", + "TwoLetterISORegionName": "KE" + }, + { + "DisplayName": "Korea", + "Name": "KR", + "ThreeLetterISORegionName": "KOR", + "TwoLetterISORegionName": "KR" + }, + { + "DisplayName": "Kuwait", + "Name": "KW", + "ThreeLetterISORegionName": "KWT", + "TwoLetterISORegionName": "KW" + }, + { + "DisplayName": "Kyrgyzstan", + "Name": "KG", + "ThreeLetterISORegionName": "KGZ", + "TwoLetterISORegionName": "KG" + }, + { + "DisplayName": "Lao P.D.R.", + "Name": "LA", + "ThreeLetterISORegionName": "LAO", + "TwoLetterISORegionName": "LA" + }, + { + "DisplayName": "Latin America", + "Name": "419", + "ThreeLetterISORegionName": "419", + "TwoLetterISORegionName": "419" + }, + { + "DisplayName": "Latvia", + "Name": "LV", + "ThreeLetterISORegionName": "LVA", + "TwoLetterISORegionName": "LV" + }, + { + "DisplayName": "Lebanon", + "Name": "LB", + "ThreeLetterISORegionName": "LBN", + "TwoLetterISORegionName": "LB" + }, + { + "DisplayName": "Libya", + "Name": "LY", + "ThreeLetterISORegionName": "LBY", + "TwoLetterISORegionName": "LY" + }, + { + "DisplayName": "Liechtenstein", + "Name": "LI", + "ThreeLetterISORegionName": "LIE", + "TwoLetterISORegionName": "LI" + }, + { + "DisplayName": "Lithuania", + "Name": "LT", + "ThreeLetterISORegionName": "LTU", + "TwoLetterISORegionName": "LT" + }, + { + "DisplayName": "Luxembourg", + "Name": "LU", + "ThreeLetterISORegionName": "LUX", + "TwoLetterISORegionName": "LU" + }, + { + "DisplayName": "Macao S.A.R.", + "Name": "MO", + "ThreeLetterISORegionName": "MAC", + "TwoLetterISORegionName": "MO" + }, + { + "DisplayName": "Macedonia (FYROM)", + "Name": "MK", + "ThreeLetterISORegionName": "MKD", + "TwoLetterISORegionName": "MK" + }, + { + "DisplayName": "Malaysia", + "Name": "MY", + "ThreeLetterISORegionName": "MYS", + "TwoLetterISORegionName": "MY" + }, + { + "DisplayName": "Maldives", + "Name": "MV", + "ThreeLetterISORegionName": "MDV", + "TwoLetterISORegionName": "MV" + }, + { + "DisplayName": "Mali", + "Name": "ML", + "ThreeLetterISORegionName": "MLI", + "TwoLetterISORegionName": "ML" + }, + { + "DisplayName": "Malta", + "Name": "MT", + "ThreeLetterISORegionName": "MLT", + "TwoLetterISORegionName": "MT" + }, + { + "DisplayName": "Mexico", + "Name": "MX", + "ThreeLetterISORegionName": "MEX", + "TwoLetterISORegionName": "MX" + }, + { + "DisplayName": "Mongolia", + "Name": "MN", + "ThreeLetterISORegionName": "MNG", + "TwoLetterISORegionName": "MN" + }, + { + "DisplayName": "Montenegro", + "Name": "ME", + "ThreeLetterISORegionName": "MNE", + "TwoLetterISORegionName": "ME" + }, + { + "DisplayName": "Morocco", + "Name": "MA", + "ThreeLetterISORegionName": "MAR", + "TwoLetterISORegionName": "MA" + }, + { + "DisplayName": "Nepal", + "Name": "NP", + "ThreeLetterISORegionName": "NPL", + "TwoLetterISORegionName": "NP" + }, + { + "DisplayName": "Netherlands", + "Name": "NL", + "ThreeLetterISORegionName": "NLD", + "TwoLetterISORegionName": "NL" + }, + { + "DisplayName": "New Zealand", + "Name": "NZ", + "ThreeLetterISORegionName": "NZL", + "TwoLetterISORegionName": "NZ" + }, + { + "DisplayName": "Nicaragua", + "Name": "NI", + "ThreeLetterISORegionName": "NIC", + "TwoLetterISORegionName": "NI" + }, + { + "DisplayName": "Nigeria", + "Name": "NG", + "ThreeLetterISORegionName": "NGA", + "TwoLetterISORegionName": "NG" + }, + { + "DisplayName": "Norway", + "Name": "NO", + "ThreeLetterISORegionName": "NOR", + "TwoLetterISORegionName": "NO" + }, + { + "DisplayName": "Oman", + "Name": "OM", + "ThreeLetterISORegionName": "OMN", + "TwoLetterISORegionName": "OM" + }, + { + "DisplayName": "Palestine", + "Name": "PS", + "ThreeLetterISORegionName": "PSE", + "TwoLetterISORegionName": "PS" + }, + { + "DisplayName": "Panama", + "Name": "PA", + "ThreeLetterISORegionName": "PAN", + "TwoLetterISORegionName": "PA" + }, + { + "DisplayName": "Paraguay", + "Name": "PY", + "ThreeLetterISORegionName": "PRY", + "TwoLetterISORegionName": "PY" + }, + { + "DisplayName": "People's Republic of China", + "Name": "CN", + "ThreeLetterISORegionName": "CHN", + "TwoLetterISORegionName": "CN" + }, + { + "DisplayName": "Peru", + "Name": "PE", + "ThreeLetterISORegionName": "PER", + "TwoLetterISORegionName": "PE" + }, + { + "DisplayName": "Philippines", + "Name": "PH", + "ThreeLetterISORegionName": "PHL", + "TwoLetterISORegionName": "PH" + }, + { + "DisplayName": "Poland", + "Name": "PL", + "ThreeLetterISORegionName": "POL", + "TwoLetterISORegionName": "PL" + }, + { + "DisplayName": "Portugal", + "Name": "PT", + "ThreeLetterISORegionName": "PRT", + "TwoLetterISORegionName": "PT" + }, + { + "DisplayName": "Principality of Monaco", + "Name": "MC", + "ThreeLetterISORegionName": "MCO", + "TwoLetterISORegionName": "MC" + }, + { + "DisplayName": "Puerto Rico", + "Name": "PR", + "ThreeLetterISORegionName": "PRI", + "TwoLetterISORegionName": "PR" + }, + { + "DisplayName": "Qatar", + "Name": "QA", + "ThreeLetterISORegionName": "QAT", + "TwoLetterISORegionName": "QA" + }, + { + "DisplayName": "Republica Moldova", + "Name": "MD", + "ThreeLetterISORegionName": "MDA", + "TwoLetterISORegionName": "MD" + }, + { + "DisplayName": "Réunion", + "Name": "RE", + "ThreeLetterISORegionName": "REU", + "TwoLetterISORegionName": "RE" + }, + { + "DisplayName": "Romania", + "Name": "RO", + "ThreeLetterISORegionName": "ROU", + "TwoLetterISORegionName": "RO" + }, + { + "DisplayName": "Russia", + "Name": "RU", + "ThreeLetterISORegionName": "RUS", + "TwoLetterISORegionName": "RU" + }, + { + "DisplayName": "Rwanda", + "Name": "RW", + "ThreeLetterISORegionName": "RWA", + "TwoLetterISORegionName": "RW" + }, + { + "DisplayName": "Saudi Arabia", + "Name": "SA", + "ThreeLetterISORegionName": "SAU", + "TwoLetterISORegionName": "SA" + }, + { + "DisplayName": "Senegal", + "Name": "SN", + "ThreeLetterISORegionName": "SEN", + "TwoLetterISORegionName": "SN" + }, + { + "DisplayName": "Serbia", + "Name": "RS", + "ThreeLetterISORegionName": "SRB", + "TwoLetterISORegionName": "RS" + }, + { + "DisplayName": "Serbia and Montenegro (Former)", + "Name": "CS", + "ThreeLetterISORegionName": "SCG", + "TwoLetterISORegionName": "CS" + }, + { + "DisplayName": "Singapore", + "Name": "SG", + "ThreeLetterISORegionName": "SGP", + "TwoLetterISORegionName": "SG" + }, + { + "DisplayName": "Slovakia", + "Name": "SK", + "ThreeLetterISORegionName": "SVK", + "TwoLetterISORegionName": "SK" + }, + { + "DisplayName": "Slovenia", + "Name": "SI", + "ThreeLetterISORegionName": "SVN", + "TwoLetterISORegionName": "SI" + }, + { + "DisplayName": "Soomaaliya", + "Name": "SO", + "ThreeLetterISORegionName": "SOM", + "TwoLetterISORegionName": "SO" + }, + { + "DisplayName": "South Africa", + "Name": "ZA", + "ThreeLetterISORegionName": "ZAF", + "TwoLetterISORegionName": "ZA" + }, + { + "DisplayName": "Spain", + "Name": "ES", + "ThreeLetterISORegionName": "ESP", + "TwoLetterISORegionName": "ES" + }, + { + "DisplayName": "Sri Lanka", + "Name": "LK", + "ThreeLetterISORegionName": "LKA", + "TwoLetterISORegionName": "LK" + }, + { + "DisplayName": "Sweden", + "Name": "SE", + "ThreeLetterISORegionName": "SWE", + "TwoLetterISORegionName": "SE" + }, + { + "DisplayName": "Switzerland", + "Name": "CH", + "ThreeLetterISORegionName": "CHE", + "TwoLetterISORegionName": "CH" + }, + { + "DisplayName": "Syria", + "Name": "SY", + "ThreeLetterISORegionName": "SYR", + "TwoLetterISORegionName": "SY" + }, + { + "DisplayName": "Taiwan", + "Name": "TW", + "ThreeLetterISORegionName": "TWN", + "TwoLetterISORegionName": "TW" + }, + { + "DisplayName": "Tajikistan", + "Name": "TJ", + "ThreeLetterISORegionName": "TAJ", + "TwoLetterISORegionName": "TJ" + }, + { + "DisplayName": "Thailand", + "Name": "TH", + "ThreeLetterISORegionName": "THA", + "TwoLetterISORegionName": "TH" + }, + { + "DisplayName": "Trinidad and Tobago", + "Name": "TT", + "ThreeLetterISORegionName": "TTO", + "TwoLetterISORegionName": "TT" + }, + { + "DisplayName": "Tunisia", + "Name": "TN", + "ThreeLetterISORegionName": "TUN", + "TwoLetterISORegionName": "TN" + }, + { + "DisplayName": "Turkey", + "Name": "TR", + "ThreeLetterISORegionName": "TUR", + "TwoLetterISORegionName": "TR" + }, + { + "DisplayName": "Turkmenistan", + "Name": "TM", + "ThreeLetterISORegionName": "TKM", + "TwoLetterISORegionName": "TM" + }, + { + "DisplayName": "U.A.E.", + "Name": "AE", + "ThreeLetterISORegionName": "ARE", + "TwoLetterISORegionName": "AE" + }, + { + "DisplayName": "Ukraine", + "Name": "UA", + "ThreeLetterISORegionName": "UKR", + "TwoLetterISORegionName": "UA" + }, + { + "DisplayName": "United Kingdom", + "Name": "GB", + "ThreeLetterISORegionName": "GBR", + "TwoLetterISORegionName": "GB" + }, + { + "DisplayName": "United States", + "Name": "US", + "ThreeLetterISORegionName": "USA", + "TwoLetterISORegionName": "US" + }, + { + "DisplayName": "Uruguay", + "Name": "UY", + "ThreeLetterISORegionName": "URY", + "TwoLetterISORegionName": "UY" + }, + { + "DisplayName": "Uzbekistan", + "Name": "UZ", + "ThreeLetterISORegionName": "UZB", + "TwoLetterISORegionName": "UZ" + }, + { + "DisplayName": "Vietnam", + "Name": "VN", + "ThreeLetterISORegionName": "VNM", + "TwoLetterISORegionName": "VN" + }, + { + "DisplayName": "Yemen", + "Name": "YE", + "ThreeLetterISORegionName": "YEM", + "TwoLetterISORegionName": "YE" + }, + { + "DisplayName": "Zimbabwe", + "Name": "ZW", + "ThreeLetterISORegionName": "ZWE", + "TwoLetterISORegionName": "ZW" + } +] diff --git a/Jellyfin.Plugin.Tvdb/iso6392.txt b/Jellyfin.Plugin.Tvdb/iso6392.txt new file mode 100644 index 0000000..b55c0fa --- /dev/null +++ b/Jellyfin.Plugin.Tvdb/iso6392.txt @@ -0,0 +1,493 @@ +aar||aa|Afar|afar +abk||ab|Abkhazian|abkhaze +ace|||Achinese|aceh +ach|||Acoli|acoli +ada|||Adangme|adangme +ady|||Adyghe; Adygei|adyghé +afa|||Afro-Asiatic languages|afro-asiatiques, langues +afh|||Afrihili|afrihili +afr||af|Afrikaans|afrikaans +ain|||Ainu|aïnou +aka||ak|Akan|akan +akk|||Akkadian|akkadien +alb|sqi|sq|Albanian|albanais +ale|||Aleut|aléoute +alg|||Algonquian languages|algonquines, langues +alt|||Southern Altai|altai du Sud +amh||am|Amharic|amharique +ang|||English, Old (ca.450-1100)|anglo-saxon (ca.450-1100) +anp|||Angika|angika +apa|||Apache languages|apaches, langues +ara||ar|Arabic|arabe +arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE) +arg||an|Aragonese|aragonais +arm|hye|hy|Armenian|arménien +arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce +arp|||Arapaho|arapaho +art|||Artificial languages|artificielles, langues +arw|||Arawak|arawak +asm||as|Assamese|assamais +ast|||Asturian; Bable; Leonese; Asturleonese|asturien; bable; léonais; asturoléonais +ath|||Athapascan languages|athapascanes, langues +aus|||Australian languages|australiennes, langues +ava||av|Avaric|avar +ave||ae|Avestan|avestique +awa|||Awadhi|awadhi +aym||ay|Aymara|aymara +aze||az|Azerbaijani|azéri +bad|||Banda languages|banda, langues +bai|||Bamileke languages|bamiléké, langues +bak||ba|Bashkir|bachkir +bal|||Baluchi|baloutchi +bam||bm|Bambara|bambara +ban|||Balinese|balinais +baq|eus|eu|Basque|basque +bas|||Basa|basa +bat|||Baltic languages|baltes, langues +bej|||Beja; Bedawiyet|bedja +bel||be|Belarusian|biélorusse +bem|||Bemba|bemba +ben||bn|Bengali|bengali +ber|||Berber languages|berbères, langues +bho|||Bhojpuri|bhojpuri +bih||bh|Bihari languages|langues biharis +bik|||Bikol|bikol +bin|||Bini; Edo|bini; edo +bis||bi|Bislama|bichlamar +bla|||Siksika|blackfoot +bnt|||Bantu (Other)|bantoues, autres langues +bos||bs|Bosnian|bosniaque +bra|||Braj|braj +bre||br|Breton|breton +btk|||Batak languages|batak, langues +bua|||Buriat|bouriate +bug|||Buginese|bugi +bul||bg|Bulgarian|bulgare +bur|mya|my|Burmese|birman +byn|||Blin; Bilin|blin; bilen +cad|||Caddo|caddo +cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues +car|||Galibi Carib|karib; galibi; carib +cat||ca|Catalan; Valencian|catalan; valencien +cau|||Caucasian languages|caucasiennes, langues +ceb|||Cebuano|cebuano +cel|||Celtic languages|celtiques, langues; celtes, langues +cha||ch|Chamorro|chamorro +chb|||Chibcha|chibcha +che||ce|Chechen|tchétchène +chg|||Chagatai|djaghataï +chi|zho|zh|Chinese|chinois +chi|zho|ze|Chinese; Bilingual|chinois +chi|zho|zh-tw|Chinese; Traditional|chinois +chi|zho|zh-hk|Chinese; Hong Kong|chinois +chk|||Chuukese|chuuk +chm|||Mari|mari +chn|||Chinook jargon|chinook, jargon +cho|||Choctaw|choctaw +chp|||Chipewyan; Dene Suline|chipewyan +chr|||Cherokee|cherokee +chu||cu|Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic|slavon d'église; vieux slave; slavon liturgique; vieux bulgare +chv||cv|Chuvash|tchouvache +chy|||Cheyenne|cheyenne +cmc|||Chamic languages|chames, langues +cop|||Coptic|copte +cor||kw|Cornish|cornique +cos||co|Corsican|corse +cpe|||Creoles and pidgins, English based|créoles et pidgins basés sur l'anglais +cpf|||Creoles and pidgins, French-based |créoles et pidgins basés sur le français +cpp|||Creoles and pidgins, Portuguese-based |créoles et pidgins basés sur le portugais +cre||cr|Cree|cree +crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé +crp|||Creoles and pidgins |créoles et pidgins +csb|||Kashubian|kachoube +cus|||Cushitic languages|couchitiques, langues +cze|ces|cs|Czech|tchèque +dak|||Dakota|dakota +dan||da|Danish|danois +dar|||Dargwa|dargwa +day|||Land Dayak languages|dayak, langues +del|||Delaware|delaware +den|||Slave (Athapascan)|esclave (athapascan) +dgr|||Dogrib|dogrib +din|||Dinka|dinka +div||dv|Divehi; Dhivehi; Maldivian|maldivien +doi|||Dogri|dogri +dra|||Dravidian languages|dravidiennes, langues +dsb|||Lower Sorbian|bas-sorabe +dua|||Duala|douala +dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350) +dut|nld|nl|Dutch; Flemish|néerlandais; flamand +dyu|||Dyula|dioula +dzo||dz|Dzongkha|dzongkha +efi|||Efik|efik +egy|||Egyptian (Ancient)|égyptien +eka|||Ekajuk|ekajuk +elx|||Elamite|élamite +eng||en|English|anglais +enm|||English, Middle (1100-1500)|anglais moyen (1100-1500) +epo||eo|Esperanto|espéranto +est||et|Estonian|estonien +ewe||ee|Ewe|éwé +ewo|||Ewondo|éwondo +fan|||Fang|fang +fao||fo|Faroese|féroïen +fat|||Fanti|fanti +fij||fj|Fijian|fidjien +fil|||Filipino; Pilipino|filipino; pilipino +fin||fi|Finnish|finnois +fiu|||Finno-Ugrian languages|finno-ougriennes, langues +fon|||Fon|fon +fre|fra|fr|French|français +frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600) +fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400) +frc||fr-ca|French (Canada)|french +frr|||Northern Frisian|frison septentrional +frs|||Eastern Frisian|frison oriental +fry||fy|Western Frisian|frison occidental +ful||ff|Fulah|peul +fur|||Friulian|frioulan +gaa|||Ga|ga +gay|||Gayo|gayo +gba|||Gbaya|gbaya +gem|||Germanic languages|germaniques, langues +geo|kat|ka|Georgian|géorgien +ger|deu|de|German|allemand +gez|||Geez|guèze +gil|||Gilbertese|kiribati +gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais +gle||ga|Irish|irlandais +glg||gl|Galician|galicien +glv||gv|Manx|manx; mannois +gmh|||German, Middle High (ca.1050-1500)|allemand, moyen haut (ca. 1050-1500) +goh|||German, Old High (ca.750-1050)|allemand, vieux haut (ca. 750-1050) +gon|||Gondi|gond +gor|||Gorontalo|gorontalo +got|||Gothic|gothique +grb|||Grebo|grebo +grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453) +gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453) +grn||gn|Guarani|guarani +gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien +guj||gu|Gujarati|goudjrati +gwi|||Gwich'in|gwich'in +hai|||Haida|haida +hat||ht|Haitian; Haitian Creole|haïtien; créole haïtien +hau||ha|Hausa|haoussa +haw|||Hawaiian|hawaïen +heb||he|Hebrew|hébreu +her||hz|Herero|herero +hil|||Hiligaynon|hiligaynon +him|||Himachali languages; Western Pahari languages|langues himachalis; langues paharis occidentales +hin||hi|Hindi|hindi +hit|||Hittite|hittite +hmn|||Hmong; Mong|hmong +hmo||ho|Hiri Motu|hiri motu +hrv||hr|Croatian|croate +hsb|||Upper Sorbian|haut-sorabe +hun||hu|Hungarian|hongrois +hup|||Hupa|hupa +iba|||Iban|iban +ibo||ig|Igbo|igbo +ice|isl|is|Icelandic|islandais +ido||io|Ido|ido +iii||ii|Sichuan Yi; Nuosu|yi de Sichuan +ijo|||Ijo languages|ijo, langues +iku||iu|Inuktitut|inuktitut +ile||ie|Interlingue; Occidental|interlingue +ilo|||Iloko|ilocano +ina||ia|Interlingua (International Auxiliary Language Association)|interlingua (langue auxiliaire internationale) +inc|||Indic languages|indo-aryennes, langues +ind||id|Indonesian|indonésien +ine|||Indo-European languages|indo-européennes, langues +inh|||Ingush|ingouche +ipk||ik|Inupiaq|inupiaq +ira|||Iranian languages|iraniennes, langues +iro|||Iroquoian languages|iroquoises, langues +ita||it|Italian|italien +jav||jv|Javanese|javanais +jbo|||Lojban|lojban +jpn||ja|Japanese|japonais +jpr|||Judeo-Persian|judéo-persan +jrb|||Judeo-Arabic|judéo-arabe +kaa|||Kara-Kalpak|karakalpak +kab|||Kabyle|kabyle +kac|||Kachin; Jingpho|kachin; jingpho +kal||kl|Kalaallisut; Greenlandic|groenlandais +kam|||Kamba|kamba +kan||kn|Kannada|kannada +kar|||Karen languages|karen, langues +kas||ks|Kashmiri|kashmiri +kau||kr|Kanuri|kanouri +kaw|||Kawi|kawi +kaz||kk|Kazakh|kazakh +kbd|||Kabardian|kabardien +kha|||Khasi|khasi +khi|||Khoisan languages|khoïsan, langues +khm||km|Central Khmer|khmer central +kho|||Khotanese; Sakan|khotanais; sakan +kik||ki|Kikuyu; Gikuyu|kikuyu +kin||rw|Kinyarwanda|rwanda +kir||ky|Kirghiz; Kyrgyz|kirghiz +kmb|||Kimbundu|kimbundu +kok|||Konkani|konkani +kom||kv|Komi|kom +kon||kg|Kongo|kongo +kor||ko|Korean|coréen +kos|||Kosraean|kosrae +kpe|||Kpelle|kpellé +krc|||Karachay-Balkar|karatchai balkar +krl|||Karelian|carélien +kro|||Kru languages|krou, langues +kru|||Kurukh|kurukh +kua||kj|Kuanyama; Kwanyama|kuanyama; kwanyama +kum|||Kumyk|koumyk +kur||ku|Kurdish|kurde +kut|||Kutenai|kutenai +lad|||Ladino|judéo-espagnol +lah|||Lahnda|lahnda +lam|||Lamba|lamba +lao||lo|Lao|lao +lat||la|Latin|latin +lav||lv|Latvian|letton +lez|||Lezghian|lezghien +lim||li|Limburgan; Limburger; Limburgish|limbourgeois +lin||ln|Lingala|lingala +lit||lt|Lithuanian|lituanien +lol|||Mongo|mongo +loz|||Lozi|lozi +ltz||lb|Luxembourgish; Letzeburgesch|luxembourgeois +lua|||Luba-Lulua|luba-lulua +lub||lu|Luba-Katanga|luba-katanga +lug||lg|Ganda|ganda +lui|||Luiseno|luiseno +lun|||Lunda|lunda +luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie) +lus|||Lushai|lushai +mac|mkd|mk|Macedonian|macédonien +mad|||Madurese|madourais +mag|||Magahi|magahi +mah||mh|Marshallese|marshall +mai|||Maithili|maithili +mak|||Makasar|makassar +mal||ml|Malayalam|malayalam +man|||Mandingo|mandingue +mao|mri|mi|Maori|maori +map|||Austronesian languages|austronésiennes, langues +mar||mr|Marathi|marathe +mas|||Masai|massaï +may|msa|ms|Malay|malais +mdf|||Moksha|moksa +mdr|||Mandar|mandar +men|||Mende|mendé +mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200) +mic|||Mi'kmaq; Micmac|mi'kmaq; micmac +min|||Minangkabau|minangkabau +mis|||Uncoded languages|langues non codées +mkh|||Mon-Khmer languages|môn-khmer, langues +mlg||mg|Malagasy|malgache +mlt||mt|Maltese|maltais +mnc|||Manchu|mandchou +mni|||Manipuri|manipuri +mno|||Manobo languages|manobo, langues +moh|||Mohawk|mohawk +mon||mn|Mongolian|mongol +mos|||Mossi|moré +mul|||Multiple languages|multilingue +mun|||Munda languages|mounda, langues +mus|||Creek|muskogee +mwl|||Mirandese|mirandais +mwr|||Marwari|marvari +myn|||Mayan languages|maya, langues +myv|||Erzya|erza +nah|||Nahuatl languages|nahuatl, langues +nai|||North American Indian languages|nord-amérindiennes, langues +nap|||Neapolitan|napolitain +nau||na|Nauru|nauruan +nav||nv|Navajo; Navaho|navaho +nbl||nr|Ndebele, South; South Ndebele|ndébélé du Sud +nde||nd|Ndebele, North; North Ndebele|ndébélé du Nord +ndo||ng|Ndonga|ndonga +nds|||Low German; Low Saxon; German, Low; Saxon, Low|bas allemand; bas saxon; allemand, bas; saxon, bas +nep||ne|Nepali|népalais +new|||Nepal Bhasa; Newari|nepal bhasa; newari +nia|||Nias|nias +nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues +niu|||Niuean|niué +nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien +nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål +nog|||Nogai|nogaï; nogay +non|||Norse, Old|norrois, vieux +nor||no|Norwegian|norvégien +nqo|||N'Ko|n'ko +nso|||Pedi; Sepedi; Northern Sotho|pedi; sepedi; sotho du Nord +nub|||Nubian languages|nubiennes, langues +nwc|||Classical Newari; Old Newari; Classical Nepal Bhasa|newari classique +nya||ny|Chichewa; Chewa; Nyanja|chichewa; chewa; nyanja +nym|||Nyamwezi|nyamwezi +nyn|||Nyankole|nyankolé +nyo|||Nyoro|nyoro +nzi|||Nzima|nzema +oci||oc|Occitan (post 1500); Provençal|occitan (après 1500); provençal +oji||oj|Ojibwa|ojibwa +ori||or|Oriya|oriya +orm||om|Oromo|galla +osa|||Osage|osage +oss||os|Ossetian; Ossetic|ossète +ota|||Turkish, Ottoman (1500-1928)|turc ottoman (1500-1928) +oto|||Otomian languages|otomi, langues +paa|||Papuan languages|papoues, langues +pag|||Pangasinan|pangasinan +pal|||Pahlavi|pahlavi +pam|||Pampanga; Kapampangan|pampangan +pan||pa|Panjabi; Punjabi|pendjabi +pap|||Papiamento|papiamento +pau|||Palauan|palau +peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.) +per|fas|fa|Persian|persan +phi|||Philippine languages|philippines, langues +phn|||Phoenician|phénicien +pli||pi|Pali|pali +pol||pl|Polish|polonais +pon|||Pohnpeian|pohnpei +por||pt|Portuguese|portugais +pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt) +pob||pt-br|Portuguese (Brazil)|portugais (pt-br) +pra|||Prakrit languages|prâkrit, langues +pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) +pus||ps|Pushto; Pashto|pachto +qaa-qtz|||Reserved for local use|réservée à l'usage local +que||qu|Quechua|quechua +raj|||Rajasthani|rajasthani +rap|||Rapanui|rapanui +rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook +roa|||Romance languages|romanes, langues +roh||rm|Romansh|romanche +rom|||Romany|tsigane +rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave +run||rn|Rundi|rundi +rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain +rus||ru|Russian|russe +sad|||Sandawe|sandawe +sag||sg|Sango|sango +sah|||Yakut|iakoute +sai|||South American Indian (Other)|indiennes d'Amérique du Sud, autres langues +sal|||Salishan languages|salishennes, langues +sam|||Samaritan Aramaic|samaritain +san||sa|Sanskrit|sanskrit +sas|||Sasak|sasak +sat|||Santali|santal +scn|||Sicilian|sicilien +sco|||Scots|écossais +sel|||Selkup|selkoupe +sem|||Semitic languages|sémitiques, langues +sga|||Irish, Old (to 900)|irlandais ancien (jusqu'à 900) +sgn|||Sign Languages|langues des signes +shn|||Shan|chan +sid|||Sidamo|sidamo +sin||si|Sinhala; Sinhalese|singhalais +sio|||Siouan languages|sioux, langues +sit|||Sino-Tibetan languages|sino-tibétaines, langues +sla|||Slavic languages|slaves, langues +slo|slk|sk|Slovak|slovaque +slv||sl|Slovenian|slovène +sma|||Southern Sami|sami du Sud +sme||se|Northern Sami|sami du Nord +smi|||Sami languages|sames, langues +smj|||Lule Sami|sami de Lule +smn|||Inari Sami|sami d'Inari +smo||sm|Samoan|samoan +sms|||Skolt Sami|sami skolt +sna||sn|Shona|shona +snd||sd|Sindhi|sindhi +snk|||Soninke|soninké +sog|||Sogdian|sogdien +som||so|Somali|somali +son|||Songhai languages|songhai, langues +sot||st|Sotho, Southern|sotho du Sud +spa||es-mx|Spanish; Latin|espagnol; Latin +spa||es|Spanish; Castilian|espagnol; castillan +srd||sc|Sardinian|sarde +srn|||Sranan Tongo|sranan tongo +srp|scc|sr|Serbian|serbe +srr|||Serer|sérère +ssa|||Nilo-Saharan languages|nilo-sahariennes, langues +ssw||ss|Swati|swati +suk|||Sukuma|sukuma +sun||su|Sundanese|soundanais +sus|||Susu|soussou +sux|||Sumerian|sumérien +swa||sw|Swahili|swahili +swe||sv|Swedish|suédois +syc|||Classical Syriac|syriaque classique +syr|||Syriac|syriaque +tah||ty|Tahitian|tahitien +tai|||Tai languages|tai, langues +tam||ta|Tamil|tamoul +tat||tt|Tatar|tatar +tel||te|Telugu|télougou +tem|||Timne|temne +ter|||Tereno|tereno +tet|||Tetum|tetum +tgk||tg|Tajik|tadjik +tgl||tl|Tagalog|tagalog +tha||th|Thai|thaï +tib|bod|bo|Tibetan|tibétain +tig|||Tigre|tigré +tir||ti|Tigrinya|tigrigna +tiv|||Tiv|tiv +tkl|||Tokelau|tokelau +tlh|||Klingon; tlhIngan-Hol|klingon +tli|||Tlingit|tlingit +tmh|||Tamashek|tamacheq +tog|||Tonga (Nyasa)|tonga (Nyasa) +ton||to|Tonga (Tonga Islands)|tongan (Îles Tonga) +tpi|||Tok Pisin|tok pisin +tsi|||Tsimshian|tsimshian +tsn||tn|Tswana|tswana +tso||ts|Tsonga|tsonga +tuk||tk|Turkmen|turkmène +tum|||Tumbuka|tumbuka +tup|||Tupi languages|tupi, langues +tur||tr|Turkish|turc +tut|||Altaic languages|altaïques, langues +tvl|||Tuvalu|tuvalu +twi||tw|Twi|twi +tyv|||Tuvinian|touva +udm|||Udmurt|oudmourte +uga|||Ugaritic|ougaritique +uig||ug|Uighur; Uyghur|ouïgour +ukr||uk|Ukrainian|ukrainien +umb|||Umbundu|umbundu +und|||Undetermined|indéterminée +urd||ur|Urdu|ourdou +uzb||uz|Uzbek|ouszbek +vai|||Vai|vaï +ven||ve|Venda|venda +vie||vi|Vietnamese|vietnamien +vol||vo|Volapük|volapük +vot|||Votic|vote +wak|||Wakashan languages|wakashanes, langues +wal|||Walamo|walamo +war|||Waray|waray +was|||Washo|washo +wel|cym|cy|Welsh|gallois +wen|||Sorbian languages|sorabes, langues +wln||wa|Walloon|wallon +wol||wo|Wolof|wolof +xal|||Kalmyk; Oirat|kalmouk; oïrat +xho||xh|Xhosa|xhosa +yao|||Yao|yao +yap|||Yapese|yapois +yid||yi|Yiddish|yiddish +yor||yo|Yoruba|yoruba +ypk|||Yupik languages|yupik, langues +zap|||Zapotec|zapotèque +zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss +zen|||Zenaga|zenaga +zgh|||Standard Moroccan Tamazight|amazighe standard marocain +zha||za|Zhuang; Chuang|zhuang; chuang +znd|||Zande languages|zandé, langues +zul||zu|Zulu|zoulou +zun|||Zuni|zuni +zxx|||No linguistic content; Not applicable|pas de contenu linguistique; non applicable +zza|||Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki|zaza; dimili; dimli; kirdki; kirmanjki; zazaki diff --git a/build.yaml b/build.yaml index f29f40d..8b87bb7 100644 --- a/build.yaml +++ b/build.yaml @@ -12,7 +12,7 @@ description: > category: "Metadata" artifacts: - "Jellyfin.Plugin.Tvdb.dll" - - "TvDbSharper.dll" + - "Tvdb.Sdk.dll" changelog: |- - Reduce log noise (#85) @IDisposable - Enriched Logging with series name (#84) @JPVenson