## 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` не применяется