Files
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

96 lines
7.4 KiB
Markdown

## 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-загрузки не затронуты.