6.9 KiB
6.9 KiB
PLib
Менеджер видеотеки на Avalonia: сканирует папки, вытаскивает превью через ffmpeg и показывает всё сеткой карточек.
Что уже работает
- Сканирование указанных папок, инкрементальное — файл, который не изменился, не переиндексируется.
- Метаданные (длительность, разрешение, кодек) через ffprobe.
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
- Настройки — выдвижной панелью в том же окне: папки библиотеки (с удалением), параметры
превью и сканирования, тема, очистка кэша. Всё пишется в
settings.jsonи подхватывается без перезапуска. - Светлая, тёмная и системная темы; выбор запоминается.
- Клик или Enter по карточке — открыть в системном плеере, правая кнопка — контекстное меню.
Требования
- .NET 10 SDK
ffmpegиffprobeвPATH
Запуск
dotnet run --project src/PLib.Desktop
dotnet test
Архитектура
Четыре слоя, зависимости направлены только внутрь:
| Проект | Отвечает за | Знает о |
|---|---|---|
PLib.Domain |
Сущность VideoItem и её инварианты |
ни о чём |
PLib.Application |
Сценарии (LibraryService) и абстракции портов |
Domain |
PLib.Infrastructure |
EF Core + SQLite, ffmpeg, файловая система | Application |
PLib.Desktop |
Avalonia, ViewModel'и, composition root | Infrastructure |
Ключевые решения:
- MVVM на ReactiveUI. Свойства —
[Reactive]изReactiveUI.SourceGenerators, команды —ReactiveCommand, производные значения (IsScanning,IsEmpty) —ToProperty. Отмена сканирования сделана штатным способом: скан живёт как observable, аCancelScanCommandпросто отписывает его черезTakeUntil, что отменяетCancellationToken. - Сетка — проекция DynamicData, а не пересборка.
SourceCache→AutoRefresh→Filter→SortAndBindотдаёт диффы: добавился один файл — одна вставка в нужную позицию. Скролл, контейнерыItemsRepeaterи уже загруженные превью остаются на месте. Поиск дебаунсится на 200 мс, изменения карточек во время скана коалесцируются в 250 мс. - Сканирование — поток событий.
ILibraryService.ScanAsyncвозвращаетIAsyncEnumerable<LibraryScanEvent>: карточки появляются по мере находок, а не после завершения всего прохода. Тяжёлая часть (ffprobe + ffmpeg) идёт параллельно черезParallel.ForEachAsync, результаты собираются вChannelи применяются к сущностям по одному — трекер изменений EF не потокобезопасен. - Вся работа вне UI-потока. ViewModel оборачивает конвейер в
Task.Runи возвращает каждое событие в UI явно черезDispatcher.UIThread. - Превью живут только пока видны.
AsyncImageзапрашивает битмап при попадании в визуальное дерево и отпускает при выходе;ThumbnailCache— LRU на 256 записей с декодированием в нужную ширину. Память зависит от размера окна, а не от размера библиотеки. - Scope на операцию.
DbContextживёт ровно одну операцию — ViewModel берётIServiceScopeFactoryи создаёт scope на каждый вызов. - Одно окно. Настройки — панель поверх сетки, а не второе окно: библиотека остаётся
видна, в alt-tab ничего не добавляется, и приложение остаётся переносимым на
ISingleViewApplicationLifetime, гдеShowDialogпопросту не существует. - Настройки — рабочая копия. Панель правит снимок
AppSettingsи записывает его целиком только по «Сохранить», так что отмена не оставляет следов. Пересканирование запускается только если изменилось то, что влияет на состав библиотеки, — смена темы или ширины кадра его не вызывает. - Кэш превью самовосстанавливается. Диск — ключ
sha256(путь|размер|mtime), память — LRU на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически на месте (IThumbnailGenerator.IsAvailable), и перерисовывает удалённые; после полного прохода лишние файлы вычищаются (PurgeUnusedAsync). Незавершённые.tmpудаляются только если им больше часа — иначе можно снести рендер второго запущенного экземпляра.
Данные
Всё пользовательское лежит в %LOCALAPPDATA%\PLib:
library.db— SQLite с метаданными;thumbnails/— кэш постеров (ключ = путь + размер + время изменения файла);settings.json— список папок, перечитывается на лету;logs/— Serilog, ротация по дням.
Схема создаётся через EnsureCreated. Когда форма таблицы устоится — заменить на
миграции EF Core (DatabaseInitializer — единственное место, которое надо будет тронуть).