Быстрый приём: сохранение в 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:
@@ -20,6 +20,7 @@ import (
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateCatched State = "catched" // поймано и сохранено; worker добавит в qBittorrent
|
||||
StateDownloading State = "downloading"
|
||||
StateCompleted State = "completed"
|
||||
StateRecognizing State = "recognizing" // Ф2
|
||||
@@ -503,6 +504,31 @@ func (s *Store) SetDownloadState(ctx context.Context, id string, state State, er
|
||||
return setState(ctx, s.DB, id, state, errCode, errMsg, false)
|
||||
}
|
||||
|
||||
// PromoteCatched переводит пойманную загрузку catched → downloading, попутно
|
||||
// записывая выведенное отображаемое имя. Гард `state = 'catched'` — это
|
||||
// ре-валидация: если загрузку успели отменить (catched → cancelled) во время
|
||||
// вывода имени/добавления вне блокировки переходов, UPDATE не заденет ни строки
|
||||
// и вернёт ошибку, а переход не применится. Пустое имя допустимо (rename не
|
||||
// задавали) — тогда display_name так и остаётся пустым.
|
||||
func (s *Store) PromoteCatched(ctx context.Context, id, displayName string) error {
|
||||
res, err := s.DB.ExecContext(ctx, `
|
||||
UPDATE download
|
||||
SET state = ?, display_name = ?, updated_at = ?
|
||||
WHERE id = ? AND state = ?`,
|
||||
string(StateDownloading), displayName, FormatTime(Now()), id, string(StateCatched))
|
||||
if err != nil {
|
||||
return fmt.Errorf("promote catched %s: %w", id, err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("promote catched %s: %w", id, err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("promote catched %s: not in catched (already added or cancelled)", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setState выполняет UPDATE состояния. reviveOK=true — вызов из гарда
|
||||
// (ActivateIfNoOtherActive), которому переход терминал→активное разрешён;
|
||||
// иначе предикат в UPDATE не даёт молча оживить терминальную задачу.
|
||||
|
||||
Reference in New Issue
Block a user