Приём (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>
6.5 KiB
Why
Сейчас приём (Ingest) синхронно делает всё: парсит источник, выводит
отображаемое имя (потенциально медленный вызов LLM в namer.DeriveName) и
добавляет источник в qBittorrent — и только потом отвечает клиенту. Долгий LLM
и внешний запрос к qBit задерживают ответ HTTP API / веб-UI / Telegram и
расширяют окно «строка в БД есть, в qBittorrent ещё нет».
Идея: сделать приём быстрым — синхронно только валидировать и сохранить
загрузку (новое состояние catched), сразу вернув ответ; вывод имени и
добавление в qBittorrent вынести в отдельный асинхронный шаг машины
состояний, который двигает worker.
What Changes
- Новое состояние
catched— загрузка поймана и персистентно сохранена (быстрый путь). Нетерминальное, активное (участвует в инварианте «≤1 активная загрузка на infohash»). - Приём (
ingest) — быстрый: парс magnet, извлечение инфохэшей, синтез контекста из полей ссылки (дёшево, без сети), атомарный дедуп и запись загрузки вcatched. Ответ клиенту сразу. Синхронного вывода имени и добавления в qBittorrent в приёме больше нет. - Асинхронный шаг (
download-tracking, worker): на каждом тике worker подхватываетcatched-загрузки, выводит отображаемое имя из контекста (LLM + фолбек), добавляет источник в qBittorrent (категория/savepath/rename) и переводитcatched → downloading(отдельногоadded-to-qbittorrentнет —downloadingи так значит «в qBit, возможно metaDL»). - Обработка сбоев вне запроса клиента: неуспешный
addоставляет загрузку вcatched(worker перетыкивает на следующем тике); предохранительcatch_timeoutуводит долго-зависшийcatchedвfailed(error_codeqbit_add) с уведомлением автора. catchedисключён из проверок «раздача не найдена» (у него раздачи нет по дизайну) — ни поллинг, ни сверка не считают его рассинхроном/orphaned.- Веб-UI показывает промежуточное состояние
catched(бейдж/фаза жизненного цикла), заголовок деградирует, пока имя не выведено (фолбек уже есть).catchedпопадает в активную группу списка.
Capabilities
New Capabilities
Нет. Изменение переиспользует существующие capabilities.
Modified Capabilities
ingest: приём становится быстрым — сохранение вcatchedи мгновенный ответ; синхронный вывод имени и добавление в qBittorrent из приёма убраны (переезжают в асинхронный шаг). Требования по выводу имени переформулированы: выполняются на шаге добавления, а не в пути ответа клиента.download-tracking: добавляется шаг «добавление пойманной загрузки в qBittorrent» (вывод имени +add+ переходcatched → downloading), предохранительcatch_timeout, исключениеcatchedиз проверок пропажи раздачи.web-ui: человекочитаемый бейдж дляcatched; активная группа включаетcatched; заголовок при пустом имени — по фолбеку; карточкаcatchedсамообновляется htmx-поллингом до перехода вdownloading.
Без спек-правок: state-reconciliation и live-status не меняются.
Исключение catched из проверок рассинхрона нормативно закреплено требованием
download-tracking «catched не считается пропажей раздачи» (само поведение уже
верно — catched не входит в desyncStates и дебаунс пропажи источника его не
трогает). Иллюстративное перечисление активных состояний в тексте
state-reconciliation остаётся на кросс-ссылке и будет выверено при следующем
касании этой спеки. live-status: у catched нет qBit-телеметрии по дизайну —
живой прогресс для него корректно отсутствует.
Impact
- Код:
internal/store(состояниеStateCatched, группа active),internal/ ingest(быстрый путь: убрать namer/qbt-add, писатьcatched),internal/ worker(новый шаг обработкиcatched: namer + qbt-add + переход, таймаут),internal/httpapi+internal/tgbot(без правок API — ответ и так по Result), веб-UI шаблоны (бейдж/фазаcatched). - Конфиг: новый
catch_timeout(предохранитель), дефолт консервативный. - Данные: у активной загрузки теперь есть фаза без infohash-раздачи; схема БД
не меняется (используем существующие
state/error_code/error_msg). - Совместимость: существующие загрузки не затронуты; переход одноразовый на уровне логики приёма.