diff --git a/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs
index 4c9a2f2..6bddd64 100644
--- a/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs
+++ b/Jellyfin.Plugin.Tvdb/Configuration/PluginConfiguration.cs
@@ -55,6 +55,11 @@ public int MetadataUpdateInHours
///
public string FallbackLanguages { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value indicating whether to import season name.
+ ///
+ public bool ImportSeasonName { get; set; } = false;
+
///
/// Gets or sets a value indicating whether to include missing specials.
///
diff --git a/Jellyfin.Plugin.Tvdb/Configuration/config.html b/Jellyfin.Plugin.Tvdb/Configuration/config.html
index 43ce888..a2f5a64 100644
--- a/Jellyfin.Plugin.Tvdb/Configuration/config.html
+++ b/Jellyfin.Plugin.Tvdb/Configuration/config.html
@@ -49,6 +49,10 @@
TheTVDB Settings:
If the preferred metadata language is not available, the plugin will attempt to retrieve metadata in these languages (in the order listed). Separate language codes with commas (e.g., en, fr, de, ja).
+
+
+ Import season name from provider.
+
Include missing specials
@@ -80,6 +84,7 @@ TheTVDB Settings:
document.getElementById('cacheDurationInDays').value = config.CacheDurationInDays;
document.getElementById('metadataUpdateInHours').value = config.MetadataUpdateInHours;
document.getElementById('fallbackLanguages').value = config.FallbackLanguages;
+ document.getElementById('importSeasonName').checked = config.ImportSeasonName
document.getElementById('includeMissingSpecials').checked = config.IncludeMissingSpecials;
document.getElementById('fallbackToOriginalLanguage').checked = config.FallbackToOriginalLanguage;
Dashboard.hideLoadingMsg();
@@ -95,6 +100,7 @@ TheTVDB Settings:
config.CacheDurationInDays = document.getElementById('cacheDurationInDays').value;
config.MetadataUpdateInHours = document.getElementById('metadataUpdateInHours').value;
config.FallbackLanguages = document.getElementById('fallbackLanguages').value;
+ config.ImportSeasonName = document.getElementById('importSeasonName').checked;
config.IncludeMissingSpecials = document.getElementById('includeMissingSpecials').checked;
config.FallbackToOriginalLanguage = document.getElementById('fallbackToOriginalLanguage').checked;
ApiClient.updatePluginConfiguration(TvdbPluginConfiguration.uniquePluginId, config).then(function (result) {
diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonProvider.cs
new file mode 100644
index 0000000..826fefc
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonProvider.cs
@@ -0,0 +1,154 @@
+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.Plugin.Tvdb.SeasonClient;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using Microsoft.Extensions.Logging;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.Providers
+{
+ ///
+ /// The Tvdb Season Provider.
+ ///
+ public class TvdbSeasonProvider : IRemoteMetadataProvider
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly TvdbClientManager _tvdbClientManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of .
+ /// Instance of .
+ public TvdbSeasonProvider(IHttpClientFactory httpClientFactory, ILogger logger, ILibraryManager libraryManager, TvdbClientManager tvdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = logger;
+ _tvdbClientManager = tvdbClientManager;
+ _libraryManager = libraryManager;
+ }
+
+ ///
+ public string Name => TvdbPlugin.ProviderName;
+
+ private static bool ImportSeasonName => TvdbPlugin.Instance?.Configuration.ImportSeasonName ?? false;
+
+ ///
+ public async Task> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
+ {
+ if (info.IndexNumber == null || !info.SeriesProviderIds.IsSupported())
+ {
+ _logger.LogDebug("No series identity found for {EpisodeName}", info.Name);
+ return new MetadataResult
+ {
+ QueriedById = true
+ };
+ }
+
+ int? seasonId = info.GetTvdbId();
+ string displayOrder;
+
+ // If the seasonId is 0, then we have to get the series metadata to get display order and then get the seasonId.
+ // If is automated is true, means that display order has changed,do the same as above.
+ if (seasonId == 0 || info.IsAutomated)
+ {
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var seriesId);
+ if (string.IsNullOrWhiteSpace(seriesId))
+ {
+ _logger.LogDebug("No series identity found for {EpisodeName}", info.Name);
+ return new MetadataResult
+ {
+ QueriedById = true
+ };
+ }
+
+ var query = new InternalItemsQuery()
+ {
+ HasAnyProviderId = new Dictionary
+ {
+ { MetadataProvider.Tvdb.ToString(), seriesId }
+ }
+ };
+
+ var series = _libraryManager.GetItemList(query).OfType().FirstOrDefault();
+ displayOrder = series!.DisplayOrder;
+ if (string.IsNullOrWhiteSpace(displayOrder))
+ {
+ displayOrder = "official";
+ }
+
+ var seriesInfo = await _tvdbClientManager.GetSeriesExtendedByIdAsync(series.GetTvdbId(), string.Empty, cancellationToken, small: true)
+ .ConfigureAwait(false);
+ seasonId = seriesInfo.Seasons.FirstOrDefault(s => s.Number == info.IndexNumber && string.Equals(s.Type.Type, displayOrder, StringComparison.OrdinalIgnoreCase))?.Id;
+
+ if (seasonId == null)
+ {
+ _logger.LogDebug("No season identity found for {SeasonName}", info.Name);
+ return new MetadataResult
+ {
+ QueriedById = true
+ };
+ }
+ }
+
+ var seasonInfo = await _tvdbClientManager.GetSeasonByIdAsync(seasonId ?? 0, string.Empty, cancellationToken)
+ .ConfigureAwait(false);
+
+ return MapSeasonToResult(info, seasonInfo);
+ }
+
+ private MetadataResult MapSeasonToResult(SeasonInfo id, CustomSeasonExtendedRecord season)
+ {
+ var result = new MetadataResult
+ {
+ HasMetadata = true,
+ Item = new Season
+ {
+ IndexNumber = id.IndexNumber,
+ // Tvdb uses 3 letter code for language (prob ISO 639-2)
+ // Reverts to OriginalName if no translation is found
+ Overview = season.Translations.GetTranslatedOverviewOrDefault(id.MetadataLanguage),
+ }
+ };
+
+ var item = result.Item;
+ item.SetTvdbId(season.Id);
+
+ if (ImportSeasonName)
+ {
+ item.Name = season.Translations.GetTranslatedNamedOrDefaultIgnoreAliasProperty(id.MetadataLanguage) ?? TvdbUtils.ReturnOriginalLanguageOrDefault(season.Name);
+ item.OriginalTitle = season.Name;
+ }
+
+ return result;
+ }
+
+ ///
+ public Task> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public Task GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/SeasonClient/CustomSeasonExtendedRecord.cs b/Jellyfin.Plugin.Tvdb/SeasonClient/CustomSeasonExtendedRecord.cs
new file mode 100644
index 0000000..2cbfbf6
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/SeasonClient/CustomSeasonExtendedRecord.cs
@@ -0,0 +1,71 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System.Text.Json.Serialization;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.SeasonClient
+{
+ public sealed class CustomSeasonExtendedRecord
+ {
+ private System.Collections.Generic.IDictionary _additionalProperties = default!;
+
+ [JsonPropertyName("artwork")]
+ public System.Collections.Generic.IReadOnlyList Artwork { get; set; } = default!;
+
+ [JsonPropertyName("companies")]
+ public Companies Companies { get; set; } = default!;
+
+ [JsonPropertyName("episodes")]
+ public System.Collections.Generic.IReadOnlyList Episodes { get; set; } = default!;
+
+ [JsonPropertyName("id")]
+ public int? Id { get; set; } = default!;
+
+ [JsonPropertyName("image")]
+ public string Image { get; set; } = default!;
+
+ [JsonPropertyName("imageType")]
+ public int? ImageType { get; set; }
+
+ [JsonPropertyName("lastUpdated")]
+ public string LastUpdated { get; set; } = default!;
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = default!;
+
+ [JsonPropertyName("nameTranslations")]
+ public System.Collections.Generic.IReadOnlyList NameTranslations { get; set; } = default!;
+
+ [JsonPropertyName("number")]
+ public long? Number { get; set; }
+
+ [JsonPropertyName("overviewTranslations")]
+ public System.Collections.Generic.IReadOnlyList OverviewTranslations { get; set; } = default!;
+
+ [JsonPropertyName("seriesId")]
+ public long? SeriesId { get; set; }
+
+ [JsonPropertyName("trailers")]
+ public System.Collections.Generic.IReadOnlyList Trailers { get; set; } = default!;
+
+ [JsonPropertyName("type")]
+ public SeasonType Type { get; set; } = default!;
+
+ [JsonPropertyName("tagOptions")]
+ public System.Collections.Generic.IReadOnlyList TagOptions { get; set; } = default!;
+
+ [JsonPropertyName("translations")]
+ public TranslationExtended Translations { get; set; } = default!;
+
+ [JsonPropertyName("year")]
+ public string Year { get; set; } = default!;
+
+ [JsonExtensionData]
+ public System.Collections.Generic.IDictionary AdditionalProperties
+ {
+ get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); }
+ set { _additionalProperties = value; }
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/SeasonClient/ExtendedSeasonClient.cs b/Jellyfin.Plugin.Tvdb/SeasonClient/ExtendedSeasonClient.cs
new file mode 100644
index 0000000..b9bb5b4
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/SeasonClient/ExtendedSeasonClient.cs
@@ -0,0 +1,179 @@
+#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods
+
+using System.Text.Json.Serialization;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.SeasonClient
+{
+ ///
+ /// Extended season client.
+ ///
+ public sealed partial class ExtendedSeasonClient : SeasonsClient, IExtendedSeasonClient
+ {
+ private System.Net.Http.HttpClient _httpClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of .
+ /// Instance of .
+ public ExtendedSeasonClient(SdkClientSettings configuration, System.Net.Http.HttpClient httpClient) : base(configuration, httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ ///
+ public async System.Threading.Tasks.Task GetSeasonExtendedWithTranslationsAsync(double id, System.Threading.CancellationToken cancellationToken = default)
+ {
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+
+ // Operation Path: "seasons/{id}/extended"
+ urlBuilder_.Append("seasons/");
+ urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture)));
+ urlBuilder_.Append("/extended?meta=translations");
+
+ await PrepareRequestAsync(client_, request_, urlBuilder_, cancellationToken).ConfigureAwait(false);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ await PrepareRequestAsync(client_, request_, url_, cancellationToken).ConfigureAwait(false);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = new System.Collections.Generic.Dictionary>();
+ foreach (var item_ in response_.Headers)
+ {
+ headers_[item_.Key] = item_.Value;
+ }
+
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ {
+ headers_[item_.Key] = item_.Value;
+ }
+ }
+
+ await ProcessResponseAsync(client_, response_, cancellationToken).ConfigureAwait(false);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new SeasonsException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+
+ return objectResponse_.Object;
+ }
+ else
+ if (status_ == 400)
+ {
+ var responseText_ = response_.Content == null ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new SeasonsException("Invalid seasons id", status_, responseText_, headers_, null);
+ }
+ else
+ if (status_ == 401)
+ {
+ var responseText_ = response_.Content == null ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new SeasonsException("Unauthorized", status_, responseText_, headers_, null);
+ }
+ else
+ if (status_ == 404)
+ {
+ var responseText_ = response_.Content == null ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new SeasonsException("Season not found", status_, responseText_, headers_, null);
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new SeasonsException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ {
+ response_.Dispose();
+ }
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ {
+ client_.Dispose();
+ }
+ }
+ }
+
+ private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
+ {
+ if (value == null)
+ {
+ return string.Empty;
+ }
+
+ if (value is System.Enum)
+ {
+ var name = System.Enum.GetName(value.GetType(), value);
+ if (name != null)
+ {
+ var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
+ if (field != null)
+ {
+ var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute))
+ as System.Runtime.Serialization.EnumMemberAttribute;
+ if (attribute != null)
+ {
+ return attribute.Value != null ? attribute.Value : name;
+ }
+ }
+
+ var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo), cultureInfo);
+ return converted == null ? string.Empty : converted;
+ }
+ }
+ else if (value is bool)
+ {
+ return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
+ }
+ else if (value is byte[])
+ {
+ return System.Convert.ToBase64String((byte[])value);
+ }
+ else if (value is string[])
+ {
+ return string.Join(",", (string[])value);
+ }
+ else if (value.GetType().IsArray)
+ {
+ var valueArray = (System.Array)value;
+ var valueTextArray = new string[valueArray.Length];
+ for (var i = 0; i < valueArray.Length; i++)
+ {
+ valueTextArray[i] = ConvertToString(valueArray.GetValue(i)!, cultureInfo);
+ }
+
+ return string.Join(",", valueTextArray);
+ }
+
+ var result = System.Convert.ToString(value, cultureInfo);
+ return result == null ? string.Empty : result;
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/SeasonClient/IExtendedSeasonClient.cs b/Jellyfin.Plugin.Tvdb/SeasonClient/IExtendedSeasonClient.cs
new file mode 100644
index 0000000..1060aee
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/SeasonClient/IExtendedSeasonClient.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Jellyfin.Plugin.Tvdb.SeasonClient
+{
+ ///
+ /// Interface IExtendedSeasonClient.
+ ///
+ public interface IExtendedSeasonClient
+ {
+ ///
+ /// Gets the season extended with translations.
+ ///
+ /// Season Id.
+ /// Cancellation token.
+ /// response.
+ System.Threading.Tasks.Task GetSeasonExtendedWithTranslationsAsync(double id, System.Threading.CancellationToken cancellationToken = default);
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/SeasonClient/Response99.cs b/Jellyfin.Plugin.Tvdb/SeasonClient/Response99.cs
new file mode 100644
index 0000000..f7b7383
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/SeasonClient/Response99.cs
@@ -0,0 +1,26 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System.Text.Json.Serialization;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.SeasonClient
+{
+ public sealed class Response99
+ {
+ private System.Collections.Generic.IDictionary _additionalProperties = default!;
+
+ [JsonPropertyName("data")]
+ public CustomSeasonExtendedRecord Data { get; set; } = default!;
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; } = default!;
+
+ [JsonExtensionData]
+ public System.Collections.Generic.IDictionary AdditionalProperties
+ {
+ get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); }
+ set { _additionalProperties = value; }
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
index b7faa57..edfdfca 100644
--- a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
+++ b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
@@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.Tvdb.Configuration;
+using Jellyfin.Plugin.Tvdb.SeasonClient;
using MediaBrowser.Common;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Globalization;
@@ -296,21 +297,21 @@ public async Task GetSeriesEpisodesAsync(
/// Metadata language.
/// Cancellation token.
/// The episode record.
- public async Task GetSeasonByIdAsync(
+ public async Task GetSeasonByIdAsync(
int seasonTvdbId,
string language,
CancellationToken cancellationToken)
{
var key = $"TvdbSeason_{seasonTvdbId.ToString(CultureInfo.InvariantCulture)}";
- if (_memoryCache.TryGetValue(key, out SeasonExtendedRecord? season)
+ if (_memoryCache.TryGetValue(key, out CustomSeasonExtendedRecord? season)
&& season is not null)
{
return season;
}
- var seasonClient = _serviceProvider.GetRequiredService();
+ var seasonClient = _serviceProvider.GetRequiredService();
await LoginAsync().ConfigureAwait(false);
- var seasonResult = await seasonClient.GetSeasonExtendedAsync(id: seasonTvdbId, cancellationToken: cancellationToken)
+ var seasonResult = await seasonClient.GetSeasonExtendedWithTranslationsAsync(id: seasonTvdbId, cancellationToken: cancellationToken)
.ConfigureAwait(false);
_memoryCache.Set(key, seasonResult.Data, TimeSpan.FromHours(CacheDurationInHours));
return seasonResult.Data;
@@ -689,7 +690,7 @@ private ServiceProvider ConfigureService(IApplicationHost applicationHost)
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 ExtendedSeasonClient(_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)));
diff --git a/Jellyfin.Plugin.Tvdb/TvdbSdkExtensions.cs b/Jellyfin.Plugin.Tvdb/TvdbSdkExtensions.cs
index 73ab5d1..0962baf 100644
--- a/Jellyfin.Plugin.Tvdb/TvdbSdkExtensions.cs
+++ b/Jellyfin.Plugin.Tvdb/TvdbSdkExtensions.cs
@@ -42,6 +42,26 @@ public static class TvdbSdkExtensions
.FirstOrDefault(name => name != null);
}
+ ///
+ /// Get the translated Name, or .
+ ///
+ /// Available translations.
+ /// Requested language.
+ /// Translated Name, or .
+ public static string? GetTranslatedNamedOrDefaultIgnoreAliasProperty(this TranslationExtended? translations, string? language)
+ {
+ return translations?
+ .NameTranslations?
+ .FirstOrDefault(translation => IsMatch(translation.Language, language))?
+ .Name
+ ?? FallbackLanguages?
+ .Select(lang => translations?
+ .NameTranslations?
+ .FirstOrDefault(translation => IsMatch(translation.Language, lang))?
+ .Name)
+ .FirstOrDefault(name => name != null);
+ }
+
///
/// Get the translated Name, or .
///