Files
jellybit/openspec/changes/archive/2026-07-08-torrent-file-ingest/proposal.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.4 KiB

Why

Приём сейчас умеет только magnet-ссылку (ingest.Request.Source — строка; magnet.Parse — единственный парсер; при неудаче — ошибка «.torrent/url — следующий заход»). Но пользователи часто получают с трекера именно .torrent- файл, а не magnet, и у файла есть два преимущества, которых нет у голого magnet:

  • Работает там, где magnet не резолвится. Раздачи закрытых трекеров и торренты без DHT по magnet-хешу метаданные не докачают (metaDL навсегда). .torrent несёт полный info-словарь — qBittorrent стартует сразу, без докачки метаданных.
  • Максимум контекста без сети. В файле уже лежат реальное имя раздачи, дерево файлов с размерами, суммарный размер, трекеры, комментарий — гораздо богаче полей magnet. Это прямой сигнал для recognition ещё до старта скачивания.

Инфраструктура почти готова: qbt.AddRequest.Torrents [][]byte и qbt.Client.Add уже умеют слать .torrent байтами (multipart-upload), константа store.SourceTorrent уже объявлена. Не хватает трёх вещей: парсера байтов в инфохэш+контекст, персистентности байтов между быстрым приёмом (catched) и добавлением воркером, и путей приёма файла в транспортах (Telegram-документ, файл-пикер в вебе).

What Changes

  • Новый парсер internal/torrent поверх github.com/anacrolix/torrent/ metainfo: Parse(data []byte) (Info, error) вытаскивает инфохэш(и) (v1 обязательно, v2 при наличии), имя раздачи, список файлов с размерами, суммарный размер, трекеры, комментарий. Инфохэш v1 — SHA1 исходных байтов info-словаря (без переэнкода). func (Info) Context() string синтезирует человекочитаемый контекст распознавания из этих полей — в том же стиле, что magnet.Info.Context().
  • Приём (ingest) принимает байты .torrent. ingest.Request получает путь для байтов торрент-файла; при их наличии источник парсится torrent.Parse (иначе — как сейчас, magnet.Parse). Загрузка заводится с SourceType = SourceTorrent, инфохэши и дедуп — тем же атомарным путём (CreateDownloadIfNoActive), контекст — синтез из полей файла, слитый с текстом транспорта.
  • Персистентность байтов. Байты .torrent нужны воркеру на шаге добавления (быстрый приём лишь сохраняет catched). Заводим таблицу-спутник download_torrent(download_id, data) — байты живут и удаляются вместе с загрузкой, не раздувая выборки download.
  • Воркер добавляет по типу источника. processCatched ветвится по SourceType: magnet/url — как сейчас (URLs, hint из magnet.Parse); torrent — грузит байты из download_torrent, hint из имени раздачи, добавляет через qbt.AddRequest.Torrents.
  • Веб-UI: файл-пикер. Рядом со строкой ввода источника — <input type="file" accept=".torrent"> и enctype="multipart/form-data" на форме. Выбран файл — приём по байтам; иначе — по тексту. Деградация без JS сохраняется (обычный multipart-POST).
  • Telegram: приём документа. handleMessage распознаёт .torrent- документ (mime application/x-bittorrent / расширение), скачивает его байты через Bot API и подаёт в приём; подпись сообщения идёт контекстом.

Вне объёма (сознательно):

  • Торрент по http(s)-ссылке (URL на .torrent) — отдельная задача: фетч тянет сеть в fast-path приёма, требует SSRF-гарда, таймаутов и обработки ошибок загрузки.
  • Изменения механизма вывода отображаемого имени и распознавания — не трогаем; они лишь получают более богатый контекст.

Capabilities

New Capabilities

Нет. Изменение укладывается в существующие capability.

Modified Capabilities

  • ingest: добавляется приём источника из байтов .torrent (парс метаданных, извлечение инфохэшей, синтез контекста из полей файла, SourceType = torrent, персистентность байтов до добавления). Существующий magnet-путь и инварианты дедупа/инфохэшей — без изменений.
  • download-tracking: шаг «добавление пойманной загрузки в qBittorrent» ветвится по типу источника — для torrent источник добавляется байтами файла, hint отображаемого имени берётся из метаданных торрента.
  • web-ui: форма добавления получает файловый ввод .torrent (multipart/form-data), с деградацией без JS.

Impact

  • Код: новый internal/torrent (парсер + Context()); internal/ingest (ветка байтов, SourceTorrent, запись байтов); internal/store (таблица download_torrent, чтение/запись байтов, миграция); internal/worker (processCatched — диспетч по SourceType); internal/httpapi (multipart в handleUIAdd, размерный лимит); internal/tgbot (обработка m.Document, скачивание файла).
  • Зависимости: + github.com/anacrolix/torrent (пакет metainfo) в go.mod.
  • Данные: новая таблица download_torrent (миграция goose) + обновление ER-схемы docs/specs/database.md. Таблица download не меняется.
  • Конфиг: размерный лимит .torrent — константа с разумным дефолтом (вынос в конфиг — при необходимости, отдельно).
  • Совместимость: аддитивно; существующие magnet-загрузки не затронуты.