diff --git a/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs b/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs index f571626..d957a3d 100644 --- a/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs +++ b/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; +using static Jellyfin.Plugin.Multilang.MultilangConstants; using Jellyfin.Plugin.Multilang.Configuration; using Jellyfin.Plugin.Multilang.Data; using Jellyfin.Plugin.Multilang.Rules; @@ -450,11 +451,11 @@ public sealed class MultilangController : ControllerBase var classificationFacts = GetClassificationFacts(rules, facts); var category = PickCategory(rules, classificationFacts); - var title = ResolveField("title", GetActionList(rules, category, "title"), facts, byLang); + var title = ResolveField(TitleField, GetActionList(rules, category, TitleField), facts, byLang); if (title.Change) item["Name"] = title.Value ?? string.Empty; - var overview = ResolveField("overview", GetActionList(rules, category, "overview"), facts, byLang); + var overview = ResolveField(OverviewField, GetActionList(rules, category, OverviewField), facts, byLang); if (overview.Change) { if (string.IsNullOrWhiteSpace(overview.Value)) @@ -463,16 +464,16 @@ public sealed class MultilangController : ControllerBase item["Overview"] = overview.Value; } - var tagline = ResolveField("tagline", GetActionList(rules, category, "tagline"), facts, byLang); + var tagline = ResolveField(TaglineField, GetActionList(rules, category, TaglineField), facts, byLang); if (tagline.Change) item["Taglines"] = string.IsNullOrWhiteSpace(tagline.Value) ? new JsonArray() : new JsonArray(tagline.Value); assets.TryGetValue(itemId, out var assetsByKind); - ApplyImageAsset(item, "Primary", ResolveAsset("poster", GetActionList(rules, category, "poster"), assetsByKind)); - ApplyImageAsset(item, "Logo", ResolveAsset("logo", GetActionList(rules, category, "logo"), assetsByKind)); - ApplyImageAsset(item, "Banner", ResolveAsset("banner", GetActionList(rules, category, "banner"), assetsByKind)); - ApplyImageAsset(item, "Thumb", ResolveAsset("thumb", GetActionList(rules, category, "thumb"), assetsByKind)); - ApplyBackdropAsset(item, ResolveAsset("backdrop", GetActionList(rules, category, "backdrop"), assetsByKind)); + ApplyImageAsset(item, "Primary", ResolveAsset(PosterKind, GetActionList(rules, category, PosterKind), assetsByKind)); + ApplyImageAsset(item, "Logo", ResolveAsset(LogoKind, GetActionList(rules, category, LogoKind), assetsByKind)); + ApplyImageAsset(item, "Banner", ResolveAsset(BannerKind, GetActionList(rules, category, BannerKind), assetsByKind)); + ApplyImageAsset(item, "Thumb", ResolveAsset(ThumbKind, GetActionList(rules, category, ThumbKind), assetsByKind)); + ApplyBackdropAsset(item, ResolveAsset(BackdropKind, GetActionList(rules, category, BackdropKind), assetsByKind)); ApplyGenres(item, facts, movieGenreNames, tvGenreNames); } } @@ -503,15 +504,14 @@ public sealed class MultilangController : ControllerBase private static void AddLanguageAction(List langs, string action) { - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { - langs.Add("Original"); + langs.Add(OriginalAction); return; } - const string prefix = "Language:"; - if (action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - langs.Add(action[prefix.Length..]); + if (action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) + langs.Add(action[LanguagePrefix.Length..]); } private static UserCategoryRule? PickCategory(UserRulesDocument rules, FactsData facts) @@ -637,30 +637,29 @@ public sealed class MultilangController : ControllerBase { foreach (var action in actions) { - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)) return new ResolvedText(false, null); - if (field.Equals("title", StringComparison.OrdinalIgnoreCase) && - action.Equals("Original", StringComparison.OrdinalIgnoreCase) && + if (field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) && + action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(facts.OriginalTitle)) return new ResolvedText(true, facts.OriginalTitle); - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { - var originalValue = GetTranslatedField(byLang, "Original", field); + var originalValue = GetTranslatedField(byLang, OriginalAction, field); if (!string.IsNullOrWhiteSpace(originalValue)) return new ResolvedText(true, originalValue); continue; } - const string prefix = "Language:"; - if (!action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) continue; - var value = GetTranslatedField(byLang, action[prefix.Length..], field); + var value = GetTranslatedField(byLang, action[LanguagePrefix.Length..], field); if (!string.IsNullOrWhiteSpace(value)) return new ResolvedText(true, value); } - return field.Equals("title", StringComparison.OrdinalIgnoreCase) + return field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) ? new ResolvedText(false, null) : new ResolvedText(true, string.Empty); } @@ -682,26 +681,25 @@ public sealed class MultilangController : ControllerBase { foreach (var action in actions) { - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)) return new ResolvedAsset(false, null); - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { - var originalValue = GetAsset(byKind, kind, "Original"); + var originalValue = GetAsset(byKind, kind, OriginalAction); if (!string.IsNullOrWhiteSpace(originalValue)) return new ResolvedAsset(true, originalValue); continue; } - const string prefix = "Language:"; - if (!action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) continue; - var value = GetAsset(byKind, kind, action[prefix.Length..]); + var value = GetAsset(byKind, kind, action[LanguagePrefix.Length..]); if (!string.IsNullOrWhiteSpace(value)) return new ResolvedAsset(true, value); } - return kind.Equals("poster", StringComparison.OrdinalIgnoreCase) + return kind.Equals(PosterKind, StringComparison.OrdinalIgnoreCase) ? new ResolvedAsset(false, null) : new ResolvedAsset(true, string.Empty); } @@ -1193,7 +1191,7 @@ public sealed class MultilangController : ControllerBase var ruleLanguages = GetNeededLanguages(rules); var debugLanguages = configuredLanguages .Concat(ruleLanguages) - .Append("Original") + .Append(OriginalAction) .Where(lang => !string.IsNullOrWhiteSpace(lang)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); @@ -1211,20 +1209,19 @@ public sealed class MultilangController : ControllerBase var effectiveSortLocale = ResolveSortLocale(rules.SortLocale, clientLocale); var genreLocale = string.IsNullOrWhiteSpace(clientLocale) ? effectiveSortLocale : clientLocale; - string[] fields = ["title", "overview", "tagline", "poster", "logo", "banner", "thumb", "backdrop"]; - var actionLists = fields.ToDictionary( + var actionLists = ActionFields.ToDictionary( field => field, field => GetActionList(rules, category, field).ToArray(), StringComparer.OrdinalIgnoreCase); - var title = TraceResolveField("title", actionLists["title"], facts, byLang); - var overview = TraceResolveField("overview", actionLists["overview"], facts, byLang); - var tagline = TraceResolveField("tagline", actionLists["tagline"], facts, byLang); - var poster = TraceResolveAsset("poster", actionLists["poster"], byKind); - var logo = TraceResolveAsset("logo", actionLists["logo"], byKind); - var banner = TraceResolveAsset("banner", actionLists["banner"], byKind); - var thumb = TraceResolveAsset("thumb", actionLists["thumb"], byKind); - var backdrop = TraceResolveAsset("backdrop", actionLists["backdrop"], byKind); + var title = TraceResolveField(TitleField, actionLists[TitleField], facts, byLang); + var overview = TraceResolveField(OverviewField, actionLists[OverviewField], facts, byLang); + var tagline = TraceResolveField(TaglineField, actionLists[TaglineField], facts, byLang); + var poster = TraceResolveAsset(PosterKind, actionLists[PosterKind], byKind); + var logo = TraceResolveAsset(LogoKind, actionLists[LogoKind], byKind); + var banner = TraceResolveAsset(BannerKind, actionLists[BannerKind], byKind); + var thumb = TraceResolveAsset(ThumbKind, actionLists[ThumbKind], byKind); + var backdrop = TraceResolveAsset(BackdropKind, actionLists[BackdropKind], byKind); var genres = ResolveGenresForDebug(facts, genreLocale); return Ok(new @@ -1239,8 +1236,8 @@ public sealed class MultilangController : ControllerBase ConfiguredLanguages = configuredLanguages, RuleLanguages = ruleLanguages, DebugLanguages = debugLanguages, - MatchedCategory = categoryMatch.Matched ?? new { Id = "fallback", Label = "Fallback" } as object, - EffectiveCategory = category ?? new { Id = "fallback", Label = "Fallback" } as object, + MatchedCategory = categoryMatch.Matched ?? new { Id = "fallback", Label = FallbackAction } as object, + EffectiveCategory = category ?? new { Id = "fallback", Label = FallbackAction } as object, DuplicateCategoryLabelResolved = categoryMatch.DuplicateLabelResolved, CategoryEvaluation = new { @@ -1311,15 +1308,15 @@ public sealed class MultilangController : ControllerBase var attempts = new List(); foreach (var action in actions) { - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)) { var result = new ResolvedText(false, null); - attempts.Add(new DebugResolutionAttempt(action, "Jellyfin", string.Empty, true, true, null, "Use Jellyfin value")); - return new DebugTextResolution(field, actions.ToArray(), attempts.ToArray(), result, "Jellyfin", "Jellyfin action reached"); + attempts.Add(new DebugResolutionAttempt(action, JellyfinAction, string.Empty, true, true, null, "Use Jellyfin value")); + return new DebugTextResolution(field, actions.ToArray(), attempts.ToArray(), result, JellyfinAction, "Jellyfin action reached"); } - if (field.Equals("title", StringComparison.OrdinalIgnoreCase) && - action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) && + action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { var hasOriginalTitle = !string.IsNullOrWhiteSpace(facts.OriginalTitle); attempts.Add(new DebugResolutionAttempt(action, "OriginalTitle", "facts.original_title", hasOriginalTitle, hasOriginalTitle, facts.OriginalTitle, hasOriginalTitle ? "Original title found" : "Original title missing")); @@ -1332,9 +1329,9 @@ public sealed class MultilangController : ControllerBase continue; } - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { - var originalValue = GetTranslatedField(byLang, "Original", field); + var originalValue = GetTranslatedField(byLang, OriginalAction, field); var hasOriginalValue = !string.IsNullOrWhiteSpace(originalValue); attempts.Add(new DebugResolutionAttempt(action, "Translation", "Original/" + field, hasOriginalValue, hasOriginalValue, originalValue, hasOriginalValue ? "Original-language translation found" : "Original-language translation missing")); if (hasOriginalValue) @@ -1346,14 +1343,13 @@ public sealed class MultilangController : ControllerBase continue; } - const string prefix = "Language:"; - if (!action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) { attempts.Add(new DebugResolutionAttempt(action, "Unknown", string.Empty, false, false, null, "Unsupported action token")); continue; } - var lang = action[prefix.Length..]; + var lang = action[LanguagePrefix.Length..]; var value = GetTranslatedField(byLang, lang, field); var hasValue = !string.IsNullOrWhiteSpace(value); attempts.Add(new DebugResolutionAttempt(action, "Translation", lang + "/" + field, hasValue, hasValue, value, hasValue ? "Translation found" : "Translation missing or empty")); @@ -1364,13 +1360,13 @@ public sealed class MultilangController : ControllerBase } } - var fallback = field.Equals("title", StringComparison.OrdinalIgnoreCase) + var fallback = field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) ? new ResolvedText(false, null) : new ResolvedText(true, string.Empty); - var reason = field.Equals("title", StringComparison.OrdinalIgnoreCase) + var reason = field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) ? "No action had data; title falls back to Jellyfin" : "No action had data; field is cleared"; - return new DebugTextResolution(field, actions.ToArray(), attempts.ToArray(), fallback, field.Equals("title", StringComparison.OrdinalIgnoreCase) ? "Jellyfin" : "Clear", reason); + return new DebugTextResolution(field, actions.ToArray(), attempts.ToArray(), fallback, field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) ? JellyfinAction : ClearAction, reason); } private static DebugAssetResolution TraceResolveAsset( @@ -1381,16 +1377,16 @@ public sealed class MultilangController : ControllerBase var attempts = new List(); foreach (var action in actions) { - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)) { var result = new ResolvedAsset(false, null); - attempts.Add(new DebugResolutionAttempt(action, "Jellyfin", string.Empty, true, true, null, "Use Jellyfin image")); - return new DebugAssetResolution(kind, actions.ToArray(), attempts.ToArray(), result, "Jellyfin", "Jellyfin action reached"); + attempts.Add(new DebugResolutionAttempt(action, JellyfinAction, string.Empty, true, true, null, "Use Jellyfin image")); + return new DebugAssetResolution(kind, actions.ToArray(), attempts.ToArray(), result, JellyfinAction, "Jellyfin action reached"); } - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) { - var originalValue = GetAsset(byKind, kind, "Original"); + var originalValue = GetAsset(byKind, kind, OriginalAction); var hasOriginalValue = !string.IsNullOrWhiteSpace(originalValue); attempts.Add(new DebugResolutionAttempt(action, "Asset", kind + "/Original", hasOriginalValue, hasOriginalValue, originalValue, hasOriginalValue ? "Original-language asset found" : "Original-language asset missing")); if (hasOriginalValue) @@ -1402,14 +1398,13 @@ public sealed class MultilangController : ControllerBase continue; } - const string prefix = "Language:"; - if (!action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) { attempts.Add(new DebugResolutionAttempt(action, "Unknown", string.Empty, false, false, null, "Unsupported action token")); continue; } - var lang = action[prefix.Length..]; + var lang = action[LanguagePrefix.Length..]; var value = GetAsset(byKind, kind, lang); var hasValue = !string.IsNullOrWhiteSpace(value); attempts.Add(new DebugResolutionAttempt(action, "Asset", kind + "/" + lang, hasValue, hasValue, value, hasValue ? "Asset found" : "Asset missing")); @@ -1420,13 +1415,13 @@ public sealed class MultilangController : ControllerBase } } - var fallback = kind.Equals("poster", StringComparison.OrdinalIgnoreCase) + var fallback = kind.Equals(PosterKind, StringComparison.OrdinalIgnoreCase) ? new ResolvedAsset(false, null) : new ResolvedAsset(true, string.Empty); - var reason = kind.Equals("poster", StringComparison.OrdinalIgnoreCase) + var reason = kind.Equals(PosterKind, StringComparison.OrdinalIgnoreCase) ? "No action had data; poster falls back to Jellyfin" : "No action had data; image is cleared"; - return new DebugAssetResolution(kind, actions.ToArray(), attempts.ToArray(), fallback, kind.Equals("poster", StringComparison.OrdinalIgnoreCase) ? "Jellyfin" : "Clear", reason); + return new DebugAssetResolution(kind, actions.ToArray(), attempts.ToArray(), fallback, kind.Equals(PosterKind, StringComparison.OrdinalIgnoreCase) ? JellyfinAction : ClearAction, reason); } private static object[] BuildMissingDataReport( @@ -1434,18 +1429,16 @@ public sealed class MultilangController : ControllerBase Dictionary>? byLang, Dictionary>? byKind) { - string[] textFields = ["title", "overview", "tagline"]; - string[] assetKinds = ["poster", "logo", "banner", "thumb", "backdrop"]; return configuredLanguages .Where(lang => !string.IsNullOrWhiteSpace(lang)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(lang => lang, StringComparer.OrdinalIgnoreCase) .Select(lang => { - var missingText = textFields + var missingText = TextFields .Where(field => string.IsNullOrWhiteSpace(GetTranslatedField(byLang, lang, field))) .ToArray(); - var missingAssets = assetKinds + var missingAssets = AssetKinds .Where(kind => string.IsNullOrWhiteSpace(GetAsset(byKind, kind, lang))) .ToArray(); return new @@ -1462,7 +1455,7 @@ public sealed class MultilangController : ControllerBase private static object DebugTextOutput(ResolvedText text) => new { - Source = text.Change ? "Multilang" : "Jellyfin", + Source = text.Change ? "Multilang" : JellyfinAction, Value = text.Value, ClearsValue = text.Change && string.IsNullOrWhiteSpace(text.Value) }; @@ -1470,7 +1463,7 @@ public sealed class MultilangController : ControllerBase private static object DebugAssetOutput(ResolvedAsset asset) => new { - Source = asset.Change ? "Multilang" : "Jellyfin", + Source = asset.Change ? "Multilang" : JellyfinAction, Value = asset.Change && !string.IsNullOrWhiteSpace(asset.Value) ? ToImageTag(asset.Value) : asset.Value, ClearsValue = asset.Change && string.IsNullOrWhiteSpace(asset.Value) }; @@ -1675,20 +1668,20 @@ public sealed class MultilangController : ControllerBase DefaultActionLists().Keys.All(field => actions.TryGetValue(field, out var list) && list.Length == 1 && - list[0].Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)); + list[0].Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)); private static bool HasMeaningfulLegacyActions(IReadOnlyDictionary legacyActions) => legacyActions.Values.Any(action => !string.IsNullOrWhiteSpace(action) && - !action.Equals("Fallback", StringComparison.OrdinalIgnoreCase) && - !action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)); + !action.Equals(FallbackAction, StringComparison.OrdinalIgnoreCase) && + !action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)); private static string[] LegacyActionToList(string field, IReadOnlyDictionary? legacyActions) { if (legacyActions is null || !legacyActions.TryGetValue(field, out var action)) - return ["Jellyfin"]; - if (string.IsNullOrWhiteSpace(action) || action.Equals("Fallback", StringComparison.OrdinalIgnoreCase)) - return ["Jellyfin"]; + return [JellyfinAction]; + if (string.IsNullOrWhiteSpace(action) || action.Equals(FallbackAction, StringComparison.OrdinalIgnoreCase)) + return [JellyfinAction]; return [action]; } @@ -1714,24 +1707,23 @@ public sealed class MultilangController : ControllerBase private static string NormalizeAction(string field, string? raw, IReadOnlySet allowedLanguages) { var action = (raw ?? string.Empty).Trim(); - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase) || - action.Equals("Fallback", StringComparison.OrdinalIgnoreCase)) + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase) || + action.Equals(FallbackAction, StringComparison.OrdinalIgnoreCase)) return action; - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) - return "Original"; - if ((field is "overview" or "tagline" or "logo") && - action.Equals("Clear", StringComparison.OrdinalIgnoreCase)) - return "Clear"; - const string prefix = "Language:"; - if (action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) + return OriginalAction; + if ((field is OverviewField or TaglineField or LogoKind) && + action.Equals(ClearAction, StringComparison.OrdinalIgnoreCase)) + return ClearAction; + if (action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase)) { - var lang = action[prefix.Length..].Trim(); + var lang = action[LanguagePrefix.Length..].Trim(); var canonical = allowedLanguages.FirstOrDefault(l => l.Equals(lang, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(canonical)) - return prefix + canonical; + return LanguagePrefix + canonical; } - return "Fallback"; + return FallbackAction; } private static string[] NormalizeActionList(string field, IEnumerable? raw, IReadOnlySet allowedLanguages) @@ -1747,11 +1739,10 @@ public sealed class MultilangController : ControllerBase result.Add(normalized); } - if (field.Equals("title", StringComparison.OrdinalIgnoreCase) || - field.Equals("poster", StringComparison.OrdinalIgnoreCase)) + if (RequiresJellyfinFallback(field)) { - if (!result.Contains("Jellyfin", StringComparer.OrdinalIgnoreCase)) - result.Add("Jellyfin"); + if (!result.Contains(JellyfinAction, StringComparer.OrdinalIgnoreCase)) + result.Add(JellyfinAction); } return result.ToArray(); @@ -1761,48 +1752,21 @@ public sealed class MultilangController : ControllerBase { var action = (raw ?? string.Empty).Trim(); if (action.Equals("J", StringComparison.OrdinalIgnoreCase)) - return "Jellyfin"; + return JellyfinAction; if (action.Equals("O", StringComparison.OrdinalIgnoreCase)) - return "Original"; - if (action.Equals("Jellyfin", StringComparison.OrdinalIgnoreCase)) - return "Jellyfin"; - if (action.Equals("Original", StringComparison.OrdinalIgnoreCase)) - return "Original"; + return OriginalAction; + if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase)) + return JellyfinAction; + if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase)) + return OriginalAction; - const string prefix = "Language:"; - var lang = action.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) - ? action[prefix.Length..].Trim() + var lang = action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase) + ? action[LanguagePrefix.Length..].Trim() : action; var canonical = allowedLanguages.FirstOrDefault(l => l.Equals(lang, StringComparison.OrdinalIgnoreCase)); - return string.IsNullOrWhiteSpace(canonical) ? string.Empty : prefix + canonical; + return string.IsNullOrWhiteSpace(canonical) ? string.Empty : LanguagePrefix + canonical; } - private static Dictionary DefaultFieldActions() - => new(StringComparer.OrdinalIgnoreCase) - { - ["title"] = "Fallback", - ["overview"] = "Fallback", - ["tagline"] = "Fallback", - ["poster"] = "Fallback", - ["logo"] = "Fallback", - ["banner"] = "Fallback", - ["thumb"] = "Fallback", - ["backdrop"] = "Fallback" - }; - - private static Dictionary DefaultActionLists() - => new(StringComparer.OrdinalIgnoreCase) - { - ["title"] = ["Jellyfin"], - ["overview"] = ["Jellyfin"], - ["tagline"] = ["Jellyfin"], - ["poster"] = ["Jellyfin"], - ["logo"] = ["Jellyfin"], - ["banner"] = ["Jellyfin"], - ["thumb"] = ["Jellyfin"], - ["backdrop"] = ["Jellyfin"] - }; - private static IReadOnlyList GetActionList(UserRulesDocument rules, UserCategoryRule? category, string field) { var actions = category is null ? rules.FallbackFieldActions : category.FieldActionLists; @@ -1812,7 +1776,7 @@ public sealed class MultilangController : ControllerBase if (category is not null && category.FieldActions is not null) return LegacyActionToList(field, category.FieldActions); - return ["Jellyfin"]; + return [JellyfinAction]; } private sealed record ProxyRequest( diff --git a/src/Jellyfin.Plugin.Multilang/Data/TranslationStore.cs b/src/Jellyfin.Plugin.Multilang/Data/TranslationStore.cs index c818ddf..77dd984 100644 --- a/src/Jellyfin.Plugin.Multilang/Data/TranslationStore.cs +++ b/src/Jellyfin.Plugin.Multilang/Data/TranslationStore.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.Json; +using Jellyfin.Plugin.Multilang; using Jellyfin.Plugin.Multilang.Services.Refresh; using Jellyfin.Plugin.Multilang.Services.Providers; using MediaBrowser.Common.Configuration; @@ -217,7 +218,7 @@ DELETE FROM assets WHERE item_id = $item_id;"; var allowed = allowedLanguages .Where(l => !string.IsNullOrWhiteSpace(l)) .Select(l => l.Trim()) - .Append("Original") + .Append(MultilangConstants.OriginalAction) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); @@ -895,7 +896,7 @@ ON CONFLICT(user_id) DO UPDATE SET langs_json = excluded.langs_json, updated_at public ImportDatabaseResult ImportTranslationDatabase(string sourcePath, IReadOnlySet liveItemIds, IReadOnlySet allowedLanguages) { var allowed = allowedLanguages.ToHashSet(StringComparer.OrdinalIgnoreCase); - allowed.Add("Original"); + allowed.Add(MultilangConstants.OriginalAction); using var destination = Open(); destination.Open(); @@ -1134,17 +1135,7 @@ public sealed class UserRulesDocument public string[] FallbackLanguages { get; set; } = []; - public Dictionary FallbackFieldActions { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - ["title"] = ["Jellyfin"], - ["overview"] = ["Jellyfin"], - ["tagline"] = ["Jellyfin"], - ["poster"] = ["Jellyfin"], - ["logo"] = ["Jellyfin"], - ["banner"] = ["Jellyfin"], - ["thumb"] = ["Jellyfin"], - ["backdrop"] = ["Jellyfin"] - }; + public Dictionary FallbackFieldActions { get; set; } = MultilangConstants.DefaultActionLists(); public bool TrustTmdbCollections { get; set; } = true; @@ -1176,29 +1167,9 @@ public sealed class UserCategoryRule public string[] Scopes { get; set; } = ["M", "S", "C"]; - public Dictionary FieldActions { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - ["title"] = "Fallback", - ["overview"] = "Fallback", - ["tagline"] = "Fallback", - ["poster"] = "Fallback", - ["logo"] = "Fallback", - ["banner"] = "Fallback", - ["thumb"] = "Fallback", - ["backdrop"] = "Fallback" - }; + public Dictionary FieldActions { get; set; } = MultilangConstants.DefaultFieldActions(); - public Dictionary FieldActionLists { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - ["title"] = ["Jellyfin"], - ["overview"] = ["Jellyfin"], - ["tagline"] = ["Jellyfin"], - ["poster"] = ["Jellyfin"], - ["logo"] = ["Jellyfin"], - ["banner"] = ["Jellyfin"], - ["thumb"] = ["Jellyfin"], - ["backdrop"] = ["Jellyfin"] - }; + public Dictionary FieldActionLists { get; set; } = MultilangConstants.DefaultActionLists(); } public sealed class UserCategoryRequirement diff --git a/src/Jellyfin.Plugin.Multilang/MultilangConstants.cs b/src/Jellyfin.Plugin.Multilang/MultilangConstants.cs new file mode 100644 index 0000000..8b60285 --- /dev/null +++ b/src/Jellyfin.Plugin.Multilang/MultilangConstants.cs @@ -0,0 +1,39 @@ +namespace Jellyfin.Plugin.Multilang; + +internal static class MultilangConstants +{ + public const string JellyfinAction = "Jellyfin"; + public const string OriginalAction = "Original"; + public const string FallbackAction = "Fallback"; + public const string ClearAction = "Clear"; + public const string LanguagePrefix = "Language:"; + + public const string TitleField = "title"; + public const string OverviewField = "overview"; + public const string TaglineField = "tagline"; + public const string PosterKind = "poster"; + public const string LogoKind = "logo"; + public const string BannerKind = "banner"; + public const string ThumbKind = "thumb"; + public const string BackdropKind = "backdrop"; + + public static readonly string[] TextFields = [TitleField, OverviewField, TaglineField]; + public static readonly string[] AssetKinds = [PosterKind, LogoKind, BannerKind, ThumbKind, BackdropKind]; + public static readonly string[] ActionFields = [TitleField, OverviewField, TaglineField, PosterKind, LogoKind, BannerKind, ThumbKind, BackdropKind]; + + public static bool RequiresJellyfinFallback(string field) + => field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) || + field.Equals(PosterKind, StringComparison.OrdinalIgnoreCase); + + public static Dictionary DefaultFieldActions() + => ActionFields.ToDictionary( + field => field, + _ => FallbackAction, + StringComparer.OrdinalIgnoreCase); + + public static Dictionary DefaultActionLists() + => ActionFields.ToDictionary( + field => field, + _ => new[] { JellyfinAction }, + StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Jellyfin.Plugin.Multilang/Services/Refresh/RefreshService.cs b/src/Jellyfin.Plugin.Multilang/Services/Refresh/RefreshService.cs index 8428daf..3eed2e1 100644 --- a/src/Jellyfin.Plugin.Multilang/Services/Refresh/RefreshService.cs +++ b/src/Jellyfin.Plugin.Multilang/Services/Refresh/RefreshService.cs @@ -4,6 +4,7 @@ using Jellyfin.Plugin.Multilang.Configuration; using Jellyfin.Plugin.Multilang.Data; using Jellyfin.Plugin.Multilang.Services.Assets; using Jellyfin.Plugin.Multilang.Services.Providers; +using static Jellyfin.Plugin.Multilang.MultilangConstants; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; @@ -13,8 +14,6 @@ namespace Jellyfin.Plugin.Multilang.Services.Refresh; public sealed class RefreshService { - private const string OriginalLanguageTag = "Original"; - private readonly ILibraryManager _libraryManager; private readonly TranslationStore _store; private readonly TmdbClient _tmdbClient; @@ -418,9 +417,9 @@ public sealed class RefreshService } Log(cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, "[{Index}/{Total}] TMDb metadata ok item={ItemId} lang={Lang}", scanIndex, totalItems, item.ItemId, lang); - _store.UpsertTranslation(item.ItemId, lang, "title", meta.Title); - _store.UpsertTranslation(item.ItemId, lang, "overview", meta.Overview); - _store.UpsertTranslation(item.ItemId, lang, "tagline", meta.Tagline); + _store.UpsertTranslation(item.ItemId, lang, TitleField, meta.Title); + _store.UpsertTranslation(item.ItemId, lang, OverviewField, meta.Overview); + _store.UpsertTranslation(item.ItemId, lang, TaglineField, meta.Tagline); StoreGenres(item, lang, meta); fetchedMetadata[lang] = meta; firstMeta ??= meta; @@ -451,10 +450,10 @@ public sealed class RefreshService if (originalMeta is not null) { - _store.UpsertTranslation(item.ItemId, OriginalLanguageTag, "title", originalMeta.Title); - _store.UpsertTranslation(item.ItemId, OriginalLanguageTag, "overview", originalMeta.Overview); - _store.UpsertTranslation(item.ItemId, OriginalLanguageTag, "tagline", originalMeta.Tagline); - StoreGenres(item, OriginalLanguageTag, originalMeta); + _store.UpsertTranslation(item.ItemId, OriginalAction, TitleField, originalMeta.Title); + _store.UpsertTranslation(item.ItemId, OriginalAction, OverviewField, originalMeta.Overview); + _store.UpsertTranslation(item.ItemId, OriginalAction, TaglineField, originalMeta.Tagline); + StoreGenres(item, OriginalAction, originalMeta); Log( cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, @@ -549,9 +548,9 @@ public sealed class RefreshService if (tmdbImages is null) continue; - await StoreImageMapAsync(item.ItemId, languages, "poster", tmdbImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); - await StoreImageMapAsync(item.ItemId, languages, "logo", tmdbImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); - await StoreImageMapAsync(item.ItemId, languages, "backdrop", tmdbImages.Backdrops, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, PosterKind, tmdbImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, LogoKind, tmdbImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, BackdropKind, tmdbImages.Backdrops, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); Log(cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, "TMDb artwork stored item={ItemId} langs={Languages}", item.ItemId, string.Join(",", languages)); continue; } @@ -562,10 +561,10 @@ public sealed class RefreshService if (fanartImages is null) continue; - await StoreImageMapAsync(item.ItemId, languages, "poster", fanartImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); - await StoreImageMapAsync(item.ItemId, languages, "logo", fanartImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); - await StoreImageMapAsync(item.ItemId, languages, "banner", fanartImages.Banners, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); - await StoreImageMapAsync(item.ItemId, languages, "thumb", fanartImages.Thumbs, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, PosterKind, fanartImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, LogoKind, fanartImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, BannerKind, fanartImages.Banners, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); + await StoreImageMapAsync(item.ItemId, languages, ThumbKind, fanartImages.Thumbs, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false); Log(cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, "Fanart artwork stored item={ItemId} langs={Languages}", item.ItemId, string.Join(",", languages)); } } @@ -583,8 +582,8 @@ public sealed class RefreshService { foreach (var lang in languages) { - var storeLang = lang.StartsWith(OriginalLanguageTag + ":", StringComparison.OrdinalIgnoreCase) - ? OriginalLanguageTag + var storeLang = lang.StartsWith(OriginalAction + ":", StringComparison.OrdinalIgnoreCase) + ? OriginalAction : lang; var key = kind + ":" + storeLang; if (written.Contains(key)) @@ -609,13 +608,13 @@ public sealed class RefreshService if (_store.HasMissingConfiguredTranslations(item.ItemId, langs)) return true; - var assetPresence = _store.GetAssetPresence(item.ItemId, langs.Append(OriginalLanguageTag)); + var assetPresence = _store.GetAssetPresence(item.ItemId, langs.Append(OriginalAction)); return IsLocalAssetStorage(cfg) && assetPresence.AnyRemote; } private static string[] AppendOriginalLanguage(string[] languages, string originalLanguage) => languages - .Concat([OriginalLanguageTag + ":" + originalLanguage]) + .Concat([OriginalAction + ":" + originalLanguage]) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); @@ -624,8 +623,8 @@ public sealed class RefreshService private static string ToIso639(string language) { - if (language.StartsWith(OriginalLanguageTag + ":", StringComparison.OrdinalIgnoreCase)) - language = language[(OriginalLanguageTag.Length + 1)..]; + if (language.StartsWith(OriginalAction + ":", StringComparison.OrdinalIgnoreCase)) + language = language[(OriginalAction.Length + 1)..]; var index = language.IndexOf('-', StringComparison.Ordinal); return (index > 0 ? language[..index] : language).Trim().ToLowerInvariant(); }