Add opt-in FFmpeg continuity slates with per-viewer localization
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Enigma2;
|
||||
|
||||
/// <summary>One catalogue for web status, native messages and cached FFmpeg slates.</summary>
|
||||
public sealed class StartupMessages
|
||||
{
|
||||
private static readonly Dictionary<string, Dictionary<string, string>> Catalogue = Load();
|
||||
private readonly IServerConfigurationManager configuration;
|
||||
private readonly IDisplayPreferencesManager preferences;
|
||||
private readonly ConcurrentDictionary<(Guid User, string Device), (string Language, DateTime Seen)> clientLanguages = new();
|
||||
public StartupMessages(IServerConfigurationManager configuration = null, IDisplayPreferencesManager preferences = null)
|
||||
{ this.configuration = configuration; this.preferences = preferences; }
|
||||
private static Dictionary<string, Dictionary<string, string>> Load()
|
||||
{
|
||||
using var stream = typeof(StartupMessages).Assembly.GetManifestResourceStream("Jellyfin.Plugin.Enigma2.Localization.startup-messages.json");
|
||||
return JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(stream);
|
||||
}
|
||||
private static string Supported(string locale)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(locale) || locale.Length > 64) return null;
|
||||
var language = locale.Trim().Replace('_', '-').Split('-')[0].ToLowerInvariant();
|
||||
return Catalogue.ContainsKey(language) ? language : null;
|
||||
}
|
||||
public static string ResolveLanguage(string uiLanguage, string acceptLanguage, string serverLanguage)
|
||||
{
|
||||
var direct = Supported(uiLanguage);
|
||||
if (direct != null) return direct;
|
||||
// Honour quality ordering and q=0 exclusions; headers are untrusted hints,
|
||||
// never file names or a reason to change metadata/audio preferences.
|
||||
if (!string.IsNullOrEmpty(acceptLanguage) && acceptLanguage.Length <= 1024)
|
||||
{
|
||||
var candidates = acceptLanguage.Split(',').Take(32).Select((entry, order) =>
|
||||
{
|
||||
var parts = entry.Trim().Split(';'); var quality = 1.0;
|
||||
foreach (var part in parts.Skip(1))
|
||||
if (part.Trim().StartsWith("q=", StringComparison.OrdinalIgnoreCase)
|
||||
&& (!double.TryParse(part.Trim().Substring(2), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out quality)
|
||||
|| quality < 0 || quality > 1)) quality = 0;
|
||||
return new { Language = Supported(parts[0]), Quality = quality, Order = order };
|
||||
}).Where(x => x.Language != null && x.Quality > 0).OrderByDescending(x => x.Quality).ThenBy(x => x.Order);
|
||||
var match = candidates.FirstOrDefault(); if (match != null) return match.Language;
|
||||
}
|
||||
return Supported(serverLanguage) ?? "en";
|
||||
}
|
||||
public string Resolve(string uiLanguage, string acceptLanguage) => ResolveLanguage(uiLanguage, acceptLanguage, configuration?.Configuration?.UICulture);
|
||||
public string Resolve(Guid user, string uiLanguage, string acceptLanguage)
|
||||
{
|
||||
var direct = Supported(uiLanguage);
|
||||
if (direct != null) return direct;
|
||||
// Jellyfin Web uses usersettings/emby display preferences. Some clients
|
||||
// save language there; current Web stores it locally. Read only the
|
||||
// requesting user's preference, without creating or changing it.
|
||||
try
|
||||
{
|
||||
var custom = preferences?.ListCustomItemDisplayPreferences(user, "usersettings".GetMD5(), "emby");
|
||||
if (custom != null && custom.TryGetValue("language", out var value) && Supported(value) is { } saved) return saved;
|
||||
}
|
||||
catch { /* An optional preference lookup must not prevent playback. */ }
|
||||
return Resolve(null, acceptLanguage);
|
||||
}
|
||||
public void RememberClientLanguage(Guid user, string device, string language)
|
||||
{
|
||||
if (user == Guid.Empty || string.IsNullOrEmpty(device) || Supported(language) is not { } supported) return;
|
||||
foreach (var entry in clientLanguages.Where(x => DateTime.UtcNow - x.Value.Seen > TimeSpan.FromMinutes(2)).ToArray()) clientLanguages.TryRemove(entry.Key, out _);
|
||||
if (clientLanguages.Count >= 512 && !clientLanguages.ContainsKey((user, device))) return;
|
||||
clientLanguages[(user, device)] = (supported, DateTime.UtcNow);
|
||||
}
|
||||
public string ClientLanguage(Guid user, string device) => device != null && clientLanguages.TryGetValue((user, device), out var value)
|
||||
&& DateTime.UtcNow - value.Seen < TimeSpan.FromMinutes(2) ? value.Language : null;
|
||||
public IReadOnlyDictionary<string, string> Texts(string language) => Catalogue[Supported(language) ?? "en"];
|
||||
public string Text(string language, string key) => Texts(language).TryGetValue(key, out var value) ? value : Catalogue["en"][key];
|
||||
}
|
||||
Reference in New Issue
Block a user