Implement animated previews and perceptual hashing in PLib video library manager. Introduce IAnimatedPreviewGenerator and IVideoPerceptualHasher interfaces, enhancing video item metadata with animated preview paths and perceptual hashes. Update LibraryService to manage indexing in three passes: metadata, animated previews, and perceptual hashes. Revise UI components to display animated previews and manage duplicate video detection. Enhance README.md to document these new features and usage instructions.

This commit is contained in:
Leonid Pershin
2026-08-09 07:48:45 +03:00
parent 10c66baea8
commit 3c77baced7
36 changed files with 2711 additions and 471 deletions
+37 -2
View File
@@ -9,6 +9,8 @@
- Слежение за папками: новые файлы подхватываются сами, без кнопки.
- Метаданные (длительность, разрешение, кодек) через ffprobe.
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
- Анимированное превью: наведите курсор на карточку — вместо постера прокручиваются
кадры, снятые по всей длительности.
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
- Настройки — боковой панелью в том же окне (сетка сдвигается, а не перекрывается): папки
библиотеки с удалением, параметры превью и сканирования, тема, очистка кэша. Всё пишется
@@ -95,6 +97,35 @@ dotnet test
экрана не доходили — чёрный экран и на GPU-, и на CPU-пути, при полностью рабочем в
приложении `OpenGlControlBase`. Нативное окно VLC через `NativeControlHost`: картинка
появилась, но окно поверх поверхности Avalonia не пропускает ни клик, ни оверлей.
- **Индексация в три прохода.** Сначала метаданные и постеры для всех файлов, затем
анимированные превью, и только потом отпечатки. Каждый следующий проход берёт больше кадров
на файл; вперемешку они задерживали бы каждую следующую карточку на всю цепочку, и сетка
наполнялась бы в разы медленнее. Порядок — по видимости: постер нужен, чтобы карточка
вообще появилась, анимация — чтобы она ожила под курсором, хеш всплывает только при поиске
дублей. Поэтому `IsIndexed` намеренно не включает ни анимацию, ни хеш: это готовность к
показу, а не завершённость всей обработки. Поздние проходы умеют стартовать только после
первого — кадры распределяются по длительности, а её устанавливает probe.
- **Анимированное превью — кадры стопкой в одном JPEG, не GIF.** Гифку никто ниже по течению
не проиграет: Avalonia декодирует только первый кадр анимированного изображения, так что
за GIF пришлось бы тащить отдельный декодер. Стопка кадров обходится тем же декодером, что
и постеры — `FilmstripImage` просто рисует каждый тик другой срез, — и весит долю от
256-цветной гифки тех же кадров, а это цена за каждое видео в библиотеке. Рендерит один
процесс ffmpeg: по входу на таймкод (seek до входа, то есть прыжок на ключевой кадр,
а не декодирование до него) и один `vstack`; процесс на кадр умножил бы проход на их число.
Число кадров зашито в имя файла, поэтому смена настройки не режет старую полосу неверным
шагом — она просто промахивается мимо кэша, а лишнее подберёт очистка.
Полоса живёт только пока играет: она весит как все её кадры вместе, а под курсором всегда
одна карточка, так что держать её в общем кэше значило бы менять ограниченную память на
растущую с тем, сколько библиотеки пролистали.
- **pHash по рецепту stash.** 25 кадров сеткой 5×5, по 5% времени отброшено с каждого
конца, кадр по ширине 160 — то есть хеш описывает те же кадры, что и у stash. Декодирует
и масштабирует ffmpeg, отдавая сырой 8-битный серый, поэтому графическая библиотека не
нужна вовсе: монтаж, уменьшение до 64×64 и DCT — арифметика над массивом байт. Бит
ставится сравнением коэффициента с медианой блока 8×8.
Сравнивается хеш расстоянием Хэмминга, а не на равенство; группы дублей собираются
системой непересекающихся множеств, чтобы цепочка «A похож на B, B на C» дала одну группу.
**Побитовая совместимость со stash не проверена** — разные реализации ресайза способны
перевернуть биты у коэффициентов рядом с медианой.
- **Теги и коллекции — одна сущность.** `LibraryLabel` с `LabelKind`: связь с видео у них
одинаковая, различается только назначение. Одна сущность — одна таблица связей, один
репозиторий и одно правило именования; разделить потом можно переименованием и миграцией,
@@ -107,16 +138,20 @@ dotnet test
разницу и так умеет считать сканирование.
- **Кэш превью самовосстанавливается.** Диск — ключ `sha256(путь|размер|mtime)`, память — LRU
на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически
на месте (`IThumbnailGenerator.IsAvailable`), и перерисовывает удалённые; после полного
на месте (`IMediaArtifactCache.IsAvailable`), и перерисовывает удалённые; после полного
прохода лишние файлы вычищаются (`PurgeUnusedAsync`). Незавершённые `.tmp` удаляются
только если им больше часа — иначе можно снести рендер второго запущенного экземпляра.
Постеры и анимации различаются лишь тем, что просят у ffmpeg, а хозяйство у них одно, и
описано оно один раз: иначе размер кэша в настройках начал бы врать в тот же день, когда
появился второй вид файлов.
## Данные
Всё пользовательское лежит в `%LOCALAPPDATA%\PLib`:
- `library.db` — SQLite с метаданными;
- `library.db` — SQLite с метаданными, метками, прогрессом просмотра и pHash;
- `thumbnails/` — кэш постеров (ключ = путь + размер + время изменения файла);
- `previews/` — кэш анимированных превью, тот же ключ плюс число кадров в имени;
- `settings.json` — папки, параметры превью и сканирования, тема, громкость;
перечитывается на лету;
- `logs/` — Serilog, ротация по дням.