Refactor ILibraryService and LibraryService to support new metadata handling features, including remote image management and enhanced label summaries. Update LabelSummary to include ImagePath for better visual representation. Revise MetadataMatchViewModel and MetadataScanViewModel to accommodate new image loading logic. Enhance README.md to document these updates and new functionalities.
This commit is contained in:
@@ -27,6 +27,9 @@ public sealed class StashBoxMetadataProvider(
|
||||
/// <summary>Name of the configured <see cref="HttpClient"/>; see the DI registration.</summary>
|
||||
public const string HttpClientName = "metadata";
|
||||
|
||||
/// <summary>Below this a picture is too small for a card and gets passed over.</summary>
|
||||
private const int MinimumImageWidth = 320;
|
||||
|
||||
/// <summary>
|
||||
/// The fingerprint queries, newest first.
|
||||
/// </summary>
|
||||
@@ -48,9 +51,10 @@ public sealed class StashBoxMetadataProvider(
|
||||
id
|
||||
title
|
||||
details
|
||||
studio { name }
|
||||
images { url width }
|
||||
studio { name images { url width } }
|
||||
tags { name }
|
||||
performers { performer { name } }
|
||||
performers { performer { name images { url width } } }
|
||||
}
|
||||
}
|
||||
""",
|
||||
@@ -65,9 +69,10 @@ public sealed class StashBoxMetadataProvider(
|
||||
id
|
||||
title
|
||||
details
|
||||
studio { name }
|
||||
images { url width }
|
||||
studio { name images { url width } }
|
||||
tags { name }
|
||||
performers { performer { name } }
|
||||
performers { performer { name images { url width } } }
|
||||
}
|
||||
}
|
||||
""",
|
||||
@@ -214,9 +219,13 @@ public sealed class StashBoxMetadataProvider(
|
||||
Text(scene, "id"),
|
||||
Text(scene, "title") ?? "Без названия",
|
||||
Text(scene, "details"),
|
||||
Names(scene, "tags"),
|
||||
Entities(scene, "tags"),
|
||||
Performers(scene),
|
||||
Studios(scene));
|
||||
Studios(scene),
|
||||
|
||||
// The scene's own cover. Left as a URL here — fetching it is the application layer's
|
||||
// call, because whether it is worth downloading depends on what asked for the match.
|
||||
PickImage(scene));
|
||||
|
||||
/// <summary>
|
||||
/// A string property, or <c>null</c> for anything else — including when the element itself
|
||||
@@ -230,7 +239,8 @@ public sealed class StashBoxMetadataProvider(
|
||||
? value.GetString()
|
||||
: null;
|
||||
|
||||
private static IReadOnlyList<string> Names(JsonElement scene, string property)
|
||||
/// <summary>A named array — tags, and anything else shaped like them.</summary>
|
||||
private static IReadOnlyList<MetadataEntity> Entities(JsonElement scene, string property)
|
||||
{
|
||||
if (scene.ValueKind != JsonValueKind.Object ||
|
||||
!scene.TryGetProperty(property, out var array) ||
|
||||
@@ -239,14 +249,14 @@ public sealed class StashBoxMetadataProvider(
|
||||
return [];
|
||||
}
|
||||
|
||||
return [.. array.EnumerateArray().Select(item => Text(item, "name")).OfType<string>()];
|
||||
return [.. array.EnumerateArray().Select(ReadEntity).OfType<MetadataEntity>()];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performers arrive wrapped in an appearance — the same person can be credited under a
|
||||
/// different name on a given scene — and it is the person's name we want.
|
||||
/// different name on a given scene — and it is the person we want, name and face both.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<string> Performers(JsonElement scene)
|
||||
private static IReadOnlyList<MetadataEntity> Performers(JsonElement scene)
|
||||
{
|
||||
if (scene.ValueKind != JsonValueKind.Object ||
|
||||
!scene.TryGetProperty("performers", out var array) ||
|
||||
@@ -261,9 +271,9 @@ public sealed class StashBoxMetadataProvider(
|
||||
.EnumerateArray()
|
||||
.Select(appearance => appearance.ValueKind == JsonValueKind.Object &&
|
||||
appearance.TryGetProperty("performer", out var performer)
|
||||
? Text(performer, "name")
|
||||
? ReadEntity(performer)
|
||||
: null)
|
||||
.OfType<string>()
|
||||
.OfType<MetadataEntity>()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -271,13 +281,61 @@ public sealed class StashBoxMetadataProvider(
|
||||
/// A scene has at most one studio, and often none — the shape is a list because a match
|
||||
/// may have nothing to say here.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<string> Studios(JsonElement scene) =>
|
||||
private static IReadOnlyList<MetadataEntity> Studios(JsonElement scene) =>
|
||||
scene.ValueKind == JsonValueKind.Object &&
|
||||
scene.TryGetProperty("studio", out var studio) &&
|
||||
Text(studio, "name") is { } name
|
||||
? [name]
|
||||
ReadEntity(studio) is { } entity
|
||||
? [entity]
|
||||
: [];
|
||||
|
||||
private static MetadataEntity? ReadEntity(JsonElement element) =>
|
||||
Text(element, "name") is { } name ? new MetadataEntity(name, PickImage(element)) : null;
|
||||
|
||||
/// <summary>
|
||||
/// Picks the picture to keep: the smallest one still wide enough for a card, and the
|
||||
/// widest available when none reaches that.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// stash-box returns every size it holds, and the first is not the best — originals run to
|
||||
/// several thousand pixels. Downloading one of those per performer to draw it 150 pixels
|
||||
/// wide would cost megabytes a head and look no better for it. Tags have no images field
|
||||
/// at all, so this simply finds nothing for them.
|
||||
/// </remarks>
|
||||
private static string? PickImage(JsonElement owner)
|
||||
{
|
||||
if (owner.ValueKind != JsonValueKind.Object ||
|
||||
!owner.TryGetProperty("images", out var images) ||
|
||||
images.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidates = images
|
||||
.EnumerateArray()
|
||||
.Where(image => image.ValueKind == JsonValueKind.Object)
|
||||
.Select(image => (Url: Text(image, "url"), Width: Number(image, "width")))
|
||||
.Where(image => !string.IsNullOrWhiteSpace(image.Url))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var enough = candidates.Where(image => image.Width >= MinimumImageWidth).ToList();
|
||||
|
||||
return enough.Count > 0
|
||||
? enough.MinBy(image => image.Width).Url
|
||||
: candidates.MaxBy(image => image.Width).Url;
|
||||
}
|
||||
|
||||
private static int Number(JsonElement element, string property) =>
|
||||
element.TryGetProperty(property, out var value) &&
|
||||
value.ValueKind == JsonValueKind.Number &&
|
||||
value.TryGetInt32(out var number)
|
||||
? number
|
||||
: 0;
|
||||
|
||||
/// <summary>One way of asking the same question, and how to read the answer.</summary>
|
||||
/// <param name="Field">Name of the query root field, used to find it in the reply.</param>
|
||||
/// <param name="IsNested">True when the result is a list of lists rather than a list.</param>
|
||||
|
||||
Reference in New Issue
Block a user