Files
jellybit/openspec/changes/archive/2026-07-08-torrent-file-ingest/tasks.md
T
avandClaude Opus 4.8 14d615a7c2 Приём: добавление загрузки по .torrent-файлу
Принимаем .torrent как загруженные байты — через файл-пикер в веб-форме и
Telegram-документ, наряду с magnet. Файл несёт полные метаданные: работает
там, где magnet не резолвится (закрытые трекеры, без DHT), и даёт максимум
контекста для распознавания без сети.

- internal/torrent: парсер поверх anacrolix/torrent/metainfo — инфохэш(и)
  (v1 SHA1 исходных байтов info; v2 BEP52 при наличии) + Context() из имени,
  дерева файлов, размера, трекеров. Извлечение файлов панико-безопасно
  (недоверенный вход).
- Персистентность байтов: таблица-спутник download_torrent (миграция 0009);
  пишется в транзакции создания загрузки, только на ветке создания (не при
  дедупе). Байты живут весь срок строки — нужны для повторного добавления
  при retry.
- ingest: Request.TorrentData/TorrentName, диспетч парсера; source_ref —
  человекочитаемый референс (имя раздачи/файла), не адрес добавления.
- worker: общий sourceAddParts ветвит по source_type в ОБОИХ add-путях —
  processCatched и Retry (torrent добавляется файлом, не magnet-хешем).
- Транспорты: multipart-форма с файл-пикером (деградация без JS) и приём
  Telegram-документа (скачивание с редактированием токена из ошибок — секрет
  не в логи; обработка до ветки pending/текста).

Разработка по OpenSpec (SDD): change torrent-file-ingest, два чекпоинта ревью
(дизайн до кода, код до архива) сабагентами; дельты влиты в спеки, change
архивирован. Ручная проверка на живом qBittorrent (7.3) — за деплоем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:52:31 +03:00

7.0 KiB
Raw Blame History

1. Парсер internal/torrent

  • 1.1 Добавить зависимость github.com/anacrolix/torrent (пакет metainfo); task tidy
  • 1.2 Пакет internal/torrent: Parse(data []byte) (Info, error) через metainfo.Load/UnmarshalInfo; Info{Infohash, Infohashes, DisplayName, Files, TotalLength, Trackers, Comment}. Инфохэш v1 — из HashInfoBytes (SHA1 исходных байтов info, нижний hex); v2/гибрид — извлечь v2 (64-hex, как infohash_v2 qBit), для v2-only хранить именно v2. Уточнить точный v2-API anacrolix при apply
  • 1.3 func (Info) Context() string — строки-факты (\n): имя раздачи, Размер: из TotalLength, сигнал по файлам (число/расширения), Трекер: домен из announce/announce-list, комментарий (если содержателен). Без сети
  • 1.4 Хелперы размера/домена: дублировать тривиальные из internal/magnet или вынести общий (решение по месту)
  • 1.5 Тесты: одиночный файл; много файлов; гибрид v1+v2; трекеры из announce-list; повреждённые/не-torrent байты → ошибка; Context() — содержательный и пустой случай; отсутствие сетевых вызовов

2. Хранилище: байты торрента

  • 2.1 Goose-миграция: таблица download_torrent(download_id TEXT PRIMARY KEY REFERENCES download(id) ON DELETE CASCADE, data BLOB NOT NULL)
  • 2.2 Обновить ER-схему docs/specs/database.md (новая таблица + связь)
  • 2.3 Изменить сигнатуру CreateDownloadIfNoActive(ctx, d, hashes, torrentBlob []byte): на ветке создания INSERT INTO download_torrent в той же tx (при дедупе — не пишет). Обновить интерфейсы Store в internal/ingest и internal/worker и вызовы magnet-приёма/discover.go (передают nil). Добавить GetTorrentData(ctx, downloadID) ([]byte, error)
  • 2.4 Тесты store: запись/чтение байтов; при дедупе байты не пишутся; CASCADE-уборка при (гипотетическом) удалении загрузки. Байты НЕ удаляются при промоушене catched → downloading (нужны для retry)

3. Приём: ветка байтов в ingest

  • 3.1 Расширить ingest.Request полем TorrentData []byte (опц.)
  • 3.2 В Ingest: при непустом TorrentDatatorrent.Parse, SourceType = SourceTorrent, SourceRef = имя раздачи/файла (не URL), контекст = mergeContext(req.Context, tinfo.Context()), запись байтов (2.3) в транзакции создания; иначе — прежний magnet-путь без изменений
  • 3.3 Ограничение размера входа на границе (константа, дефолт напр. 8 MiB)
  • 3.4 Тесты ingest: приём .torrentcatched, source_type=torrent, инфохэши, байты сохранены, контекст синтезирован; дедуп по инфохэшу (байты не пишутся); превышение лимита → ошибка

4. Воркер: диспетч добавления по типу источника (оба add-пути)

  • 4.1 Общий хелпер addSource(ctx, d, rename): ветвь по d.SourceType — magnet/url (URLs, hint из magnet.Parse); torrent (GetTorrentData, hint из torrent.Parse(data).DisplayName, qbt.Add(AddRequest{Torrents: [][]byte{data}, …}))
  • 4.2 Применить хелпер в processCatched (worker.go:359-365) И в Retry (worker.go:677-691): снять magnet-only гейт в Retry — torrent без живой раздачи должен добавляться байтами, а не активироваться без Add. Сохранить откат активации при сбое add в Retry
  • 4.3 Общими остаются: ре-валидация перехода, обработка сбоя add (processCatched — остаётся catched; Retry — откат), namer/qbt вне блокировки
  • 4.4 Тесты воркера: (а) catched torrent → qbt.Add с Torrents (не URLs), переход в downloading, сбой add → остаётся catched; (б) Retry torrent без живой раздачи → Add с Torrents, переход в downloading; сбой add → откат в прежнее состояние

5. Транспорт: веб-форма (multipart)

  • 5.1 Шаблон формы: enctype="multipart/form-data", <input type="file" name="torrent" accept=".torrent,application/x-bittorrent"> рядом со строкой источника; строка источника не обязательна при выбранном файле
  • 5.2 handleUIAdd: http.MaxBytesReader/лимит, ParseMultipartForm; есть файл torrentIngest(Request{TorrentData, Context}); иначе — прежний путь. PRG-редирект/htmx как сейчас
  • 5.3 Деградация без JS: обычный multipart-POST приводит к тому же результату
  • 5.4 Тест обработчика: multipart с .torrent → загрузка заведена; без файла → прежний текстовый путь; превышение размера → ошибка

6. Транспорт: Telegram-документ

  • 6.1 В handleMessage обрабатывать m.Document до ветки pending-hint и текстовой ветки (у документа m.Text пуст): mime application/x-bittorrent или расширение .torrent → скачать байты (GetFileDirectURL + HTTP GET, лимит), Ingest(Request{TorrentData, Context: m.Caption})
  • 6.2 Документ не-.torrent → прежний ответ-подсказка; сохранить allowlist-проверку. AllowedUpdates не менять (документ — внутри message)
  • 6.3 Тест парса/маршрутизации документа (mime/расширение → ветка байтов); скачивание — за интерфейсом/мок

7. Проверка

  • 7.1 task test и task lint зелёные
  • 7.2 openspec validate torrent-file-ingest --strict проходит
  • 7.3 Ручная проверка: .torrent через веб-пикер и Telegram-документ → catched → downloading, задача видна в qBittorrent с rename