# Жизненный цикл загрузки и машина состояний Как загрузка проходит путь от приёма источника до разложенных файлов: состояния, переходы и то, что их вызывает. Кто владеет переходами и общее устройство — в [architecture.md](architecture.md); детали распознавания — в [recognition.md](recognition.md); действия человека в ревью — в [review-ux.md](review-ux.md). ## Граф состояний ```mermaid stateDiagram-v2 [*] --> downloading: ingest (источник отдан в qBittorrent) downloading --> completed: файлы на месте downloading --> stuck: stalledDL дольше stuck_after downloading --> failed: metaDL дольше magnet_timeout / error completed --> recognizing recognizing --> linking: авто (матч в базе + валидация) recognizing --> review: нужно подтверждение / ответ LLM не разобран review --> linking: Применить review --> recognizing: Уточнить / Распознать заново review --> deferred: Позже review --> cancelled: Отклонить deferred --> review: любое действие (та же поверхность) linking --> done linking --> review: коллизия цели linking --> failed: ошибка ФС done --> reverted: Undo reverted --> recognizing: Привязать заново cancelled --> recognizing: Привязать заново stuck --> downloading: Retry failed --> downloading: Retry done --> [*] cancelled --> [*] reverted --> [*] note right of cancelled «Отклонить» доступно из любого нетерминального состояния end note ``` Условно-терминальные состояния — `done`, `cancelled`, `failed`, `reverted`: задача в них останавливается, но из `failed`/`stuck` есть **Retry**, а из `reverted`/`cancelled` — **Привязать заново**. `stuck` восстановимо ретраем. ## Состояния и переходы - **ingest → downloading** — приняли источник + контекст, отдали в qBittorrent (категория `qbittorrent.category`), записали в БД с ключом идемпотентности. См. [architecture.md](architecture.md) → «Транспорты». - **downloading / completed** — `worker` поллит qBittorrent (`worker.poll_interval`, 5 с). Готовность — только когда файлы на месте (не `moving`/`checking*`), см. «Завершение в qBittorrent» ниже. - **recognizing** — `recognize` строит план и оценку уверенности ([recognition.md](recognition.md)). Невалидный/непарсящийся ответ LLM → review (не failed). - **review** — план уходит человеку ([review-ux.md](review-ux.md)); цикл `review ⇄ recognizing` — перераспознавание по подсказке. «Уточнить» — подсказка + перераспознавание; «Распознать заново» — повторный прогон без новой подсказки, по уже накопленному контексту и подсказкам. - **deferred** — «Позже» паркует задачу; принимает те же команды, что и `review`, и возвращается в поверхность ревью по любому действию. - **linking** — `layout` создаёт хардлинки; идемпотентно, батчем. Коллизия цели возвращает в review, ошибка ФС → failed. См. [architecture.md](architecture.md) → «Раскладка файлов». - **done** — при входе неблокирующе дёргаем пересканирование Jellyfin (опц., см. [architecture.md](architecture.md) → «Пересканирование Jellyfin»); доступен **Undo** → `reverted` (убрать созданные ссылки). - **stuck / failed / cancelled** — не качается дольше таймаута; ошибка (ретраибельна); «Отклонить». - **reverted / cancelled → recognizing** — «Привязать заново»: после отката или отклонения можно перезапустить распознавание для той же раздачи. Перепривязка всегда идёт через review с ручным подтверждением (авто-раскладку не делаем) и требует, чтобы раздача всё ещё была в qBittorrent. Все переходы и команды идут через `worker` под per-download блокировкой — два транспорта не гонятся за одно состояние. Состояние персистентно в SQLite; `worker` периодически сверяет qBittorrent с БД и **усыновляет** раздачи с нашей категорией (`qbittorrent.category`) **или** тегом (`qbittorrent.tag`), которых ещё нет в БД, заводя для них задачу в состоянии `downloading`. Категория ставится на добавляемые нами раздачи (push, задаёт savepath); тег позволяет подхватить уже существующую раздачу, не трогая её категорию и файлы (pull). ## Завершение в qBittorrent `worker` опрашивает qBittorrent и сопоставляет его состояния с нашими: - **готово к раскладке:** `uploading`/`stalledUP`/`pausedUP`/`stoppedUP`/ `queuedUP`/`forcedUP` (имена `paused*`/`stopped*` различаются между qBit v4 и v5 — поддержаны оба). - **переходное, ждём:** `moving`/`checkingUP`/`checkingResumeData`/ `allocating` — остаёмся в `downloading`, пока qBit не закончит перенос/ проверку (готовность не объявляем, даже если флаги «UP»). - **ещё качается:** `downloading`/`stalledDL`/`metaDL`/`forcedMetaDL`/ `queuedDL`/`checkingDL`/`forcedDL`/`pausedDL`/`stoppedDL`. - **застряло/ошибка по таймауту:** `metaDL`/`forcedMetaDL` дольше `magnet_timeout` → `failed`; `stalledDL` дольше `stuck_after` → `stuck` (восстановимо ретраем). Возраст считаем от создания задачи. - **ошибка:** `error`/`missingFiles` → `failed`. Пути файлов берём из API (`save_path` + относительные имена из `/torrents/files`, уже включающие корневую папку торрента), не из константы (обычно это уже хост-путь). «Incomplete»-каталог в qBittorrent **включён** (`/srv/media/incomplete`): пока качается — файлы там, по завершении qBit переносит их в `/srv/media/downloads` (состояние `moving` — дожидаемся окончания переноса и только потом берём финальный путь). Подробнее о путях и песочнице — [architecture.md](architecture.md) → «Пути и контейнеры».