7.7 KiB
Жизненный цикл загрузки и машина состояний
Как загрузка проходит путь от приёма источника до разложенных файлов: состояния, переходы и то, что их вызывает. Кто владеет переходами и общее устройство — в architecture.md; детали распознавания — в recognition.md; действия человека в ревью — в review-ux.md.
Граф состояний
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 → «Транспорты». - downloading / completed —
workerполлит qBittorrent (worker.poll_interval, 5 с). Готовность — только когда файлы на месте (неmoving/checking*), см. «Завершение в qBittorrent» ниже. - recognizing —
recognizeстроит план и оценку уверенности (recognition.md). Невалидный/непарсящийся ответ LLM → review (не failed). - review — план уходит человеку (review-ux.md); цикл
review ⇄ recognizing— перераспознавание по подсказке. «Уточнить» — подсказка + перераспознавание; «Распознать заново» — повторный прогон без новой подсказки, по уже накопленному контексту и подсказкам. - deferred — «Позже» паркует задачу; принимает те же команды, что и
review, и возвращается в поверхность ревью по любому действию. - linking —
layoutсоздаёт хардлинки; идемпотентно, батчем. Коллизия цели возвращает в review, ошибка ФС → failed. См. architecture.md → «Раскладка файлов». - done — при входе неблокирующе дёргаем пересканирование Jellyfin
(опц., см. 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
→ «Пути и контейнеры».