Release 0.2.5: harden proxy and streamline metadata fetching
Remove obsolete rule formats, batch SQLite work, preserve artwork source URLs, and coordinate refresh workers with maintenance. Add regression coverage and document backup format changes.
This commit is contained in:
@@ -22,6 +22,9 @@ public readonly record struct ItemsProxyCategoryMatch(
|
||||
UserCategoryRule? Effective,
|
||||
bool DuplicateLabelResolved);
|
||||
|
||||
public sealed record ItemsProxyResolutionAttempt(
|
||||
string Action, string Source, string LookupKey, bool HasValue, bool Chosen, string? Value, string Reason);
|
||||
|
||||
public sealed class ItemsProxyTransformer
|
||||
{
|
||||
private readonly TranslationStore _store;
|
||||
@@ -157,8 +160,6 @@ public sealed class ItemsProxyTransformer
|
||||
{
|
||||
foreach (var action in GetAllActionTokens(category.FieldActionLists))
|
||||
AddLanguageAction(langs, action);
|
||||
foreach (var action in (category.FieldActions ?? []).Values)
|
||||
AddLanguageAction(langs, action);
|
||||
}
|
||||
|
||||
foreach (var action in GetAllActionTokens(rules.FallbackFieldActions))
|
||||
@@ -184,29 +185,28 @@ public sealed class ItemsProxyTransformer
|
||||
}
|
||||
|
||||
public FactsData GetClassificationFacts(UserRulesDocument rules, FactsData facts)
|
||||
=> GetClassificationFacts(rules, new Dictionary<string, FactsData> { [facts.ItemId] = facts })[facts.ItemId];
|
||||
|
||||
private IReadOnlyDictionary<string, FactsData> GetClassificationFacts(UserRulesDocument rules, IReadOnlyDictionary<string, FactsData> facts)
|
||||
{
|
||||
if (rules.TrustTmdbCollections ||
|
||||
!facts.Kind.Equals("collection", StringComparison.OrdinalIgnoreCase) ||
|
||||
!Guid.TryParseExact(facts.ItemId, "N", out var itemGuid))
|
||||
{
|
||||
if (rules.TrustTmdbCollections)
|
||||
return facts;
|
||||
}
|
||||
var children = facts.Values.Where(fact => fact.Kind == "collection")
|
||||
.ToDictionary(fact => fact.ItemId, fact =>
|
||||
(_libraryManager.GetItemById(Guid.Parse(fact.ItemId)) as Folder)?.GetLinkedChildren()
|
||||
.OfType<MediaBrowser.Controller.Entities.Movies.Movie>()
|
||||
.Select(child => TranslationStore.ToItemId32(child.Id)).ToArray() ?? []);
|
||||
var childFacts = _store.GetFactsForItems(children.Values.SelectMany(ids => ids));
|
||||
var result = new Dictionary<string, FactsData>(facts, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (id, ids) in children)
|
||||
result[id] = AggregateCollectionFacts(facts[id], ids.Where(childFacts.ContainsKey).Select(id => childFacts[id]).ToArray());
|
||||
return result;
|
||||
}
|
||||
|
||||
var item = _libraryManager.GetItemById(itemGuid);
|
||||
var children = (item as Folder)?.GetLinkedChildren()
|
||||
.Where(child => child.GetType().Name.Equals("Movie", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray() ?? [];
|
||||
|
||||
var childIds = children.Select(child => TranslationStore.ToItemId32(child.Id)).ToArray();
|
||||
if (childIds.Length == 0)
|
||||
return facts;
|
||||
|
||||
var childFacts = _store.GetFactsForItems(childIds).Values
|
||||
.Where(child => child.Kind.Equals("movie", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
public static FactsData AggregateCollectionFacts(FactsData facts, FactsData[] childFacts)
|
||||
{
|
||||
if (childFacts.Length == 0)
|
||||
return facts;
|
||||
|
||||
return facts with
|
||||
{
|
||||
OriginalLanguage = SharedSingle(childFacts.Select(child => child.OriginalLanguage)),
|
||||
@@ -228,74 +228,61 @@ public sealed class ItemsProxyTransformer
|
||||
if (actions is not null && actions.TryGetValue(field, out var list))
|
||||
return list;
|
||||
|
||||
return category is not null && category.FieldActions is not null
|
||||
? LegacyActionToList(field, category.FieldActions)
|
||||
: [JellyfinAction];
|
||||
return [JellyfinAction];
|
||||
}
|
||||
|
||||
public static ItemsProxyResolvedText ResolveField(
|
||||
string field,
|
||||
IReadOnlyList<string> actions,
|
||||
FactsData facts,
|
||||
Dictionary<string, Dictionary<string, string>>? byLang)
|
||||
string field, IReadOnlyList<string> actions, FactsData facts,
|
||||
Dictionary<string, Dictionary<string, string>>? byLang,
|
||||
Action<ItemsProxyResolutionAttempt>? trace = null)
|
||||
{
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase))
|
||||
return new ItemsProxyResolvedText(false, null);
|
||||
if (field.Equals(TitleField, StringComparison.OrdinalIgnoreCase) &&
|
||||
action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(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, facts.OriginalLanguage);
|
||||
continue;
|
||||
trace?.Invoke(new(action, JellyfinAction, "", true, true, null, "Use Jellyfin value"));
|
||||
return new(false, null);
|
||||
}
|
||||
|
||||
if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
var original = action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase);
|
||||
if (!original && !action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var value = GetTranslatedField(byLang, action[LanguagePrefix.Length..], field);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return new ItemsProxyResolvedText(true, value, action[LanguagePrefix.Length..]);
|
||||
var lang = original ? OriginalAction : action[LanguagePrefix.Length..];
|
||||
var originalTitle = original && field == TitleField && !string.IsNullOrWhiteSpace(facts.OriginalTitle);
|
||||
var value = originalTitle ? facts.OriginalTitle : GetTranslatedField(byLang, lang, field);
|
||||
var found = !string.IsNullOrWhiteSpace(value);
|
||||
trace?.Invoke(new(action, originalTitle ? "OriginalTitle" : "Translation",
|
||||
originalTitle ? "facts.original_title" : lang + "/" + field,
|
||||
found, found, value, found ? "Value found" : "Value missing or empty"));
|
||||
if (found)
|
||||
return new(true, value, original ? facts.OriginalLanguage : lang);
|
||||
}
|
||||
|
||||
return field.Equals(TitleField, StringComparison.OrdinalIgnoreCase)
|
||||
? new ItemsProxyResolvedText(false, null)
|
||||
: new ItemsProxyResolvedText(true, string.Empty);
|
||||
return field == TitleField ? new(false, null) : new(true, string.Empty);
|
||||
}
|
||||
|
||||
public static ItemsProxyResolvedAsset ResolveAsset(
|
||||
string kind,
|
||||
IReadOnlyList<string> actions,
|
||||
Dictionary<string, Dictionary<string, string>>? byKind)
|
||||
string kind, IReadOnlyList<string> actions,
|
||||
Dictionary<string, Dictionary<string, string>>? byKind,
|
||||
Action<ItemsProxyResolutionAttempt>? trace = null)
|
||||
{
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (action.Equals(JellyfinAction, StringComparison.OrdinalIgnoreCase))
|
||||
return new ItemsProxyResolvedAsset(false, null);
|
||||
if (action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var originalValue = GetAsset(byKind, kind, OriginalAction);
|
||||
if (!string.IsNullOrWhiteSpace(originalValue))
|
||||
return new ItemsProxyResolvedAsset(true, originalValue);
|
||||
continue;
|
||||
trace?.Invoke(new(action, JellyfinAction, "", true, true, null, "Use Jellyfin image"));
|
||||
return new(false, null);
|
||||
}
|
||||
|
||||
if (!action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
var original = action.Equals(OriginalAction, StringComparison.OrdinalIgnoreCase);
|
||||
if (!original && !action.StartsWith(LanguagePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var value = GetAsset(byKind, kind, action[LanguagePrefix.Length..]);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return new ItemsProxyResolvedAsset(true, value);
|
||||
var lang = original ? OriginalAction : action[LanguagePrefix.Length..];
|
||||
var value = GetAsset(byKind, kind, lang);
|
||||
var found = !string.IsNullOrWhiteSpace(value);
|
||||
trace?.Invoke(new(action, "Asset", kind + "/" + lang, found, found, value,
|
||||
found ? "Asset found" : "Asset missing"));
|
||||
if (found)
|
||||
return new(true, value);
|
||||
}
|
||||
|
||||
return kind.Equals(PosterKind, StringComparison.OrdinalIgnoreCase)
|
||||
? new ItemsProxyResolvedAsset(false, null)
|
||||
: new ItemsProxyResolvedAsset(true, string.Empty);
|
||||
return kind == PosterKind ? new(false, null) : new(true, string.Empty);
|
||||
}
|
||||
|
||||
public static string? GetTranslatedField(
|
||||
@@ -373,6 +360,7 @@ public sealed class ItemsProxyTransformer
|
||||
var langs = GetNeededLanguages(rules);
|
||||
var translations = _store.GetTranslations(ids, langs);
|
||||
var assets = _store.GetAssets(ids, langs);
|
||||
var classification = GetClassificationFacts(rules, factsByItem);
|
||||
var titleLanguages = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var movieGenreNames = _store.GetGenreNames("movie", clientLocale);
|
||||
var tvGenreNames = _store.GetGenreNames("tv", clientLocale);
|
||||
@@ -384,7 +372,7 @@ public sealed class ItemsProxyTransformer
|
||||
continue;
|
||||
|
||||
translations.TryGetValue(itemId, out var byLang);
|
||||
var classificationFacts = GetClassificationFacts(rules, facts);
|
||||
var classificationFacts = classification[itemId];
|
||||
var category = PickCategoryMatch(rules, classificationFacts).Effective;
|
||||
|
||||
var title = ResolveField(TitleField, GetActionList(rules, category, TitleField), facts, byLang);
|
||||
@@ -583,15 +571,6 @@ public sealed class ItemsProxyTransformer
|
||||
langs.Add(action[LanguagePrefix.Length..]);
|
||||
}
|
||||
|
||||
private static string[] LegacyActionToList(string field, IReadOnlyDictionary<string, string>? legacyActions)
|
||||
{
|
||||
if (legacyActions is null || !legacyActions.TryGetValue(field, out var action))
|
||||
return [JellyfinAction];
|
||||
if (string.IsNullOrWhiteSpace(action) || action.Equals(FallbackAction, StringComparison.OrdinalIgnoreCase))
|
||||
return [JellyfinAction];
|
||||
return [action];
|
||||
}
|
||||
|
||||
private static string SharedSingle(IEnumerable<string> values)
|
||||
{
|
||||
var unique = values
|
||||
|
||||
Reference in New Issue
Block a user