Пересканирование Jellyfin: расширить триггер на reverted и deleted

Скан Jellyfin (POST /Library/Refresh) слался только при входе в done.
После Undo (reverted) и Delete (deleted) наши хардлинки сняты, а Jellyfin
держал битые записи до скана по расписанию.

Гейт скана в едином чекпоинте transitionErr переведён с state == done на
предикат triggersScan(state) по множеству {done, reverted, deleted}: гейт по
состоянию-цели естественно ловит пользовательские Undo/Delete и
reconcile-производный deleted, идемпотентно. target_missing/orphaned —
промежуточный рассинхрон (ждём relink/лечения) — исключены.

OpenSpec: заведена и влита дельта file-layout (требование
«Пересканирование Jellyfin после изменения библиотечных ссылок»); change
архивирован. Синк рукописных доков architecture.md/workflow.md. Тесты:
скан стреляет на reverted и deleted, молчит на входе вне множества.
Закрыта задача беклога jellyfin-skan-posle-udaleniya.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-17 21:14:18 +03:00
co-authored by Claude Opus 4.8
parent 0354a8c96b
commit 1639ebfdd7
10 changed files with 303 additions and 72 deletions
+22 -4
View File
@@ -836,10 +836,14 @@ func (w *Worker) transitionErr(ctx context.Context, d store.Download, state stor
}
}
// Раскладка завершена — просим Jellyfin пересканировать библиотеку, чтобы
// новые файлы быстрее появились в проигрывателе. Тоже неблокирующе и вне
// w.mu; недоступность Jellyfin не влияет на состояние задачи.
if w.scanner != nil && state == store.StateDone {
// Наши библиотечные хардлинки изменились — просим Jellyfin пересканировать
// библиотеку, чтобы плеер не держал битые пути и быстрее подхватил новые
// файлы. Триггерят входы, где раскладка «улеглась»: done (ссылки разложены),
// reverted (Undo снял ссылки), deleted (Delete снял / сверка констатировала
// отсутствие). target_missing/orphaned — промежуточный рассинхрон, ждём
// relink/лечения, не сканируем. Неблокирующе и вне w.mu; недоступность
// Jellyfin не влияет на состояние задачи.
if w.scanner != nil && triggersScan(state) {
// Скан Jellyfin — неблокирующе и вне w.mu, в фоновом ctx со scoped-логгером
// (download_id для корреляции ext.*-записи клиента). Недоступность Jellyfin
// на задачу не влияет; ошибку вызова логирует сам клиент (ext.*), здесь гасим.
@@ -849,6 +853,20 @@ func (w *Worker) transitionErr(ctx context.Context, d store.Download, state stor
return nil
}
// triggersScan сообщает, стоит ли на входе в state дёргать пересканирование
// Jellyfin: наши библиотечные хардлинки только что изменились. Гейт по
// состоянию-цели в едином чекпоинте ловит и пользовательские Undo/Delete, и
// reconcile-производный deleted (инициатор роли не играет); target_missing/
// orphaned — промежуточный рассинхрон (ждём relink/лечения) — исключены.
func triggersScan(state store.State) bool {
switch state {
case store.StateDone, store.StateReverted, store.StateDeleted:
return true
default:
return false
}
}
// shouldNotifyFail дебаунсит повторные уведомления о падении одной задачи
// (мерцающий stalled-торрент: stuck↔downloading), чтобы не спамить. Вызывается
// под w.mu. НЕ сбрасываем запись при восстановлении — иначе дебаунс не гасил бы