Refactor metadata application logic in PLib video library manager to utilize a set of sources instead of a single source, improving the accuracy of metadata retrieval. Update tests to validate new behavior, ensuring that previously answered sources are not queried again and that multiple applications of the same source are recorded correctly. Revise README.md to reflect these changes and clarify the new metadata handling processes.

This commit is contained in:
Leonid Pershin
2026-08-10 11:31:14 +03:00
parent 70611c2051
commit 3bf79a3996
2 changed files with 72 additions and 6 deletions
+9 -3
View File
@@ -183,9 +183,15 @@ dotnet test
**Признак «уже размечено» — отдельная отметка, а не наличие описания.** Сначала прогон
пропускал видео с непустым `Description`, и это ломалось на источниках, у которых есть
название, актёры и теги, но нет синопсиса: такие видео возвращались на каждом запуске.
Теперь применение ставит `MetadataAppliedAt` и имя источника, а миграция проставляет
отметку тем, у кого описание уже есть, — иначе первый же запуск после обновления предложил
бы заново всё, что уже было сделано.
Хранится **набор** источников (`VideoMetadataSources`), а не последний применённый.
Одного поля хватало ровно до второго применения, после которого первый источник
забывался и предлагался заново. Набор же отвечает на вопрос, который прогон и задаёт, —
«этот источник уже высказался?», — и делает осмысленным режим «источники, которых ещё
не спрашивали»: у видео уже есть ответы PornDB, вопрос в том, что добавит StashDB.
Спрашивать того, кто уже ответил, значит потратить запрос, чтобы услышать написанное.
Режимов три: только неразмеченные, недостающие источники, всё заново.
Миграция переносит старую одиночную колонку в набор первой строкой, и делает это до
удаления колонок — сгенерированный вариант удалял их первыми и терял всё разом.
- **Картинки скачиваются один раз и по размеру.** `images` в stash-box есть у сцены,
у актёра и у студии; у тега такого поля нет вовсе, поэтому там карточка показывает первую
букву — это норма, а не отсутствие данных. Обложка сцены нужна там, где выбирают из
+63 -3
View File
@@ -50,7 +50,7 @@ public sealed class MetadataScanTests
(await CollectAsync()).OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(1);
// Asking again for everything is what the switch is for.
var all = await CollectAsync(new MetadataScanRequest(OnlyWithoutMetadata: false));
var all = await CollectAsync(new MetadataScanRequest(MetadataScanScope.Everything));
all.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(2);
}
@@ -66,8 +66,8 @@ public sealed class MetadataScanTests
await CollectAsync(new MetadataScanRequest(ApplyUnambiguous: true));
video.Description.ShouldBeNull();
video.MetadataAppliedAt.ShouldNotBeNull();
video.MetadataSource.ShouldBe("StashDB");
video.HasMetadata.ShouldBeTrue();
video.AppliedSources.ShouldHaveSingleItem().SourceName.ShouldBe("StashDB");
// Judging "already done" by the description sent these back on every single run.
var again = await CollectAsync(new MetadataScanRequest(ApplyUnambiguous: true));
@@ -148,6 +148,66 @@ public sealed class MetadataScanTests
events.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(3);
}
[Fact]
public async Task A_new_source_can_be_asked_about_videos_the_old_one_already_answered()
{
var video = Video("видео", hash: 1);
video.MarkMetadataApplied("PornDb");
_videos.Seed(video);
var fresh = new MetadataSourceOptions { Name = "StashDB", Endpoint = "https://stashdb/graphql" };
var known = new MetadataSourceOptions { Name = "PornDb", Endpoint = "https://porndb/graphql" };
Answer(fresh, [Match("Из нового источника")]);
Answer(known, [Match("Из старого источника")]);
var events = await CollectAsync(
new MetadataScanRequest(MetadataScanScope.MissingSources),
known,
fresh);
// The point of the mode: the source that already had its say is not asked to repeat
// itself, and only what the new one adds comes back.
var matched = events.OfType<MetadataScanEvent.Matched>().ShouldHaveSingleItem();
matched.Matches.ShouldHaveSingleItem().Title.ShouldBe("Из нового источника");
await _provider.DidNotReceive().FindByPerceptualHashAsync(
known,
Arg.Any<ulong>(),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task A_video_every_source_has_already_answered_is_left_out_of_that_mode()
{
var video = Video("видео", hash: 1);
video.MarkMetadataApplied("StashDB");
_videos.Seed(video);
Answer(_source, [Match("Название")]);
var events = await CollectAsync(new MetadataScanRequest(MetadataScanScope.MissingSources));
events.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(0);
}
[Fact]
public async Task Applying_the_same_source_twice_records_it_once()
{
var video = Video("видео", hash: 1);
_videos.Seed(video);
video.MarkMetadataApplied("StashDB");
video.MarkMetadataApplied("stashdb");
// Free text the user typed: "PornDb" and "PornDB" are plainly the same source to
// everyone but a byte comparison.
video.AppliedSources.ShouldHaveSingleItem();
video.HasMetadataFrom("STASHDB").ShouldBeTrue();
await Task.CompletedTask;
}
private void Answer(MetadataSourceOptions source, IReadOnlyList<VideoMetadataMatch> matches) =>
_provider.FindByPerceptualHashAsync(source, Arg.Any<ulong>(), Arg.Any<CancellationToken>())
.Returns(matches);