80 lines
5.7 KiB
Markdown
80 lines
5.7 KiB
Markdown
# PLib
|
||
|
||
Менеджер видеотеки на Avalonia: сканирует папки, вытаскивает превью через ffmpeg и
|
||
показывает всё сеткой карточек.
|
||
|
||
## Что уже работает
|
||
|
||
- Сканирование указанных папок, инкрементальное — файл, который не изменился, не переиндексируется.
|
||
- Метаданные (длительность, разрешение, кодек) через ffprobe.
|
||
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
|
||
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
|
||
- Светлая и тёмная темы.
|
||
- Клик или Enter по карточке — открыть в системном плеере, правая кнопка — контекстное меню.
|
||
|
||
## Требования
|
||
|
||
- .NET 10 SDK
|
||
- `ffmpeg` и `ffprobe` в `PATH`
|
||
|
||
## Запуск
|
||
|
||
```bash
|
||
dotnet run --project src/PLib.Desktop
|
||
```
|
||
|
||
```bash
|
||
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 на каждый вызов.
|
||
- **Кэш превью самовосстанавливается.** Диск — ключ `sha256(путь|размер|mtime)`, память — LRU
|
||
на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически
|
||
на месте (`IThumbnailGenerator.IsAvailable`), и перерисовывает удалённые; после полного
|
||
прохода лишние файлы вычищаются (`PurgeUnusedAsync`). Незавершённые `.tmp` удаляются
|
||
только если им больше часа — иначе можно снести рендер второго запущенного экземпляра.
|
||
|
||
## Данные
|
||
|
||
Всё пользовательское лежит в `%LOCALAPPDATA%\PLib`:
|
||
|
||
- `library.db` — SQLite с метаданными;
|
||
- `thumbnails/` — кэш постеров (ключ = путь + размер + время изменения файла);
|
||
- `settings.json` — список папок, перечитывается на лету;
|
||
- `logs/` — Serilog, ротация по дням.
|
||
|
||
Схема создаётся через `EnsureCreated`. Когда форма таблицы устоится — заменить на
|
||
миграции EF Core (`DatabaseInitializer` — единственное место, которое надо будет тронуть).
|