Быстрый приём: сохранение в catched, добавление в qBittorrent — шаг worker'а

Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из
полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` —
ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent
вынесены в асинхронный шаг машины состояний, который двигает worker.

- store: состояние `catched` (нетерминальное, активная группа); атомарный
  переход PromoteCatched (catched → downloading + display_name) с гардом
  state='catched' (ре-валидация после сетевых вызовов вне блокировки)
- ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу
- worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий
  переход; сбой add оставляет catched (ретрай тиком); предохранитель
  catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи
- config: worker.catch_timeout (дефолт 10m)
- веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг
  карточки/страницы до перехода в downloading; Telegram-текст без сырого catched
- OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change
  заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-07 21:29:28 +03:00
co-authored by Claude Opus 4.8
parent f0ce6b4bc8
commit 0d263270cb
30 changed files with 1198 additions and 348 deletions
+8 -9
View File
@@ -80,13 +80,13 @@ func runServe(args []string) error {
}
// Вывод отображаемого имени торрента из контекста (best-effort). Без LLM
// работает только алгоритмический фолбек.
// работает только алгоритмический фолбек. Namer зовёт worker на шаге
// добавления пойманной загрузки (не синхронный приём).
namer := naming.New(llmProvider, cfg.LLM.MaxRetries, logger)
ingestor := ingest.New(st, qb, namer, ingest.Config{
Category: cfg.QBittorrent.Category,
SavePath: cfg.QBittorrent.SavePath,
}, logger)
// Быстрый приём: сохраняет загрузку в catched и сразу отвечает; добавление в
// qBittorrent и вывод имени делает worker (см. download-tracking).
ingestor := ingest.New(st, logger)
// Ф4: базы метаданных (опц.). Без них авто-раскладки нет — всё в review.
providers, err := metadataProviders(cfg, logger)
@@ -126,8 +126,11 @@ func runServe(args []string) error {
PollInterval: cfg.Worker.PollInterval.Std(),
StuckAfter: cfg.Worker.StuckAfter.Std(),
MagnetTimeout: cfg.Worker.MagnetTimeout.Std(),
CatchTimeout: cfg.Worker.CatchTimeout.Std(),
SourceMissingThreshold: cfg.Worker.SourceMissingThreshold,
}, logger)
// Вывод имени на шаге добавления пойманной загрузки (best-effort).
wrk.SetNamer(namer)
// Пересканирование Jellyfin после раскладки (опц.). Недоступность Jellyfin
// не валит сервис — скан просто не сработает (залогируется в воркере).
@@ -188,10 +191,6 @@ func runServe(args []string) error {
WebBaseURL: cfg.Telegram.WebBaseURL,
}, logger)
wrk.SetNotifier(bot)
// Приёмные падения (qbit_add) минуют worker — уведомляем напрямую.
ingestor.SetFailureNotifier(func(id string) {
bot.Notify(context.Background(), id, worker.EventFailed)
})
go bot.Run(ctx)
logger.Info("telegram bot enabled",
"bot", api.Self.UserName, "allowed_users", len(cfg.Telegram.AllowedUserIDs))