From 3c82485a0a7d0c6c98c3c0babce8aac59a2a2a77 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sun, 19 Jul 2026 01:35:02 +0000 Subject: [PATCH] Add language-aware article sorting --- .../Api/MultilangController.cs | 21 ++++++ .../Configuration/PluginConfiguration.cs | 4 ++ .../Configuration/configPage.html | 61 +++++++++++++++-- .../Configuration/shared.css | 4 ++ .../Services/ItemsProxySorting.cs | 59 ++++++++++++----- .../Services/ItemsProxyTransformer.cs | 51 ++++++++++++--- .../ItemsProxySortingTests.cs | 65 ++++++++++++++++++- .../ItemsProxyTransformerResolutionTests.cs | 9 ++- 8 files changed, 237 insertions(+), 37 deletions(-) diff --git a/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs b/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs index 1e40602..a2df1f7 100644 --- a/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs +++ b/src/Jellyfin.Plugin.Multilang/Api/MultilangController.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -42,6 +43,7 @@ public sealed class MultilangController : ControllerBase private readonly IUserManager _userManager; private readonly IHttpClientFactory _httpClientFactory; private readonly IApplicationPaths _appPaths; + private readonly ILocalizationManager _localizationManager; private readonly ProviderCatalog _providerCatalog; private readonly SortArticleCatalog _articleCatalog; private readonly RefreshService _refreshService; @@ -59,6 +61,7 @@ public sealed class MultilangController : ControllerBase IUserManager userManager, IHttpClientFactory httpClientFactory, IApplicationPaths appPaths, + ILocalizationManager localizationManager, ProviderCatalog providerCatalog, SortArticleCatalog articleCatalog, RefreshService refreshService, @@ -75,6 +78,7 @@ public sealed class MultilangController : ControllerBase _userManager = userManager; _httpClientFactory = httpClientFactory; _appPaths = appPaths; + _localizationManager = localizationManager; _providerCatalog = providerCatalog; _articleCatalog = articleCatalog; _refreshService = refreshService; @@ -111,6 +115,13 @@ public sealed class MultilangController : ControllerBase public IActionResult ArticleBuiltins() => Ok(_articleCatalog.GetBuiltIns().OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(k => k.Key, v => v.Value)); + [HttpGet("MetadataLanguages")] + public IActionResult MetadataLanguages() + => Ok(_localizationManager.GetCultures() + .Where(c => !string.IsNullOrWhiteSpace(c.Name)) + .OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(c => new { c.Name, c.DisplayName })); + [HttpGet("AdminConfig")] public async Task AdminConfig() { @@ -143,6 +154,8 @@ public sealed class MultilangController : ControllerBase private static bool AdminConfigAffectsCachedItems(PluginConfiguration previous, PluginConfiguration next) => !StringArrayEquals(previous.Languages, next.Languages, StringComparer.OrdinalIgnoreCase) || !string.Equals(previous.AssetStorageMode, next.AssetStorageMode, StringComparison.OrdinalIgnoreCase) || + previous.IgnoreArticlesWhenSorting != next.IgnoreArticlesWhenSorting || + !string.Equals(previous.JellyfinTitleLanguageFallback, next.JellyfinTitleLanguageFallback, StringComparison.OrdinalIgnoreCase) || !ArticleEntriesEqual(previous.ArticleEntries, next.ArticleEntries); private static bool StringArrayEquals(string[] previous, string[] next, StringComparer comparer) @@ -1401,6 +1414,10 @@ public sealed class AdminConfigDto public string AssetStorageMode { get; set; } = "url"; + public bool IgnoreArticlesWhenSorting { get; set; } = true; + + public string JellyfinTitleLanguageFallback { get; set; } = string.Empty; + public SortArticleEntry[] ArticleEntries { get; set; } = []; public bool EnableLogging { get; set; } @@ -1422,6 +1439,8 @@ public sealed class AdminConfigDto ItemsProxyCacheTtlMinutes = cfg.ItemsProxyCacheTtlMinutes, ItemsProxyCacheMaxMiB = cfg.ItemsProxyCacheMaxMiB, AssetStorageMode = NormalizeAssetStorageMode(cfg.AssetStorageMode), + IgnoreArticlesWhenSorting = cfg.IgnoreArticlesWhenSorting, + JellyfinTitleLanguageFallback = cfg.JellyfinTitleLanguageFallback, ArticleEntries = cfg.ArticleEntries, EnableLogging = cfg.EnableLogging, VerboseLogging = cfg.VerboseLogging, @@ -1444,6 +1463,8 @@ public sealed class AdminConfigDto ItemsProxyCacheTtlMinutes = Math.Max(1, ItemsProxyCacheTtlMinutes), ItemsProxyCacheMaxMiB = Math.Max(1, ItemsProxyCacheMaxMiB), AssetStorageMode = NormalizeAssetStorageMode(AssetStorageMode), + IgnoreArticlesWhenSorting = IgnoreArticlesWhenSorting, + JellyfinTitleLanguageFallback = JellyfinTitleLanguageFallback?.Trim() ?? string.Empty, ArticleEntries = ArticleEntries .Where(e => !string.IsNullOrWhiteSpace(e.Language)) .Select(e => new SortArticleEntry { Language = e.Language.Trim(), Articles = e.Articles ?? string.Empty, AlwaysApply = e.AlwaysApply }) diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/PluginConfiguration.cs b/src/Jellyfin.Plugin.Multilang/Configuration/PluginConfiguration.cs index 3ad5c7d..7081333 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/PluginConfiguration.cs +++ b/src/Jellyfin.Plugin.Multilang/Configuration/PluginConfiguration.cs @@ -29,6 +29,10 @@ public sealed class PluginConfiguration : BasePluginConfiguration public string AssetStorageMode { get; set; } = "url"; + public bool IgnoreArticlesWhenSorting { get; set; } = true; + + public string JellyfinTitleLanguageFallback { get; set; } = string.Empty; + [XmlArray("SortArticles")] [XmlArrayItem("SortArticleEntry")] public SortArticleEntry[] ArticleEntries { get; set; } = []; diff --git a/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html b/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html index f7a6c25..54bad28 100644 --- a/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html +++ b/src/Jellyfin.Plugin.Multilang/Configuration/configPage.html @@ -28,16 +28,25 @@
- Languages + Languages to fetch
- +
Use BCP-47 language tags. These are the languages refresh jobs fetch and user rules can select.
+
+ +
+
+ + +
+
- Articles + List of articles
Comma- or semicolon-separated leading articles to ignore per language.
+
To ignore a language's articles, even on titles in other languages, check "Always ignore".
@@ -179,7 +188,7 @@