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:
Leonid Pershin
2026-08-10 09:25:41 +03:00
parent 9e239e045f
commit b82b0b4555
38 changed files with 4661 additions and 3316 deletions
@@ -51,11 +51,11 @@ public sealed class StashBoxPayloadTests
first.RemoteId.ShouldBe("abc");
first.Title.ShouldBe("Первая");
first.Description.ShouldBe("Описание");
first.Studios.ShouldBe(["Студия"]);
first.Tags.ShouldBe(["драма", "нуар"]);
first.Studios.Select(x => x.Name).ShouldBe(["Студия"]);
first.Tags.Select(x => x.Name).ShouldBe(["драма", "нуар"]);
// The credited alias is not the person; the label has to be the performer's own name.
first.Performers.ShouldBe(["Актёр"]);
first.Performers.Select(x => x.Name).ShouldBe(["Актёр"]);
matches[1].Studios.ShouldBeEmpty();
}
@@ -94,6 +94,119 @@ public sealed class StashBoxPayloadTests
match.Tags.ShouldBeEmpty();
}
[Fact]
public void The_smallest_picture_still_wide_enough_for_a_card_is_the_one_kept()
{
// stash-box returns every size it holds, and the first is not the best: originals run
// to several thousand pixels, and one of those per performer to draw it 150 wide would
// cost megabytes a head.
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"studio": { "name": "Студия", "images": [ { "url": "s/4000.jpg", "width": 4000 }, { "url": "s/500.jpg", "width": 500 } ] },
"tags": [ { "name": "драма" } ],
"performers": [
{ "performer": { "name": "Актёр", "images": [
{ "url": "p/2000.jpg", "width": 2000 },
{ "url": "p/400.jpg", "width": 400 },
{ "url": "p/100.jpg", "width": 100 } ] } }
]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.Performers.Single().ImageUrl.ShouldBe("p/400.jpg");
match.Studios.Single().ImageUrl.ShouldBe("s/500.jpg");
// stash-box holds no picture for a tag at all, so there is nothing to find.
match.Tags.Single().ImageUrl.ShouldBeNull();
}
[Fact]
public void The_scene_carries_its_own_cover_apart_from_the_pictures_of_its_people()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"images": [ { "url": "scene/1920.jpg", "width": 1920 }, { "url": "scene/640.jpg", "width": 640 } ],
"performers": [ { "performer": { "name": "Актёр", "images": [ { "url": "p/400.jpg", "width": 400 } ] } } ]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.ImageUrl.ShouldBe("scene/640.jpg");
match.Performers.Single().ImageUrl.ShouldBe("p/400.jpg");
// Still a URL, and it stays one: whether to spend the bandwidth is the caller's
// decision, and a candidate must never wait on a picture host to be shown.
match.ImageUrl.ShouldNotBeNull();
}
[Fact]
public void When_every_picture_is_too_small_the_widest_is_taken_rather_than_none()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"performers": [
{ "performer": { "name": "Актёр", "images": [
{ "url": "p/80.jpg", "width": 80 },
{ "url": "p/200.jpg", "width": 200 } ] } }
]
}
]
}
}
""";
// A small picture beats an empty card.
Read(payload, Flat).Single().Performers.Single().ImageUrl.ShouldBe("p/200.jpg");
}
[Fact]
public void An_entity_with_no_pictures_at_all_is_still_a_match()
{
const string payload = """
{
"data": {
"findSceneByFingerprint": [
{
"id": "abc",
"title": "Сцена",
"studio": { "name": "Студия", "images": [] },
"performers": [ { "performer": { "name": "Актёр" } } ]
}
]
}
}
""";
var match = Read(payload, Flat).ShouldHaveSingleItem();
match.Studios.Single().ImageUrl.ShouldBeNull();
match.Performers.Single().Name.ShouldBe("Актёр");
}
[Fact]
public void Errors_are_raised_even_though_the_server_answered_two_hundred()
{