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