# 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`: карточки появляются по мере находок, а не после завершения всего прохода. Тяжёлая часть (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` — единственное место, которое надо будет тронуть).