# download-tracking Specification ## Purpose Отслеживание скачивания и прямой путь машины состояний загрузки: поллинг qBittorrent и сопоставление его состояний (downloading → completed; готовность только когда файлы на месте), таймауты-предохранители (`magnet_timeout`/ `stuck_after`), ошибка qBit → failed, усыновление раздач по категории/тегу и переходы под per-download блокировкой. Сверка уже разложенного с реальностью — в `state-reconciliation`. ## Requirements ### Requirement: Поллинг qBittorrent и сопоставление состояний Worker SHALL периодически (`worker.poll_interval`, дефолт 5 с) опрашивать qBittorrent и сопоставлять его состояния раздачи с состоянием загрузки в БД. Готовые к раскладке состояния (`uploading`/`stalledUP`/`pausedUP`/`stoppedUP`/ `queuedUP`/`forcedUP`, с учётом различий имён между qBit v4 и v5) SHALL переводить загрузку в `completed`. Ещё качающиеся состояния (`downloading`/`stalledDL`/ `metaDL`/…) SHALL оставлять её в `downloading`. #### Scenario: Раздача завершилась - **GIVEN** загрузка в `downloading` - **WHEN** qBittorrent сообщает состояние `stalledUP` и файлы на месте - **THEN** загрузка переходит в `completed` ### Requirement: Готовность только когда файлы на месте Переходные состояния qBittorrent система SHALL трактовать как «ждём» (`moving`/`checkingUP`/`checkingResumeData`/`allocating`): оставаться в `downloading` и НЕ объявлять готовность, даже если выставлены флаги `UP`, пока qBit не завершит перенос/проверку. Финальные пути файлов система SHALL брать из API после завершения переноса. #### Scenario: Ждём завершения переноса - **GIVEN** загрузка, у которой qBittorrent в состоянии `moving` - **WHEN** идёт тик поллинга - **THEN** загрузка остаётся в `downloading`, готовность не объявляется ### Requirement: Таймауты-предохранители downloading Система SHALL переводить `metaDL`/`forcedMetaDL` дольше `magnet_timeout` (дефолт `24h`, редкий предохранитель) в `failed` (`error_code` `magnet_timeout`), а `stalledDL` дольше `stuck_after` — в `stuck` (`error_code` `stalled`). Возраст система SHALL считать от времени добавления в qBittorrent (`added_on`), а не от создания задачи, чтобы базис переживал retry и усыновление. Долгий `metaDL` система НЕ SHALL убивать агрессивно (медленные трекеры — норма). #### Scenario: Завис на метаданных дольше таймаута - **GIVEN** раздача в `metaDL` дольше `magnet_timeout` от `added_on` - **WHEN** идёт тик поллинга - **THEN** загрузка переходит в `failed` с `error_code` `magnet_timeout` ### Requirement: Ошибка qBittorrent переводит в failed Состояния `error`/`missingFiles` система SHALL трактовать как настоящий провал и переводить загрузку в `failed` (`error_code` `qbit_error`) — в отличие от таймаутов-предохранителей, такой провал сверкой не воскрешается. #### Scenario: qBit сообщает об ошибке - **GIVEN** раздача в состоянии `missingFiles` - **WHEN** идёт тик поллинга - **THEN** загрузка переходит в `failed` с `error_code` `qbit_error` ### Requirement: Усыновление раздач по категории или тегу Worker SHALL периодически сверять раздачи qBittorrent с БД и **усыновлять** те, у которых наша категория (`qbittorrent.category`) ИЛИ тег (`qbittorrent.tag`), а записи в БД ещё нет, заводя для них загрузку в состоянии `downloading`. Категория ставится на добавляемые нами раздачи (push); тег позволяет подхватить уже существующую раздачу (pull), не трогая её категорию и файлы. #### Scenario: Подхват существующей раздачи по тегу - **GIVEN** в qBittorrent есть раздача с тегом `qbittorrent.tag`, которой нет в БД - **WHEN** worker сверяет qBittorrent с БД - **THEN** для раздачи заводится загрузка в состоянии `downloading` ### Requirement: Переходы состояний сериализуются воркером Все переходы состояний загрузки SHALL сериализоваться worker'ом под единой блокировкой (поллинг-цикл и команды всех транспортов проходят через неё), чтобы два источника перехода не гонялись за одно состояние. Состояние SHALL быть персистентным в SQLite; активность загрузки SHALL выводиться только из `state`, без отдельного флага. #### Scenario: Команды сериализуются - **GIVEN** две одновременные команды к одной загрузке из разных транспортов - **WHEN** они обрабатываются - **THEN** переходы применяются последовательно под блокировкой, без гонки ### Requirement: Добавление пойманной загрузки в qBittorrent Worker SHALL периодически (в поллинг-цикле, под единой блокировкой переходов) подхватывать загрузки в состоянии `catched` и для каждой: вывести отображаемое имя из контекста (см. `ingest` «Отображаемое имя торрента из контекста»), добавить источник в qBittorrent (категория `qbittorrent.category`, savepath, `rename`) и перевести загрузку `catched → downloading`. Отдельного состояния между `catched` и `downloading` быть SHALL NOT — успешный `add` сразу переводит в `downloading` (которое и означает «в qBit, возможно `metaDL`»). Неуспешный `add` (qBittorrent недоступен и т.п.) SHALL оставлять загрузку в `catched` для повторной попытки на следующем тике; переход в терминальное состояние по единичному сбою происходить SHALL NOT (ретраи — естественными тиками поллинга). Медленные вызовы (вывод имени через LLM, `qbt.Add`) SHALL выполняться **вне** блокировки сериализации переходов, чтобы не задерживать команды транспортов и поллинг. Под блокировкой сериализуется только **запись перехода** `catched → downloading` (см. «Переходы состояний сериализуются воркером»), с ре-валидацией, что загрузка всё ещё в `catched` (иначе переход отклоняется — например, при параллельной отмене). #### Scenario: Пойманная загрузка добавляется в qBittorrent - **GIVEN** загрузка в состоянии `catched` - **WHEN** worker обрабатывает тик - **THEN** выводится отображаемое имя, источник добавляется в qBittorrent с нашей категорией и `rename` - **AND** загрузка переходит в `downloading` #### Scenario: Временный сбой добавления — повтор - **GIVEN** загрузка в `catched`, qBittorrent временно недоступен - **WHEN** worker пытается добавить источник и `add` не удался - **THEN** загрузка остаётся в `catched` - **AND** на следующем тике попытка добавления повторяется #### Scenario: Отмена во время добавления - **GIVEN** загрузка в `catched`, worker выводит имя и добавляет её вне блокировки - **WHEN** параллельно приходит команда отмены (`catched → cancelled`), а затем worker берёт блокировку для записи перехода - **THEN** ре-валидация видит, что загрузка уже не в `catched`, и переход в `downloading` не применяется ### Requirement: Предохранитель зависшего catched Система SHALL переводить загрузку, задержавшуюся в `catched` дольше `catch_timeout` (конфигурируемый предохранитель, дефолт консервативный), в `failed` (`error_code` `qbit_add`) и уведомлять автора. Возраст SHALL считать от времени попадания в `catched` (создания загрузки). Предохранитель — редкий страховочный механизм на случай устойчивой недоступности qBittorrent, а не штатный путь. #### Scenario: catched висит дольше таймаута - **GIVEN** загрузка в `catched` дольше `catch_timeout` - **WHEN** идёт тик поллинга - **THEN** загрузка переходит в `failed` с `error_code` `qbit_add` - **AND** автор загрузки уведомляется ### Requirement: catched не считается пропажей раздачи Система SHALL исключать состояние `catched` из проверок «раздача не найдена в qBittorrent» — как в поллинге активных загрузок, так и в сверке рассинхрона (`state-reconciliation`). У пойманной загрузки раздачи в qBittorrent ещё нет по дизайну, поэтому её отсутствие система SHALL NOT трактовать как рассинхрон, `orphaned` или пропажу источника. #### Scenario: Отсутствие раздачи у catched — не рассинхрон - **GIVEN** загрузка в `catched` (раздачи в qBittorrent ещё нет) - **WHEN** идёт тик поллинга и сверки - **THEN** загрузка не считается пропавшей/рассинхронизированной и остаётся в `catched` (до добавления воркером или срабатывания `catch_timeout`)