processCatched перед Add проверяет присутствие торрента в qBittorrent (один листинг на тик): если раздача уже есть — усыновляем (promote catched→downloading без повторного Add и без LLM-namer, имя из раздачи), иначе добавляем как раньше. Это убирает бесконечный цикл дубль-Add → 409 → ретрай и лишние вызовы LLM. Инвариант приёма «одна активная на infohash» делает различие «наш/чужой» ненужным. source_type перечитывается под замком (сужение гонки апгрейда F6); при недоступности qBittorrent тик пропускается без вызова LLM. Дедуп на приёме (дубль на уже активную задачу) теперь отражается явным ответом бота «дубль уже активной #id — добавление отменено». Спека download-tracking обновлена (OpenSpec change заархивирован); закрыта задача беклога review-f2-promote-without-add. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.3 KiB
MODIFIED Requirements
Requirement: Добавление пойманной загрузки в qBittorrent
Worker SHALL периодически (в поллинг-цикле, под единой блокировкой переходов)
подхватывать загрузки в состоянии catched и для каждой (кроме случая уже
присутствующего в qBittorrent торрента, см. ниже): вывести отображаемое имя из
контекста (см. ingest «Отображаемое имя торрента из контекста»), добавить
источник в qBittorrent (категория qbittorrent.category, savepath, rename) и
перевести загрузку catched → downloading. Отдельного состояния между catched
и downloading быть SHALL NOT — успешный add сразу переводит в downloading
(которое и означает «в qBit, возможно metaDL»).
Перед добавлением worker SHALL проверять, присутствует ли торрент загрузки уже
в qBittorrent (по любому из её infohash), опираясь на листинг раздач того же
тика. Если торрент уже присутствует, worker SHALL усыновить его: перевести
загрузку catched → downloading без повторного add и без вывода имени
через LLM (display_name берётся из имени присутствующей раздачи). Повторный
add здесь не нужен и вреден — qBittorrent отверг бы дубль (напр. 409 Conflict), и загрузка зациклилась бы на ретраях. Усыновлённая раздача дальше
идёт обычным путём отслеживания и раскладки. Проверка присутствия SHALL
выполняться до вывода отображаемого имени, чтобы не тратить LLM-вызов на
загрузку, которую добавлять не требуется.
Инвариант приёма («одна активная загрузка на infohash», см. ingest) гарантирует,
что до этого шага доходит лишь загрузка, для которой в jellybit НЕТ другой
активной задачи; поэтому присутствие торрента в qBittorrent worker трактует как
«усыновить и разложить», а не как конфликт с чужой задачей.
Если листинг раздач qBittorrent недоступен (сетевой сбой), worker пойманную
загрузку в этот тик трогать SHALL NOT (ни add, ни namer) и повторить на
следующем; устойчивая недоступность отсекается предохранителем catch_timeout
(см. «Предохранитель зависшего catched»).
Добавление в qBittorrent worker SHALL выполнять по типу источника
(source_type):
- Для
magnet/url— передаватьsource_refкак ссылку (urlsAPI/torrents/add); подсказку отображаемого имени брать из полей самой ссылки. - Для
torrent— загружать сохранённые байты.torrent(привязанные к загрузке при приёме) и передавать их файлом (torrentsAPI/torrents/add), НЕ как ссылку; подсказку отображаемого имени брать из метаданных торрента (имя раздачи). Добавление байтами SHALL сохранять полные метаданные (qBittorrent стартует без докачки), поэтому воскрешать раздачу по magnet-хешу вместо файла система SHALL NOT.
source_type для выбора способа добавления worker SHALL перечитывать под
блокировкой переходов непосредственно перед добавлением (а не полагаться на
снимок, снятый ранее вне блокировки): иначе при точном оверлапе тика с апгрейдом
пойманной magnet-задачи до .torrent (см. ingest) воркер добавил бы magnet из
устаревшего снимка, хотя БД уже torrent.
Неуспешный add (qBittorrent временно отверг/недоступен) SHALL оставлять
загрузку в catched для повторной попытки на следующем тике; переход в
терминальное состояние по единичному сбою происходить SHALL NOT (ретраи —
естественными тиками поллинга, отсечка — catch_timeout).
Медленные вызовы (вывод имени через LLM, qbt.Add) SHALL выполняться вне
блокировки сериализации переходов, чтобы не задерживать команды транспортов и
поллинг. Под блокировкой сериализуется только запись перехода catched → downloading (см. «Переходы состояний сериализуются воркером»), с ре-валидацией,
что загрузка всё ещё в catched (иначе переход отклоняется — например, при
параллельной отмене).
Scenario: Пойманная magnet-загрузка добавляется в qBittorrent
- GIVEN загрузка в состоянии
catchedсsource_type = magnet, торрента ещё нет в qBittorrent - WHEN worker обрабатывает тик
- THEN выводится отображаемое имя, ссылка добавляется в qBittorrent с
нашей категорией и
rename - AND загрузка переходит в
downloading
Scenario: Пойманная .torrent-загрузка добавляется файлом
- GIVEN загрузка в состоянии
catchedсsource_type = torrentи сохранёнными байтами файла, торрента ещё нет в qBittorrent - WHEN worker обрабатывает тик
- THEN сохранённые байты добавляются в qBittorrent файлом (
torrents), с нашей категорией иrename, без обращения к magnet-хешу - AND загрузка переходит в
downloading
Scenario: Торрент уже присутствует в qBittorrent — усыновление без add
- GIVEN загрузка в состоянии
catched, торрент которой уже присутствует в qBittorrent (добавлен ранее вручную/другим клиентом либоaddпрошёл на прошлом тике, а запись перехода не удалась) - WHEN worker обрабатывает тик
- THEN worker НЕ вызывает
qbt.Addи НЕ выводит отображаемое имя через LLM - AND
display_nameзаписывается из имени присутствующей раздачи - AND загрузка переходит в
downloadingи идёт обычным путём к раскладке
Scenario: qBittorrent недоступен при проверке присутствия — повтор
- GIVEN загрузка в
catched, листинг раздач qBittorrent не удался - WHEN worker обрабатывает тик
- THEN worker НЕ вызывает namer и НЕ добавляет источник
- AND загрузка остаётся в
catchedи попытка повторяется на следующем тике
Scenario: Временный сбой добавления — повтор
- GIVEN загрузка в
catched, торрента в qBittorrent нет, ноaddне удался - WHEN worker пытается добавить источник и
addвозвращает ошибку - THEN загрузка остаётся в
catched - AND на следующем тике попытка добавления повторяется
Scenario: Отмена во время добавления
- GIVEN загрузка в
catched, worker выводит имя и добавляет её вне блокировки - WHEN параллельно приходит команда отмены (
catched → cancelled), а затем worker берёт блокировку для записи перехода - THEN ре-валидация видит, что загрузка уже не в
catched, и переход вdownloadingне применяется