Add language-aware article sorting
This commit is contained in:
@@ -29,6 +29,9 @@ public static class ItemsProxySorting
|
||||
}
|
||||
|
||||
public static void Apply(JsonNode root, ItemsProxyControls controls, CultureInfo culture, string[] titleArticles)
|
||||
=> Apply(root, controls, culture, _ => titleArticles);
|
||||
|
||||
public static void Apply(JsonNode root, ItemsProxyControls controls, CultureInfo culture, Func<JsonObject, string[]> titleArticlesForItem)
|
||||
{
|
||||
if (!TryGetItemsArray(root, out var itemsArray))
|
||||
return;
|
||||
@@ -36,9 +39,9 @@ public static class ItemsProxySorting
|
||||
var items = itemsArray.OfType<JsonObject>().ToList();
|
||||
var specs = BuildSortSpecs(controls.SortBy);
|
||||
if (specs.Length > 0 && items.Count > 1)
|
||||
items = Sort(items, specs, controls.SortOrder, culture, titleArticles);
|
||||
items = Sort(items, specs, controls.SortOrder, culture, titleArticlesForItem);
|
||||
|
||||
items = FilterByNameStartsWith(items, controls.NameStartsWith, culture, titleArticles);
|
||||
items = FilterByNameStartsWith(items, controls.NameStartsWith, culture, titleArticlesForItem);
|
||||
var filteredCount = items.Count;
|
||||
items = Slice(items, controls.StartIndex, controls.Limit);
|
||||
|
||||
@@ -77,24 +80,45 @@ public static class ItemsProxySorting
|
||||
}
|
||||
}
|
||||
|
||||
public static string[] GetSortArticles(SortArticleCatalog articleCatalog, IEnumerable<SortArticleEntry> configuredArticles, string? sortLocale)
|
||||
public static string[] GetSortArticles(
|
||||
SortArticleCatalog articleCatalog,
|
||||
IEnumerable<SortArticleEntry> configuredArticles,
|
||||
string? titleLanguage,
|
||||
bool ignoreArticles = true)
|
||||
{
|
||||
var locale = (sortLocale ?? string.Empty).Trim();
|
||||
if (locale.Length == 0)
|
||||
locale = "en";
|
||||
if (!ignoreArticles)
|
||||
return [];
|
||||
|
||||
var configured = configuredArticles.ToArray();
|
||||
var custom = configured.FirstOrDefault(e => e.Language.Equals(locale, StringComparison.OrdinalIgnoreCase))
|
||||
?? configured.FirstOrDefault(e => locale.StartsWith(e.Language + "-", StringComparison.OrdinalIgnoreCase));
|
||||
var always = configured
|
||||
.Where(e => e.AlwaysApply == true)
|
||||
.SelectMany(e => SplitArticles(e.Articles));
|
||||
return always
|
||||
.Concat(GetLanguageArticles(articleCatalog, configured, titleLanguage))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] GetLanguageArticles(
|
||||
SortArticleCatalog articleCatalog,
|
||||
SortArticleEntry[] configured,
|
||||
string? titleLanguage)
|
||||
{
|
||||
var language = (titleLanguage ?? string.Empty).Trim();
|
||||
if (language.Length == 0)
|
||||
return [];
|
||||
|
||||
var custom = configured.FirstOrDefault(e => e.Language.Equals(language, StringComparison.OrdinalIgnoreCase))
|
||||
?? configured.FirstOrDefault(e => language.StartsWith(e.Language + "-", StringComparison.OrdinalIgnoreCase));
|
||||
if (custom is not null)
|
||||
return SplitArticles(custom.Articles);
|
||||
|
||||
var builtIns = articleCatalog.GetBuiltIns();
|
||||
if (builtIns.TryGetValue(locale, out var exact))
|
||||
if (builtIns.TryGetValue(language, out var exact))
|
||||
return exact;
|
||||
|
||||
var dash = locale.IndexOf('-', StringComparison.Ordinal);
|
||||
return dash > 0 && builtIns.TryGetValue(locale[..dash], out var languageOnly)
|
||||
var dash = language.IndexOf('-', StringComparison.Ordinal);
|
||||
return dash > 0 && builtIns.TryGetValue(language[..dash], out var languageOnly)
|
||||
? languageOnly
|
||||
: [];
|
||||
}
|
||||
@@ -149,12 +173,12 @@ public static class ItemsProxySorting
|
||||
_ => SortKind.Text
|
||||
};
|
||||
|
||||
private static List<JsonObject> Sort(List<JsonObject> items, SortSpec[] specs, string sortOrder, CultureInfo culture, string[] titleArticles)
|
||||
private static List<JsonObject> Sort(List<JsonObject> items, SortSpec[] specs, string sortOrder, CultureInfo culture, Func<JsonObject, string[]> titleArticlesForItem)
|
||||
{
|
||||
var descending = sortOrder.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.FirstOrDefault()?.Equals("Descending", StringComparison.OrdinalIgnoreCase) == true;
|
||||
var comparer = StringComparer.Create(culture, true);
|
||||
var values = items.Select(item => new { Item = item, Values = specs.Select(spec => ValueFor(item, spec, culture, titleArticles)).ToArray() }).ToList();
|
||||
var values = items.Select(item => new { Item = item, Values = specs.Select(spec => ValueFor(item, spec, culture, titleArticlesForItem(item))).ToArray() }).ToList();
|
||||
|
||||
values.Sort((left, right) =>
|
||||
{
|
||||
@@ -197,14 +221,14 @@ public static class ItemsProxySorting
|
||||
|
||||
private readonly record struct SortValue(bool HasValue, string Text, double Number);
|
||||
|
||||
private static List<JsonObject> FilterByNameStartsWith(List<JsonObject> items, string prefix, CultureInfo culture, string[] titleArticles)
|
||||
private static List<JsonObject> FilterByNameStartsWith(List<JsonObject> items, string prefix, CultureInfo culture, Func<JsonObject, string[]> titleArticlesForItem)
|
||||
{
|
||||
prefix = (prefix ?? string.Empty).Trim();
|
||||
if (prefix.Length == 0)
|
||||
return items;
|
||||
|
||||
return items
|
||||
.Where(i => culture.CompareInfo.IsPrefix(StripArticle(GetString(i, "Name"), titleArticles), prefix, CompareOptions.IgnoreCase))
|
||||
.Where(i => culture.CompareInfo.IsPrefix(StripArticle(GetString(i, "Name"), titleArticlesForItem(i)), prefix, CompareOptions.IgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -227,8 +251,9 @@ public static class ItemsProxySorting
|
||||
var word = article.Trim();
|
||||
if (word.Length == 0 || text.Length <= word.Length)
|
||||
continue;
|
||||
if (text.StartsWith(word + " ", StringComparison.OrdinalIgnoreCase))
|
||||
return text[(word.Length + 1)..].TrimStart();
|
||||
var attachedArticle = word.EndsWith("'", StringComparison.Ordinal);
|
||||
if (text.StartsWith(word + (attachedArticle ? string.Empty : " "), StringComparison.OrdinalIgnoreCase))
|
||||
return text[word.Length..].TrimStart();
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Jellyfin.Plugin.Multilang.Services;
|
||||
|
||||
public readonly record struct ItemsProxyTransformResult(string Body, string[] ItemIds);
|
||||
|
||||
public readonly record struct ItemsProxyResolvedText(bool Change, string? Value);
|
||||
public readonly record struct ItemsProxyResolvedText(bool Change, string? Value, string? Language = null);
|
||||
|
||||
public readonly record struct ItemsProxyResolvedAsset(bool Change, string? Value);
|
||||
|
||||
@@ -61,9 +61,9 @@ public sealed class ItemsProxyTransformer
|
||||
if (ids.Length == 0)
|
||||
return new ItemsProxyTransformResult(body, ids);
|
||||
|
||||
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
var sortLocale = ItemsProxySorting.ResolveSortLocale(rules.SortLocale, controls.ClientLocale);
|
||||
var sortCulture = ItemsProxySorting.GetSortCulture(sortLocale);
|
||||
var sortArticles = ItemsProxySorting.GetSortArticles(_articleCatalog, Plugin.Instance?.Configuration?.ArticleEntries ?? [], sortLocale);
|
||||
|
||||
var localGenreIds = rules.Enabled ? ItemsProxyRequestBuilder.ParseLocalGenreIds(controls.GenreIds) : [];
|
||||
if (rules.Enabled || localGenreIds.Length > 0)
|
||||
@@ -80,10 +80,19 @@ public sealed class ItemsProxyTransformer
|
||||
ids = GetItemIds(items);
|
||||
}
|
||||
|
||||
if (rules.Enabled)
|
||||
ApplyRules(items, ids, rules, controls.ClientLocale, factsByItem);
|
||||
var titleLanguages = rules.Enabled
|
||||
? ApplyRules(items, ids, rules, controls.ClientLocale, factsByItem)
|
||||
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var jellyfinTitleLanguages = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ItemsProxySorting.Apply(root, controls, sortCulture, sortArticles);
|
||||
ItemsProxySorting.Apply(root, controls, sortCulture, item =>
|
||||
{
|
||||
var itemId = TryGetItemId(item);
|
||||
var language = itemId is not null && titleLanguages.TryGetValue(itemId, out var translatedLanguage)
|
||||
? translatedLanguage
|
||||
: ResolveJellyfinTitleLanguage(itemId, config, jellyfinTitleLanguages);
|
||||
return ItemsProxySorting.GetSortArticles(_articleCatalog, config.ArticleEntries, language, config.IgnoreArticlesWhenSorting);
|
||||
});
|
||||
return new ItemsProxyTransformResult(root.ToJsonString(new JsonSerializerOptions { WriteIndented = false }), ids);
|
||||
}
|
||||
|
||||
@@ -230,12 +239,12 @@ public sealed class ItemsProxyTransformer
|
||||
if (field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) &&
|
||||
action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(facts.OriginalTitle))
|
||||
return new ItemsProxyResolvedText(true, facts.OriginalTitle);
|
||||
return new ItemsProxyResolvedText(true, facts.OriginalTitle, facts.OriginalLanguage);
|
||||
if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var originalValue = GetTranslatedField(byLang, OriginalAction, field);
|
||||
if (!string.IsNullOrWhiteSpace(originalValue))
|
||||
return new ItemsProxyResolvedText(true, originalValue);
|
||||
return new ItemsProxyResolvedText(true, originalValue, facts.OriginalLanguage);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -244,7 +253,7 @@ public sealed class ItemsProxyTransformer
|
||||
|
||||
var value = GetTranslatedField(byLang, action[LanguagePrefix.Length..], field);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return new ItemsProxyResolvedText(true, value);
|
||||
return new ItemsProxyResolvedText(true, value, action[LanguagePrefix.Length..]);
|
||||
}
|
||||
|
||||
return field.Equals(TitleField, StringComparison.OrdinalIgnoreCase)
|
||||
@@ -347,7 +356,7 @@ public sealed class ItemsProxyTransformer
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private void ApplyRules(
|
||||
private Dictionary<string, string> ApplyRules(
|
||||
IEnumerable<JsonObject> items,
|
||||
string[] ids,
|
||||
UserRulesDocument rules,
|
||||
@@ -357,6 +366,7 @@ public sealed class ItemsProxyTransformer
|
||||
var langs = GetNeededLanguages(rules);
|
||||
var translations = _store.GetTranslations(ids, langs);
|
||||
var assets = _store.GetAssets(ids, langs);
|
||||
var titleLanguages = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var movieGenreNames = _store.GetGenreNames("movie", clientLocale);
|
||||
var tvGenreNames = _store.GetGenreNames("tv", clientLocale);
|
||||
|
||||
@@ -373,6 +383,8 @@ public sealed class ItemsProxyTransformer
|
||||
var title = ResolveField(TitleField, GetActionList(rules, category, TitleField), facts, byLang);
|
||||
if (title.Change)
|
||||
item["Name"] = title.Value ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(title.Language))
|
||||
titleLanguages[itemId] = title.Language;
|
||||
|
||||
var overview = ResolveField(OverviewField, GetActionList(rules, category, OverviewField), facts, byLang);
|
||||
if (overview.Change)
|
||||
@@ -395,6 +407,27 @@ public sealed class ItemsProxyTransformer
|
||||
ApplyBackdropAsset(item, ResolveAsset(BackdropKind, GetActionList(rules, category, BackdropKind), assetsByKind));
|
||||
ApplyGenres(item, facts, movieGenreNames, tvGenreNames);
|
||||
}
|
||||
|
||||
return titleLanguages;
|
||||
}
|
||||
|
||||
private string ResolveJellyfinTitleLanguage(
|
||||
string? itemId,
|
||||
PluginConfiguration config,
|
||||
Dictionary<string, string> resolvedLanguages)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(itemId))
|
||||
return config.JellyfinTitleLanguageFallback;
|
||||
|
||||
if (resolvedLanguages.TryGetValue(itemId, out var existing))
|
||||
return existing;
|
||||
|
||||
var language = Guid.TryParse(itemId, out var itemGuid)
|
||||
? _libraryManager.GetItemById(itemGuid)?.GetPreferredMetadataLanguage()
|
||||
: null;
|
||||
language = string.IsNullOrWhiteSpace(language) ? config.JellyfinTitleLanguageFallback : language;
|
||||
resolvedLanguages[itemId] = language ?? string.Empty;
|
||||
return resolvedLanguages[itemId];
|
||||
}
|
||||
|
||||
private static void ApplyImageAsset(JsonObject item, string imageType, ItemsProxyResolvedAsset asset)
|
||||
|
||||
Reference in New Issue
Block a user