Add opt-in FFmpeg continuity slates with per-viewer localization

This commit is contained in:
2026-10-23 22:35:40 +02:00
parent d097aad032
commit 5f6b0b5ff6
22 changed files with 1407 additions and 30 deletions
@@ -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];
}