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; /// One catalogue for web status, native messages and cached FFmpeg slates. public sealed class StartupMessages { private static readonly Dictionary> 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> Load() { using var stream = typeof(StartupMessages).Assembly.GetManifestResourceStream("Jellyfin.Plugin.Enigma2.Localization.startup-messages.json"); return JsonSerializer.Deserialize>>(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 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]; }