aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Naming/Common/NamingOptions.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json26
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs141
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs11
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs7
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs6
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs32
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.Model/Session/HardwareEncodingType.cs48
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs2
-rw-r--r--README.md12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs20
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json147
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs3
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs179
19 files changed, 541 insertions, 104 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index b44961bf8..c2d2aff34 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -212,3 +212,4 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
+ - [gnuyent](https://github.com/gnuyent)
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 5f125eb4f..915ce42cc 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -284,7 +284,7 @@ namespace Emby.Naming.Common
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
- new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
+ new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 8b753400e..0bde76e74 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
- "Shows": "Programmi",
+ "Shows": "Serie TV",
"Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index c6b904045..771c91d59 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -43,7 +43,7 @@
"NameInstallFailed": "{0} kurulumu başarısız",
"NameSeasonNumber": "Sezon {0}",
"NameSeasonUnknown": "Bilinmeyen Sezon",
- "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.",
+ "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
"NotificationOptionApplicationUpdateInstalled": "Uygulama güncellemesi yüklendi",
"NotificationOptionAudioPlayback": "Ses çalma başladı",
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
- "Sync": "Eşitle",
+ "Sync": "Eşzamanlama",
"System": "Sistem",
"TvShows": "Diziler",
"User": "Kullanıcı",
@@ -89,34 +89,36 @@
"UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
- "ValueHasBeenAddedToLibrary": "Medya kitaplığınıza {0} eklendi",
+ "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
- "VersionNumber": "Versiyon {0}",
+ "VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Geçici dosya klasörünü temizle",
"TasksChannelsCategory": "İnternet kanalları",
"TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
- "TasksMaintenanceCategory": "Onarım",
+ "TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
"TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
- "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.",
+ "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
"TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
"TaskUpdatePlugins": "Eklentileri Güncelle",
"TaskRefreshPeople": "Kullanıcıları Yenile",
- "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.",
- "TaskCleanLogs": "Log Dizinini Temizle",
- "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.",
+ "TaskCleanLogsDescription": "{0} günden eski günlük dosyalarını siler.",
+ "TaskCleanLogs": "Günlük Dizinini Temizle",
+ "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.",
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
- "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
- "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
+ "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",
+ "TaskCleanActivityLogDescription": "Yapılandırılan tarihten daha eski olan etkinlik günlüğü girişlerini siler.",
"Undefined": "Bilinmeyen",
"Default": "Varsayılan",
- "Forced": "Zorla"
+ "Forced": "Zorla",
+ "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.",
+ "TaskOptimizeDatabase": "Veritabanını optimize et"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index a9e3bfdb0..9808e47de 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -23,6 +23,9 @@ namespace Emby.Server.Implementations.Localization
public class LocalizationManager : ILocalizationManager
{
private const string DefaultCulture = "en-US";
+ private const string RatingsPath = "Emby.Server.Implementations.Localization.Ratings.";
+ private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
+ private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
@@ -58,43 +61,39 @@ namespace Emby.Server.Implementations.Localization
/// <returns><see cref="Task" />.</returns>
public async Task LoadAll()
{
- const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings.";
-
// Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames())
{
- if (!resource.StartsWith(RatingsResource, StringComparison.Ordinal))
+ if (!resource.StartsWith(RatingsPath, StringComparison.Ordinal))
{
continue;
}
- string countryCode = resource.Substring(RatingsResource.Length, 2);
+ string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
- using (var str = _assembly.GetManifestResourceStream(resource))
- using (var reader = new StreamReader(str))
+ await using var str = _assembly.GetManifestResourceStream(resource);
+ using var reader = new StreamReader(str);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ if (string.IsNullOrWhiteSpace(line))
{
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
+ continue;
+ }
+
+ string[] parts = line.Split(',');
+ if (parts.Length == 2
+ && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ var name = parts[0];
+ dict.Add(name, new ParentalRating(name, value));
+ }
#if DEBUG
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
- }
-#endif
+ else
+ {
+ _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
+#endif
}
_allParentalRatings[countryCode] = dict;
@@ -114,52 +113,48 @@ namespace Emby.Server.Implementations.Localization
{
List<CultureDto> list = new List<CultureDto>();
- const string ResourcePath = "Emby.Server.Implementations.Localization.iso6392.txt";
-
- using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
- using (var reader = new StreamReader(stream))
+ await using var stream = _assembly.GetManifestResourceStream(CulturesPath);
+ using var reader = new StreamReader(stream);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ if (string.IsNullOrWhiteSpace(line))
{
- if (string.IsNullOrWhiteSpace(line))
+ continue;
+ }
+
+ var parts = line.Split('|');
+
+ if (parts.Length == 5)
+ {
+ string name = parts[3];
+ if (string.IsNullOrWhiteSpace(name))
{
continue;
}
- var parts = line.Split('|');
+ string twoCharName = parts[2];
+ if (string.IsNullOrWhiteSpace(twoCharName))
+ {
+ continue;
+ }
- if (parts.Length == 5)
+ string[] threeletterNames;
+ if (string.IsNullOrWhiteSpace(parts[1]))
+ {
+ threeletterNames = new[] { parts[0] };
+ }
+ else
{
- 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] };
- }
-
- list.Add(new CultureDto
- {
- DisplayName = name,
- Name = name,
- ThreeLetterISOLanguageNames = threeletterNames,
- TwoLetterISOLanguageName = twoCharName
- });
+ threeletterNames = new[] { parts[0], parts[1] };
}
+
+ list.Add(new CultureDto
+ {
+ DisplayName = name,
+ Name = name,
+ ThreeLetterISOLanguageNames = threeletterNames,
+ TwoLetterISOLanguageName = twoCharName
+ });
}
}
@@ -188,7 +183,7 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
{
- using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
+ using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream(CountriesPath));
return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
}
@@ -350,23 +345,21 @@ namespace Emby.Server.Implementations.Localization
private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
{
- using (var stream = _assembly.GetManifestResourceStream(resourcePath))
+ await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+ // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
+ if (stream != null)
{
- // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
- if (stream != null)
- {
- var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
- foreach (var key in dict.Keys)
- {
- dictionary[key] = dict[key];
- }
- }
- else
+ foreach (var key in dict.Keys)
{
- _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+ dictionary[key] = dict[key];
}
}
+ else
+ {
+ _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+ }
}
private static string GetResourceFilename(string culture)
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c295af7eb..05fa5b135 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -380,7 +380,7 @@ namespace Jellyfin.Api.Helpers
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
var directory = Path.GetDirectoryName(outputFilePath)
- ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
var name = Path.GetFileNameWithoutExtension(outputFilePath);
@@ -444,6 +444,10 @@ namespace Jellyfin.Api.Helpers
{
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
+ var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+ HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString)
+ ? null
+ : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true);
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
@@ -458,6 +462,7 @@ namespace Jellyfin.Api.Helpers
AudioChannels = state.OutputAudioChannels,
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+ HardwareAccelerationType = hardwareAccelerationType,
TranscodeReasons = state.TranscodeReasons
});
}
@@ -759,8 +764,8 @@ namespace Jellyfin.Api.Helpers
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
{
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
- new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
- cancellationTokenSource.Token)
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
.ConfigureAwait(false);
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 2eef223e5..3e5982eed 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -58,9 +58,12 @@ namespace Jellyfin.Server.Middleware
return;
}
- if (!startsWithBaseUrl)
+ if (!startsWithBaseUrl
+ || localPath.Length == baseUrlPrefix.Length
+ // Local path is /baseUrl/
+ || (localPath.Length == baseUrlPrefix.Length + 1 && localPath[^1] == '/'))
{
- // Always redirect back to the default path if the base prefix is invalid or missing
+ // Always redirect back to the default path if the base prefix is invalid, missing, or is the full path.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
return;
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 6bc39d6f4..b38ee1146 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -26,6 +26,12 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
+ /// <typeparam name="T">The type of data.</typeparam>
+ /// <param name="name">Name of message type.</param>
+ /// <param name="messageId">Message ID.</param>
+ /// <param name="data">Data to send.</param>
+ /// <param name="cancellationToken">CancellationToken for operation.</param>
+ /// <returns>A task.</returns>
Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 4c3cf5ffe..0ff32fb53 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -83,6 +83,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
+ /// <returns>Session information.</returns>
SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
/// <summary>
@@ -105,7 +106,7 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackProgress(PlaybackProgressInfo info);
Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated);
@@ -115,14 +116,13 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackStopped(PlaybackStopInfo info);
/// <summary>
/// Reports the session ended.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
- /// <returns>Task.</returns>
void ReportSessionEnded(string sessionId);
/// <summary>
@@ -170,6 +170,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="session">The session.</param>
/// <param name="command">The group update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <typeparam name="T">Type of group.</typeparam>
/// <returns>Task.</returns>
Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken);
@@ -196,8 +197,8 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message to admin sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="name">The name.</param>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -206,18 +207,31 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message to user sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
+ /// <summary>
+ /// Sends the message to user sessions.
+ /// </summary>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="dataFn">Data function.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user device sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
/// <param name="deviceId">The device identifier.</param>
- /// <param name="name">The name.</param>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -353,6 +367,8 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Revokes the user tokens.
/// </summary>
+ /// <param name="userId">User ID.</param>
+ /// <param name="currentAccessToken">Current access token.</param>
void RevokeUserTokens(Guid userId, string currentAccessToken);
/// <summary>
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 411b7c82b..6da9886a4 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -26,7 +26,7 @@
<PackageReference Include="libse" Version="3.6.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
- <PackageReference Include="UTF.Unknown" Version="2.3.0" />
+ <PackageReference Include="UTF.Unknown" Version="2.4.0" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index d0a76c4ca..9196fe139 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -63,7 +63,8 @@ namespace MediaBrowser.MediaEncoding.Probing
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
{
if (tags.TryGetValue(key, out var val)
- && DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var dateTime))
+ && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var dateTime)
+ || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out dateTime)))
{
return dateTime.ToUniversalTime();
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index c9ad3c41e..875ee6f04 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -121,6 +121,7 @@ namespace MediaBrowser.MediaEncoding.Probing
// Several different forms of retail/premiere date
info.PremiereDate =
+ FFProbeHelpers.GetDictionaryDateTime(tags, "originaldate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs
new file mode 100644
index 000000000..0e172f35f
--- /dev/null
+++ b/MediaBrowser.Model/Session/HardwareEncodingType.cs
@@ -0,0 +1,48 @@
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Enum HardwareEncodingType.
+ /// </summary>
+ public enum HardwareEncodingType
+ {
+ /// <summary>
+ /// AMD AMF
+ /// </summary>
+ AMF = 0,
+
+ /// <summary>
+ /// Intel Quick Sync Video
+ /// </summary>
+ QSV = 1,
+
+ /// <summary>
+ /// NVIDIA NVENC
+ /// </summary>
+ NVENC = 2,
+
+ /// <summary>
+ /// OpenMax OMX
+ /// </summary>
+ OMX = 3,
+
+ /// <summary>
+ /// Exynos V4L2 MFC
+ /// </summary>
+ V4L2M2M = 4,
+
+ /// <summary>
+ /// MediaCodec Android
+ /// </summary>
+ MediaCodec = 5,
+
+ /// <summary>
+ /// Video Acceleration API (VAAPI)
+ /// </summary>
+ VAAPI = 6,
+
+ /// <summary>
+ /// Video ToolBox
+ /// </summary>
+ VideoToolBox = 7
+ }
+}
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index 064a087d5..68ab691f8 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -34,6 +34,8 @@ namespace MediaBrowser.Model.Session
public int? AudioChannels { get; set; }
+ public HardwareEncodingType? HardwareAccelerationType { get; set; }
+
public TranscodeReason[] TranscodeReasons { get; set; }
}
}
diff --git a/README.md b/README.md
index 6859a8a76..3259777c8 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,8 @@ Check out our <a href="https://translate.jellyfin.org">Weblate instance</a> to h
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/>
</a>
+---
+
## Jellyfin Server
This repository contains the code for Jellyfin's backend server. Note that this is only one of many projects under the Jellyfin GitHub [organization](https://github.com/jellyfin/) on GitHub. If you want to contribute, you can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on.
@@ -162,3 +164,13 @@ switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
**NOTE:** The setup wizard can not be run if the web client is hosted separately.
+
+---
+<p align="center">
+This project is supported by:
+<br/>
+<br/>
+<a href="https://www.digitalocean.com"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50px" alt="DigitalOcean"></a>
+ &nbsp;
+<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/fa104b7d73f759d7262794b94569f1b89df41c0b/jetbrains.svg" height="50px" alt="JetBrains logo"></a>
+</p>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index d8089eea2..59037c263 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -71,5 +71,25 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.True(res.PremiereDate.HasValue);
Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
}
+
+ [Fact]
+ public void GetMediaInfo_GivenOriginalDateContainsOnlyYear_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/music_year_only_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, null, true, "Test Data/Probing/music.flac", MediaProtocol.File);
+
+ Assert.Equal("Baker Street", res.Name);
+ Assert.Single(res.Artists);
+ Assert.Equal("Gerry Rafferty", res.Artists[0]);
+ Assert.Equal("City to City", res.Album);
+ Assert.Equal(1978, res.ProductionYear);
+ Assert.True(res.PremiereDate.HasValue);
+ Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Contains("Electronic", res.Genres);
+ Assert.Contains("Ambient", res.Genres);
+ Assert.Contains("Pop", res.Genres);
+ Assert.Contains("Jazz", res.Genres);
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json
new file mode 100644
index 000000000..ddf890c45
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json
@@ -0,0 +1,147 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "flac",
+ "codec_long_name": "FLAC (Free Lossless Audio Codec)",
+ "codec_type": "audio",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "s16",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/44100",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 16394616,
+ "duration": "371.760000",
+ "bits_per_raw_sample": "16",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "mjpeg",
+ "codec_long_name": "Motion JPEG",
+ "profile": "Baseline",
+ "codec_type": "video",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 500,
+ "height": 498,
+ "coded_width": 500,
+ "coded_height": 498,
+ "closed_captions": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "250:249",
+ "pix_fmt": "yuvj420p",
+ "level": -99,
+ "color_range": "pc",
+ "color_space": "bt470bg",
+ "chroma_location": "center",
+ "refs": 1,
+ "r_frame_rate": "90000/1",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 33458400,
+ "duration": "371.760000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 1,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "comment": "Cover (front)"
+ }
+ }
+ ],
+ "format": {
+ "filename": "02 Baker Street.flac",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "flac",
+ "format_long_name": "raw FLAC",
+ "start_time": "0.000000",
+ "duration": "371.760000",
+ "size": "37072649",
+ "bit_rate": "797775",
+ "probe_score": 100,
+ "tags": {
+ "MUSICBRAINZ_RELEASEGROUPID": "238c3fb4-5792-342b-b217-02f66298b424",
+ "ORIGINALDATE": "1978",
+ "ORIGINALYEAR": "1978",
+ "RELEASETYPE": "album",
+ "MUSICBRAINZ_ALBUMID": "30156786-e511-3106-ac95-66f0e880b24b",
+ "ASIN": "B000007O5H",
+ "MUSICBRAINZ_ALBUMARTISTID": "563201cb-721c-4cfb-acca-c1ba69e3d1fb",
+ "album_artist": "Gerry Rafferty",
+ "ALBUMARTISTSORT": "Rafferty, Gerry",
+ "LABEL": "Liberty EMI Records UK",
+ "CATALOGNUMBER": "CDP 7 46049 2",
+ "DATE": "1989-07-26",
+ "RELEASECOUNTRY": "GB",
+ "BARCODE": "077774604925",
+ "ALBUM": "City to City",
+ "SCRIPT": "Latn",
+ "RELEASESTATUS": "official",
+ "TOTALDISCS": "1",
+ "disc": "1",
+ "MEDIA": "CD",
+ "TOTALTRACKS": "10",
+ "MUSICBRAINZ_TRACKID": "9235e22e-afbd-48f7-b329-21dae6da2810",
+ "ISRC": "GBAYE1100924;GBAYE7800619",
+ "PERFORMER": "Hugh Burns (electric guitar);Nigel Jenkins (electric guitar);Tommy Eyre (keyboard and Moog);Glen LeFleur (percussion);Raphael Ravenscroft (saxophone);Henry Spinetti (drums (drum set));Gary Taylor (bass);Gerry Rafferty (lead vocals)",
+ "ARRANGER": "Graham Preskett",
+ "MIXER": "Declan O’Doherty",
+ "PRODUCER": "Hugh Murphy;Gerry Rafferty",
+ "MUSICBRAINZ_WORKID": "a9eb3c45-784c-3c32-860c-4b406f03961b",
+ "LANGUAGE": "eng",
+ "WORK": "Baker Street",
+ "COMPOSER": "Gerry Rafferty",
+ "COMPOSERSORT": "Rafferty, Gerry",
+ "LYRICIST": "Gerry Rafferty",
+ "TITLE": "Baker Street",
+ "MUSICBRAINZ_ARTISTID": "563201cb-721c-4cfb-acca-c1ba69e3d1fb",
+ "ARTIST": "Gerry Rafferty",
+ "ARTISTSORT": "Rafferty, Gerry",
+ "ARTISTS": "Gerry Rafferty",
+ "MUSICBRAINZ_RELEASETRACKID": "407cf7f7-440d-3e76-8b89-8686198868ea",
+ "track": "2",
+ "GENRE": "Electronic;Ambient;Pop;Jazz",
+ "WEBSITE": "http://www.gerryrafferty.com/",
+ "ACOUSTID_ID": "68f8d979-a659-4aa0-a216-eb3721a951eb",
+ "MOOD": "Acoustic;Not aggressive;Not electronic;Not happy;Party;Relaxed;Not sad",
+ "TRACKTOTAL": "10",
+ "DISCTOTAL": "1"
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
index 89579c037..6d49ac832 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -21,7 +21,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
[InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
[InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
- // TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
+ [InlineData("[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken/[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken - 12 (NVENC H.265 1080p).mkv", "Tensura Nikki - Tensei Shitara Slime Datta Ken", null, 12)]
+ [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
// TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)]
// TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)]
public void TestSimple(string path, string seriesName, int? seasonNumber, int? episodeNumber)
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
new file mode 100644
index 000000000..edd4b1e55
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Localization;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Localization
+{
+ public class LocalizationManagerTests
+ {
+ [Fact]
+ public void GetCountries_All_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ var countries = localizationManager.GetCountries().ToList();
+
+ Assert.Equal(139, countries.Count);
+
+ var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
+ Assert.NotNull(germany);
+ Assert.Equal("Germany", germany!.DisplayName);
+ Assert.Equal("DEU", germany.ThreeLetterISORegionName);
+ Assert.Equal("DE", germany.TwoLetterISORegionName);
+ }
+
+ [Fact]
+ public async Task GetCultures_All_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ var cultures = localizationManager.GetCultures().ToList();
+
+ Assert.Equal(189, cultures.Count);
+
+ var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
+ Assert.NotNull(germany);
+ Assert.Equal("ger", germany!.ThreeLetterISOLanguageName);
+ Assert.Equal("German", germany.DisplayName);
+ Assert.Equal("German", germany.Name);
+ Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
+ Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
+ }
+
+ [Theory]
+ [InlineData("de")]
+ [InlineData("ger")]
+ [InlineData("german")]
+ public async Task FindLanguageInfo_Valid_Success(string identifier)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+
+ var germany = localizationManager.FindLanguageInfo(identifier);
+ Assert.NotNull(germany);
+
+ Assert.Equal("ger", germany.ThreeLetterISOLanguageName);
+ Assert.Equal("German", germany.DisplayName);
+ Assert.Equal("German", germany.Name);
+ Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
+ Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
+ }
+
+ [Fact]
+ public async Task GetParentalRatings_Default_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ var ratings = localizationManager.GetParentalRatings().ToList();
+
+ Assert.Equal(23, ratings.Count);
+
+ var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
+ Assert.NotNull(tvma);
+ Assert.Equal(9, tvma!.Value);
+ }
+
+ [Fact]
+ public async Task GetParentalRatings_ConfiguredCountryCode_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ MetadataCountryCode = "DE"
+ });
+ await localizationManager.LoadAll();
+ var ratings = localizationManager.GetParentalRatings().ToList();
+
+ Assert.Equal(10, ratings.Count);
+
+ var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
+ Assert.NotNull(fsk);
+ Assert.Equal(7, fsk!.Value);
+ }
+
+ [Theory]
+ [InlineData("CA-R", "CA", 10)]
+ [InlineData("FSK-16", "DE", 8)]
+ [InlineData("FSK-18", "DE", 9)]
+ [InlineData("FSK-18", "US", 9)]
+ [InlineData("TV-MA", "US", 9)]
+ [InlineData("XXX", "asdf", 100)]
+ [InlineData("Germany: FSK-18", "DE", 9)]
+ public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ MetadataCountryCode = countryCode
+ });
+ await localizationManager.LoadAll();
+ var level = localizationManager.GetRatingLevel(value);
+ Assert.NotNull(level);
+ Assert.Equal(expectedLevel, level!);
+ }
+
+ [Fact]
+ public async Task GetRatingLevel_GivenUnratedString_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ Assert.Null(localizationManager.GetRatingLevel("n/a"));
+ }
+
+ [Theory]
+ [InlineData("Default", "Default")]
+ [InlineData("HeaderLiveTV", "Live TV")]
+ public void GetLocalizedString_Valid_Success(string key, string expected)
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "en-US"
+ });
+
+ var translated = localizationManager.GetLocalizedString(key);
+ Assert.NotNull(translated);
+ Assert.Equal(expected, translated);
+ }
+
+ [Fact]
+ public void GetLocalizedString_Invalid_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "en-US"
+ });
+
+ var key = "SuperInvalidTranslationKeyThatWillNeverBeAdded";
+
+ var translated = localizationManager.GetLocalizedString(key);
+ Assert.NotNull(translated);
+ Assert.Equal(key, translated);
+ }
+
+ private LocalizationManager Setup(ServerConfiguration config)
+ {
+ var mockConfiguration = new Mock<IServerConfigurationManager>();
+ mockConfiguration.SetupGet(x => x.Configuration).Returns(config);
+
+ return new LocalizationManager(mockConfiguration.Object, new NullLogger<LocalizationManager>());
+ }
+ }
+}