5920f7a9408a76ce053c35c10b26979295a8718f
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 — единственное место, которое надо будет тронуть).
Languages
C#
99.8%
Batchfile
0.2%