2026-08-08 03:30:10 +00:00
2026-08-08 03:30:10 +00:00

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, а не пересборка. SourceCacheAutoRefreshFilterSortAndBind отдаёт диффы: добавился один файл — одна вставка в нужную позицию. Скролл, контейнеры 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 — единственное место, которое надо будет тронуть).

S
Description
No description provided
Readme MIT
648 KiB
Languages
C# 99.8%
Batchfile 0.2%