Принимаем .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>
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- документ (mimeapplication/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-загрузки не затронуты.