Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.
- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
привязывается к существующей записи (спящей, без обращения к qBittorrent),
а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
дедупе ведёт на страницу существующей записи.
Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
42 KiB
ingest Specification
Purpose
Приём загрузки — единый use-case для всех транспортов (HTTP, Telegram, CLI):
парс источника (Ф1 — magnet), извлечение инфохэшей (download_infohash),
дедупликация по активной задаче, атомарное заведение download и отдача
источника в qBittorrent, а также вывод человекочитаемого отображаемого имени из
контекста (через LLM или алгоритмический фолбек). Держит инвариант «не более
одной активной загрузки на infohash» (атомарный возврат в активное состояние).
Requirements
Requirement: Отображаемое имя торрента из контекста
На шаге добавления пойманной загрузки в qBittorrent (worker) система SHALL
выводить из контекста загрузки человекочитаемое отображаемое имя и передавать
его в qBittorrent (параметр rename API /torrents/add), чтобы задача в списке
qBit не показывалась безликим dn magnet-ссылки. Это же имя система SHALL
сохранять у загрузки (download.display_name) для последующего показа
заголовком в веб-UI.
Имя SHALL быть коротким читаемым ярлыком (название, опционально режиссёр и год; для сериала — номер сезона, если он определён), а не куском сырого контекста. Имя SHALL очищаться от управляющих символов и переводов строк и SHALL обрезаться по ограничению длины.
Вывод имени SHALL выполняться на шаге добавления, непосредственно перед вызовом
add (параметр rename действует только в момент добавления), а НЕ в
синхронном пути ответа приёма. В состоянии catched (до добавления)
download.display_name ещё пуст — веб-UI берёт заголовок из фолбека.
Отображаемое имя SHALL влиять только на отображение (в qBittorrent и как заголовок в веб-UI) и SHALL NOT влиять на пути файлов на диске, распознавание или раскладку — реальные пути система по-прежнему читает из qBit API.
Scenario: Имя из контекста передаётся в qBittorrent
- WHEN на шаге добавления получен непустой контекст, из которого удалось вывести имя
- THEN система передаёт это имя в qBittorrent в параметре
rename - AND имя — короткий читаемый ярлык вида «название (режиссёр, год)», где режиссёр и год опциональны
Scenario: Имя сохраняется у загрузки
- WHEN на шаге добавления выведено непустое отображаемое имя
- THEN система сохраняет его в
download.display_name(обновлением записи загрузки) - AND веб-UI использует его заголовком карточки и страницы загрузки
Scenario: Контекст пуст или имя не получено
- WHEN контекста нет либо ни один способ вывода не дал непустого имени
- THEN система добавляет загрузку без параметра
rename - AND qBittorrent оставляет собственное имя (из
dn/торрента) - AND
download.display_nameостаётся пустым, а веб-UI берёт заголовок из фолбека (распознанное название или усечённый источник)
Requirement: Вывод имени через LLM со структурированным выводом
Система SHALL строить отображаемое имя с помощью LLM (структурированный JSON-вывод), извлекая из контекста тип (movie/series), название, год, режиссёра и (для сериала) номер сезона. Год и режиссёр — опциональные поля.
Если и контекст, и подсказка из полей источника (dn magnet / имя .torrent)
пусты, система SHALL считать имя не выведенным и SHALL NOT вызывать LLM (выводить
имя не из чего — вызов на пустом входе способен лишь галлюцинировать). В этом
случае загрузка добавляется без rename, а download.display_name остаётся
пустым.
Название SHALL быть на русском языке для российского контента и на английском (оригинальном) — для остального.
Система SHALL предпринять ограниченное число попыток получить от LLM валидный
результат (корректный JSON с непустым названием); бюджет попыток —
[llm].max_retries (по умолчанию 3). Транспортные ретраи провайдера LLM
(сетевые сбои, 429, 5xx) в этот счёт не входят.
Недоступность или ошибка LLM SHALL NOT прерывать приём загрузки: система переходит к алгоритмическому фолбеку.
Scenario: LLM возвращает структурированное имя
- WHEN LLM по контексту возвращает валидный JSON с непустым названием
- THEN система формирует отображаемое имя из его полей (название, год, для сериала — сезон)
Scenario: Российский контент — название на русском
- WHEN контент распознан как российский
- THEN в отображаемом имени используется русское название
Scenario: Исчерпан бюджет попыток LLM
- WHEN LLM за отведённые попытки (
[llm].max_retries) не вернул валидный результат либо недоступен - THEN система не прерывает приём и переходит к алгоритмическому фолбеку
Scenario: Пустой вход — LLM не вызывается
- GIVEN пойманная загрузка без пользовательского контекста и без подсказки
из полей источника (голый magnet без
dn) - WHEN система выводит отображаемое имя на шаге добавления
- THEN LLM не вызывается, имя считается не выведенным
- AND загрузка добавляется без
rename, аdownload.display_nameпуст (заголовок в веб-UI берётся из фолбека)
Requirement: Алгоритмический фолбек вывода имени без сети
При неудаче LLM система SHALL выводить имя алгоритмически, без сетевых запросов: брать первую содержательную строку контекста (без ссылок, команд бота и UI-мусора), отсекать технические характеристики, очищать и обрезать по длине.
Если и фолбек не дал непустого имени, система SHALL добавить загрузку без
параметра rename.
Scenario: Фолбек извлекает имя из контекста
- WHEN LLM недоступен или исчерпал попытки, а контекст содержательный
- THEN система берёт первую содержательную строку контекста, отсекает технические характеристики и использует результат как отображаемое имя
- AND при этом не делается ни одного сетевого запроса
Scenario: Фолбек тоже пуст
- WHEN ни LLM, ни алгоритмический фолбек не дали непустого имени
- THEN система добавляет загрузку без параметра
rename
Requirement: Обновление отображаемого имени по распознаванию
Система SHALL уметь обновлять отображаемое имя загрузки после того, как
распознавание дало каноническое название, — переливая уже вычисленное имя (без
нового вызова LLM) в download.display_name и в имя раздачи qBittorrent.
Источником имени SHALL быть эффективное распознанное название: пины
title/year (если матч подтверждён), иначе recognition.title/year — то
же название, что использует раскладка. Формат SHALL быть коротким детерминированным
ярлыком Title (Year) (год опционален), с той же очисткой от управляющих
символов и обрезкой по длине, что и вывод имени на шаге добавления. Пустой
источник (нет распознавания или пустое название) SHALL приводить к отсутствию
изменений (no-op).
Переименование раздачи в qBittorrent SHALL адресоваться по infohash своей
раздачи и SHALL быть best-effort: сбой (раздача удалена, qBittorrent недоступен)
SHALL NOT проваливать обновление — download.display_name обновляется в любом
случае, ошибка внешнего вызова логируется. Как и на шаге добавления,
отображаемое имя SHALL влиять только на отображение и SHALL NOT влиять на пути
файлов, распознавание или раскладку.
Обновление имени SHALL иметь две точки входа: авто — при подтверждённом
матче (см. capability review); ручную — по явному действию пользователя.
Ручное действие SHALL перезаписывать текущее имя всегда; авто SHALL перезаписывать,
когда выведенное имя непусто.
Scenario: Перелив имени в загрузку и раздачу
- GIVEN загрузка с распознанным непустым каноническим названием
- WHEN запускается обновление отображаемого имени
- THEN
download.display_nameустанавливается вTitle (Year) - AND раздача в qBittorrent переименовывается в то же имя (по infohash своей раздачи)
Scenario: qBittorrent недоступен — имя у загрузки всё равно обновлено
- GIVEN обновление отображаемого имени с выведенным непустым именем
- WHEN переименование раздачи в qBittorrent завершается ошибкой (недоступен или раздача удалена)
- THEN
download.display_nameвсё равно обновлён - AND ошибка внешнего вызова qBittorrent логируется, операция не проваливается
Scenario: Нет распознавания — обновление ничего не делает
- GIVEN загрузка без распознанного названия (пустой источник имени)
- WHEN запускается обновление отображаемого имени
- THEN ни
download.display_name, ни имя раздачи не меняются (no-op)
Requirement: Приём источника и заведение загрузки
Приём SHALL быть единым быстрым use-case, общим для всех транспортов (HTTP,
Telegram, CLI): по источнику (Ф1 — magnet) и текстовому контексту система SHALL
синхронно извлечь инфохэши, синтезировать контекст из полей ссылки (без сети),
дедуплицировать по блокирующей повторный приём задаче (активной либо
удерживающей источник ради незакрытого намерения — target_missing/orphaned;
см. «Дедупликация приёма по любому из хешей») и при отсутствии дубля завести
загрузку (download в состоянии catched + записи download_infohash),
после чего сразу вернуть ответ транспорту. Заведение загрузки и запись её
хешей SHALL выполняться атомарно (см. «Атомарность возврата загрузки в активное
состояние»).
Синхронный путь приёма SHALL NOT обращаться к qBittorrent и SHALL NOT выводить
отображаемое имя (потенциально медленный LLM): и добавление источника в
qBittorrent, и вывод имени выполняются отдельным асинхронным шагом машины
состояний (worker) — см. download-tracking «Добавление пойманной загрузки в
qBittorrent».
catched — нетерминальное активное состояние: оно участвует в инварианте «не
более одной активной загрузки на infohash» наравне с прочими активными.
Scenario: Быстрый приём magnet
- GIVEN валидная magnet-ссылка и контекст
- WHEN вызывается приём
- THEN создаётся
downloadв состоянииcatchedс записямиdownload_infohash - AND ответ транспорту отдан без обращения к qBittorrent и без вывода имени
Scenario: Дубль по активной задаче на быстром пути
- GIVEN уже есть активная (в т.ч.
catched) загрузка с тем же infohash - WHEN вызывается приём
- THEN новая загрузка не создаётся, возвращается существующая
Requirement: Множество инфохэшей загрузки
Загрузка SHALL иметь одну или более записей инфохэша (download_infohash:
infohash lowercase hex, kind ∈ v1|v2). При приёме magnet-ссылки
SHALL записываться ВСЕ известные из неё хеши — гибридный magnet несёт и
btih (v1), и btmh (v2); kind определяется по длине hex (40 — v1, 64 —
v2). Когда qBittorrent сообщает для раздачи оба хеша (infohash_v1,
infohash_v2), система SHALL дописывать недостающие записи загрузке;
усечённый хеш v2-only раздачи (поле hash qBittorrent, 40 hex от v2)
записываться SHALL NOT. Сопоставление раздачи qBittorrent с загрузкой
(поллинг, discover) SHALL выполняться по любому из известных хешей. Один и
тот же infohash MAY принадлежать нескольким загрузкам во времени (повторный
приём после терминального состояния), но активной из них MUST быть не более
одной.
Scenario: Гибридный торрент раскрывает оба хеша
- GIVEN загрузка принята по magnet с v1-хешем
- WHEN qBittorrent отдаёт раздачу с заполненными
infohash_v1иinfohash_v2 - THEN у загрузки появляются обе записи (
kind=v1иv2)
Scenario: Сопоставление по v2-хешу
- GIVEN загрузка с записями v1- и v2-хешей
- WHEN поллинг находит раздачу, совпавшую только по v2-хешу
- THEN раздача сопоставляется с этой загрузкой
Requirement: Дедупликация приёма по любому из хешей
При приёме система SHALL искать загрузку, блокирующую повторный приём, по
любому из известных хешей и, найдя, SHALL возвращать её вместо создания новой.
Блокирующими SHALL считаться загрузки в активном (нетерминальном) состоянии
либо удерживающие источник ради незакрытого намерения — target_missing
(источник жив в qBittorrent, ждёт relink) и orphaned (источник пропал, запись
держит претензию на последнюю копию). Прочие терминальные состояния (done,
cancelled, failed, reverted, deleted) блокирующими быть SHALL NOT:
повторный приём такого инфохэша — осознанное «хочу заново» и SHALL заводить
новую загрузку.
Когда найденная блокирующая загрузка терминальна (target_missing/orphaned),
приём SHALL возвращать её как существующую (Deduplicated) спящей: система
SHALL NOT переводить её в активное состояние и SHALL NOT обращаться к qBittorrent
(перепривязка — отдельное явное действие пользователя, а не побочный эффект
приёма); ответ транспорту SHALL сообщать, что запись существует и требует
перепривязки либо закрытия.
Атомарный инвариант касается активной составляющей: проверка отсутствия
другой активной загрузки на любом из хешей и вставка новой загрузки с её хешами
SHALL выполняться в одной write-транзакции, поддерживая «не более одной активной
загрузки на infohash» (тот же общий active-гард, что у прочих путей активации).
Расширение критерия на desync-состояния (target_missing/orphaned) SHALL быть
устойчивым пред-ридом до создания, коротко замыкающим приём на возврат
существующей записи; desync-состояния в общий active-гард заводиться SHALL NOT
(их терминальность оставляет state-инвариант «активности» нетронутым).
Отдельного снимаемого/восстанавливаемого ключа идемпотентности в схеме быть SHALL
NOT — активность выводится только из state.
Scenario: Повторный приём при активной загрузке
- GIVEN активная загрузка с infohash
h - WHEN принимается magnet с тем же
h - THEN новая загрузка не создаётся, возвращается существующая
Scenario: Повторный приём при записи без цели
- GIVEN загрузка с infohash
hвtarget_missing(источник жив, цель удалена) - WHEN принимается magnet с тем же
h - THEN новая загрузка не создаётся, возвращается существующая запись как
Deduplicated - AND её состояние остаётся
target_missing(в активное не переводится, к qBittorrent обращения нет) - AND ответ транспорту указывает, что запись существует и её нужно привязать заново или закрыть
Scenario: Повторный приём при осиротевшей записи
- GIVEN загрузка с infohash
hвorphaned(источник пропал) - WHEN принимается magnet/torrent с тем же
h - THEN новая загрузка не создаётся, возвращается существующая запись как
Deduplicated
Scenario: Повторный приём после завершения
- GIVEN загрузка с infohash
hв терминальном состоянииdone - WHEN принимается magnet с тем же
h - THEN создаётся новая загрузка со своим ULID и записью
h
Scenario: Повторный приём после закрытия записи
- GIVEN загрузка с infohash
hвcancelled(в т.ч. закрытая изtarget_missing) - WHEN принимается magnet с тем же
h - THEN создаётся новая загрузка со своим ULID и записью
h
Scenario: Повторный приём при прочих терминальных состояниях
- GIVEN загрузка с infohash
hвfailedилиreverted(не удерживает источник ради незакрытого намерения) - WHEN принимается magnet/torrent с тем же
h - THEN создаётся новая загрузка со своим ULID и записью
h(повторный приём — свежая попытка; старая терминальная запись хешем не владеет)
Requirement: Атомарность возврата загрузки в активное состояние
Система SHALL атомарно (в одной write-транзакции) проверять на каждом пути, возвращающем загрузку из терминального состояния в активное (ручной retry, воскрешение фоновой сверкой, повторная раскладка/relink) или создающем её (приём, adopt чужой раздачи), что никакая другая активная загрузка не владеет любым из хешей этой, и при владении SHALL отказывать в переходе, сохраняя инвариант «не более одной активной загрузки на infohash». Отказ SHALL происходить до побочных эффектов во внешних системах (повторного добавления торрента в qBittorrent).
Та же проверка SHALL применяться к дозаписи хешей загрузке (раскрытие гибридного торрента) на ВСЕХ путях дозаписи, включая дедуп-дозапись при приёме: хеш, которым владеет другая активная загрузка, дописан быть SHALL NOT — ни отдельным методом дозаписи, ни дедуп-веткой атомарного заведения, которая доносит недостающие хеши найденной активной задаче. Прямой перевод терминальной загрузки в активное состояние в обход этой проверки SHALL отклоняться хранилищем (механический бэкстоп вместо удалённого unique-индекса).
Scenario: Retry при занятом хеше
- GIVEN загрузка #1 в
failedс хешемh, и другая активная загрузка #2 с тем жеh - WHEN пользователь вызывает retry для #1
- THEN переход отклоняется с пояснением, #1 остаётся в
failed - AND активной по
hостаётся #2
Scenario: Дедуп-дозапись не крадёт чужой хеш
- GIVEN активная загрузка A владеет хешем
v1, активная загрузка B владеет хешемv2того же гибридного торрента - WHEN принимается источник с обоими хешами
{v1, v2}и дедупится на B - THEN B получает только незанятые хеши, а
v1(в собственности A) B не дописывается - AND инвариант «не более одной активной загрузки на infohash»
сохраняется (по
v1активна только A)
Requirement: Синтез контекста распознавания из полей magnet
При приёме система SHALL извлекать из полей magnet-ссылки дополнительный контекст и дополнять им контекст, пришедший из транспорта: факты из полей SHALL добавляться к пользовательскому тексту (пользовательский текст — первым), а при пустом тексте SHALL становиться единственным контекстом. Синтез SHALL выполняться только из самой ссылки, без сетевых запросов.
В синтез SHALL включаться следующие поля, когда они присутствуют:
dn(display name) — как строка названия релиза, если это содержательное имя, а не заглушка-идентификатор вида*-topic-<id>(напримерrutracker-topic-6514485); такие заглушки в контекст-название включаться SHALL NOT.xl(exact length) — как человекочитаемый размер (например «≈ 2.1 GiB»); нечисловое/некорректное значение игнорируется.tr/xs(трекеры / exact source) — как сигнал происхождения по домену (хост трекера/источника), помогающий определить язык и тип контента.kt(keyword topic) — как ключевые слова.
Обогащённый контекст система SHALL сохранять в download.Context — его читают
recognition (LLM-промпт) и веб-UI (страница загрузки). Синтез и слияние SHALL
выполняться до создания загрузки.
Синтезированные строки-факты (размер, домен трекера, ключевые слова) SHALL NOT
влиять на вывод отображаемого имени (download.display_name / параметр
rename qBittorrent): вход вывода имени остаётся прежним (пользовательский
текст и подсказка dn), см. требование «Отображаемое имя торрента из
контекста».
Приём только по magnet (пустой текст контекста) SHALL быть штатным сценарием во всех транспортах (HTTP, Telegram, CLI).
Scenario: dn — содержательное имя релиза
- WHEN magnet содержит
dnс релиз-именем (напримерDune.Part.Two.2024.2160p.BluRay) - THEN это имя добавляется в
download.Context - AND становится доступно recognition
Scenario: dn — заглушка-идентификатор темы
- WHEN
dnимеет вид*-topic-<id>(напримерrutracker-topic-6514485) - THEN система не включает его как строку-название в контекст
- AND остальные поля magnet (размер, домен трекера) всё равно синтезируются
Scenario: Размер из xl
- WHEN magnet содержит корректный числовой
xl - THEN в
download.Contextдобавляется человекочитаемый размер загрузки
Scenario: Происхождение по домену трекера
- WHEN magnet содержит
trи/илиxsс распознаваемым хостом - THEN в
download.Contextдобавляется сигнал происхождения (домен), пригодный как подсказка языка/типа контента
Scenario: Дополнение непустого пользовательского контекста
- WHEN транспорт передал непустой текст контекста, а magnet несёт поля
- THEN
download.Contextсодержит и текст пользователя, и факты из полей magnet (текст пользователя не теряется) - AND текст пользователя идёт первым
Scenario: Приём только по magnet
- WHEN magnet принят с пустым текстом контекста
- THEN приём проходит штатно
- AND
download.Contextсинтезируется из полей magnet и сохраняется
Scenario: Строки-факты не становятся отображаемым именем
- GIVEN голый magnet с заглушкой
dn=rutracker-topic-<id>и трекером, без пользовательского текста - WHEN выполняется приём
- THEN
download.display_nameне выводится из строк-фактов (не равенТрекер: …/Размер: …) - AND отображаемое имя определяется прежним путём (подсказка
dnили его отсутствие → безrename)
Scenario: Синтез без сети
- WHEN выполняется извлечение контекста из полей magnet
- THEN не делается ни одного сетевого запроса (только разбор строки ссылки)
Scenario: Полей для контекста нет
- WHEN текст пользователя пуст и ни одно пригодное поле magnet не даёт
содержательного контекста (нет
dn-имени,xl, распознаваемого домена,kt) - THEN
download.Contextостаётся пустым - AND приём проходит штатно (пустой контекст допустим)
Requirement: Приём источника из .torrent-файла
Приём SHALL принимать источник в виде байтов .torrent-файла (наряду с
magnet-ссылкой) — тем же быстрым use-case, общим для транспортов. Получив
непустые байты торрента, система SHALL разобрать их локально (без сети),
извлечь инфохэш(и) и завести загрузку с source_type = torrent, после чего
сразу вернуть ответ транспорту (синхронный путь к qBittorrent не обращается —
добавление делает воркер, см. download-tracking).
Инфохэши система SHALL извлекать такими, какими их сообщает qBittorrent, чтобы
сопоставление раздач и дедупликация работали: v1-хеш (для v1/гибридного файла)
SHALL вычисляться как SHA1 исходных байтов info-словаря (без переэнкода);
v2-хеш (для v2/гибридного файла, BEP52) SHALL извлекаться как 64-hex infohash_v2.
Для чистого v2-only файла система SHALL записывать v2-хеш (v1 у него нет).
Извлечение всех известных хешей и дозапись недостающих подчиняются требованию
«Множество инфохэшей загрузки».
Дедупликацию по активной задаче, атомарное заведение (download в состоянии
catched + записи download_infohash) и инвариант «не более одной активной
загрузки на infohash» torrent-приём SHALL проходить тем же атомарным путём, что
и magnet (см. «Приём источника и заведение загрузки», «Дедупликация приёма по
любому из хешей», «Атомарность возврата загрузки в активное состояние»).
Байты .torrent система SHALL сохранять персистентно, привязанными к загрузке,
чтобы воркер мог добавить источник в qBittorrent именно файлом (не по magnet):
раздачи закрытых трекеров и торренты без DHT по magnet-хешу метаданные не
получат. Сохранение байтов SHALL выполняться в той же write-транзакции, что и
заведение загрузки; при дедупликации (новая загрузка не создана) байты в общем
случае сохраняться SHALL NOT.
Исключение — апгрейд пойманной magnet-задачи до torrent. Если входящий
источник — байты .torrent, а дедуп попал на активную загрузку с
source_type = magnet, ещё НЕ отданную в qBittorrent (состояние catched),
система SHALL в одной write-транзакции сохранить байты .torrent,
привязав их к этой загрузке, и сменить её source_type на torrent. Тем
самым воркер добавит раздачу файлом, а не magnet-хешем (иначе на закрытом
трекере без DHT метаданные не докачаются, а magnet застрянет в metaDL →
failed). Апгрейд SHALL применяться ТОЛЬКО пока загрузка в catched (воркер
источник ещё не добавил); для уже добавленной (downloading и далее)
загрузки смена source_type при дедупе выполняться SHALL NOT — её судьба
решается путями retry/сверки, а не приёмом. Апгрейд SHALL быть best-effort:
его неуспех приём не прерывает.
Из полей .torrent система SHALL синтезировать контекст распознавания (имя
раздачи, суммарный размер, сигнал по дереву файлов, домен трекера, комментарий)
и дополнять им контекст транспорта — тем же правилом слияния, что и синтез
из полей magnet (пользовательский текст первым; при пустом тексте — только
синтез). Обогащённый контекст система SHALL сохранять в download.Context.
Синтез SHALL выполняться без сетевых запросов.
source_ref у torrent-загрузки SHALL быть человекочитаемым референсом (имя
раздачи или файла), а НЕ адресом добавления: добавление в qBittorrent идёт
байтами, и трактовать source_ref как magnet/URL для добавления система SHALL
NOT.
Scenario: Быстрый приём .torrent-файла
- GIVEN валидные байты
.torrent-файла и (опц.) текст контекста - WHEN вызывается приём
- THEN из файла извлекаются инфохэши и создаётся
downloadв состоянииcatched(source_type = torrent) с записямиdownload_infohash - AND байты файла сохраняются привязанными к загрузке
- AND ответ транспорту отдан без обращения к qBittorrent
Scenario: Инфохэш из исходных байтов info
- WHEN система разбирает v1/гибридный
.torrent-файл - THEN инфохэш v1 вычисляется как SHA1 исходных байтов info-словаря
- AND совпадает с хешем, по которому qBittorrent позже сопоставит раздачу
Scenario: v2-only файл записывается под v2-хешем
- WHEN система разбирает
.torrentтолько с метаданными v2 (без v1) - THEN у загрузки записывается v2-хеш (64-hex), совпадающий с
infohash_v2qBittorrent - AND сопоставление раздачи работает по нему
Scenario: Дубль .torrent по активной torrent-задаче
- GIVEN уже есть активная (в т.ч.
catched) загрузка с тем же infohash иsource_type = torrent - WHEN принимается
.torrentс тем же инфохэшем - THEN новая загрузка не создаётся, возвращается существующая
- AND байты торрента повторно не сохраняются (дубль)
Scenario: Апгрейд catched-magnet до torrent
- GIVEN активная загрузка в
catchedсsource_type = magnetи хешемh(magnet-задача ещё не отдана в qBittorrent) - WHEN принимается
.torrentс тем же инфохэшемh - THEN новая загрузка не создаётся, возвращается существующая
- AND байты
.torrentсохраняются привязанными к ней, а еёsource_typeстановитсяtorrent— в одной транзакции - AND воркер добавит раздачу файлом (не по magnet)
Scenario: Magnet-задача уже добавлена — апгрейда нет
- GIVEN активная загрузка с
source_type = magnetуже вdownloading(отдана в qBittorrent) - WHEN принимается
.torrentс тем же инфохэшем - THEN возвращается существующая загрузка, её
source_typeостаётсяmagnet, байты.torrentне сохраняются
Scenario: Контекст из полей файла
- WHEN принят
.torrentс именем раздачи, деревом файлов и трекерами - THEN в
download.Contextдобавляется синтез (имя, размер, сигнал по файлам, домен трекера), дополняющий текст транспорта - AND синтез выполнен без сетевых запросов
Scenario: Слишком большой .torrent отклоняется
- WHEN принимаемый
.torrent-файл превышает ограничение размера - THEN приём отклоняется с ошибкой, загрузка не создаётся