Files
PLib/README.md
T

94 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLib
Менеджер видеотеки на Avalonia: сканирует папки, вытаскивает превью через ffmpeg и
показывает всё сеткой карточек.
## Что уже работает
- Сканирование указанных папок, инкрементальное — файл, который не изменился, не переиндексируется.
- Метаданные (длительность, разрешение, кодек) через ffprobe.
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
- Настройки — боковой панелью в том же окне (сетка сдвигается, а не перекрывается): папки
библиотеки с удалением, параметры превью и сканирования, тема, очистка кэша. Всё пишется
в `settings.json` и подхватывается без перезапуска.
- Светлая, тёмная и системная темы; выбор запоминается.
- Клик или 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 на каждый вызов.
- **Одно окно.** Настройки — колонка макета, а не второе окно и не оверлей: открываясь, она
сдвигает сетку, и та переливается в меньшее число столбцов, оставаясь целиком доступной.
В 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` — единственное место, которое надо будет тронуть).