Files
avandClaude Opus 4.8 b8657120fe Приём: усыновление присутствующего в qBittorrent торрента вместо дубль-Add (409)
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>
2026-07-10 18:36:47 +03:00

115 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` как ссылку (`urls` API
`/torrents/add`); подсказку отображаемого имени брать из полей самой ссылки.
- Для `torrent` — загружать сохранённые байты `.torrent` (привязанные к
загрузке при приёме) и передавать их **файлом** (`torrents` API
`/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` не применяется