From 3bf79a3996988ffd1b1165cafd622d7999cc2d1d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 10 Aug 2026 11:31:14 +0300 Subject: [PATCH] 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. --- README.md | 12 +++- tests/PLib.Tests/Library/MetadataScanTests.cs | 66 ++++++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index da405ff..fdb6991 100644 --- a/README.md +++ b/README.md @@ -183,9 +183,15 @@ dotnet test **Признак «уже размечено» — отдельная отметка, а не наличие описания.** Сначала прогон пропускал видео с непустым `Description`, и это ломалось на источниках, у которых есть название, актёры и теги, но нет синопсиса: такие видео возвращались на каждом запуске. - Теперь применение ставит `MetadataAppliedAt` и имя источника, а миграция проставляет - отметку тем, у кого описание уже есть, — иначе первый же запуск после обновления предложил - бы заново всё, что уже было сделано. + Хранится **набор** источников (`VideoMetadataSources`), а не последний применённый. + Одного поля хватало ровно до второго применения, после которого первый источник + забывался и предлагался заново. Набор же отвечает на вопрос, который прогон и задаёт, — + «этот источник уже высказался?», — и делает осмысленным режим «источники, которых ещё + не спрашивали»: у видео уже есть ответы PornDB, вопрос в том, что добавит StashDB. + Спрашивать того, кто уже ответил, значит потратить запрос, чтобы услышать написанное. + Режимов три: только неразмеченные, недостающие источники, всё заново. + Миграция переносит старую одиночную колонку в набор первой строкой, и делает это до + удаления колонок — сгенерированный вариант удалял их первыми и терял всё разом. - **Картинки скачиваются один раз и по размеру.** `images` в stash-box есть у сцены, у актёра и у студии; у тега такого поля нет вовсе, поэтому там карточка показывает первую букву — это норма, а не отсутствие данных. Обложка сцены нужна там, где выбирают из diff --git a/tests/PLib.Tests/Library/MetadataScanTests.cs b/tests/PLib.Tests/Library/MetadataScanTests.cs index 711eb02..a6697d3 100644 --- a/tests/PLib.Tests/Library/MetadataScanTests.cs +++ b/tests/PLib.Tests/Library/MetadataScanTests.cs @@ -50,7 +50,7 @@ public sealed class MetadataScanTests (await CollectAsync()).OfType().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().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().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().ShouldHaveSingleItem(); + matched.Matches.ShouldHaveSingleItem().Title.ShouldBe("Из нового источника"); + + await _provider.DidNotReceive().FindByPerceptualHashAsync( + known, + Arg.Any(), + Arg.Any()); + } + + [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().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 matches) => _provider.FindByPerceptualHashAsync(source, Arg.Any(), Arg.Any()) .Returns(matches);