11 KiB
Context
ingest.Ingest() принимает источник (magnet) и текстовый контекст
(вычищенный заголовок релиза от торрент-бота), дедуплицирует по infohash,
заводит задачу и отдаёт источник в qBittorrent через qbt.Add(). Сейчас
qbt.AddRequest несёт URLs/Category/SavePath/Paused, но не имя — в списке
qBit задача показывается своим dn из magnet, часто мусорным
(rutracker-topic-…).
В проекте уже есть всё нужное: llm.Provider (один вызов модели, JSONMode,
транспортные ретраи внутри; бюджет переразбора схемы — на стороне
вызывающего, как в recognize), пред-парс имени через go-ptn, контекст с
заголовком релиза. API qBittorrent /torrents/add принимает поле rename,
задающее отображаемое имя торрента.
Ключевое ограничение: rename действует только в момент добавления —
значит вывод имени должен случиться синхронно, до qbt.Add().
Goals / Non-Goals
Goals:
- Из контекста загрузки получать короткое читаемое имя и класть его в qBit.
- Имя строит LLM (структурированный вывод): название, год, режиссёр, тип, сезон; язык названия — русский для российского контента, иначе английский.
- До трёх попыток получить валидное имя от LLM; иначе алгоритмический фолбек без сети.
- Вывод имени — в ядре (
ingest), общий для всех транспортов; деградирует штатно и никогда не валит приём загрузки.
Non-Goals:
- Переименование уже добавленных задач; влияние на пути на диске и на
recognize/layout(имя — только ярлык в qBit). - Сетевая сверка имени с метабазами (TMDB/TVDB) — это
recognize, не здесь. - Источники кроме magnet.
Decisions
Решение 1: вывод имени — синхронно в ingest, перед qbt.Add()
rename применяется лишь при добавлении, поэтому имя считается до отдачи
источника в qBit. Вывод живёт в ядре ingest (принцип «единое ядро, тонкие
транспорты»): транспорты по-прежнему передают только Source + Context.
- Альтернатива (отклонена): добавить торрент
paused, переименовать отдельным вызовом API, снять с паузы. Сложнее, лишние запросы, гонка с поллингом — выгоды для ярлыка не оправдывают. - Плата: приём становится зависим от LLM по латентности. Гасится ограниченным таймаутом и быстрым фолбеком (см. Решение 4 и Риски).
Решение 2: имя строит LLM со структурированным выводом
Отдельный узкий промпт (не трогаем схему recognize): на вход — контекст
(и, как подсказка, имя/dn из magnet), на выход — строгий JSON:
{
"type": "movie" | "series",
"title": "название на нужном языке",
"original_title": "оригинальное название или пустая строка",
"year": число или 0,
"director": "режиссёр или пустая строка",
"season": число или null,
"is_russian": true | false
}
title модель отдаёт уже на нужном языке: для российского контента
(is_russian=true) — русское название, иначе — английское/оригинальное.
director и year — опциональные поля; если модель их извлекла, они
попадают в ярлык (Решение 3).
- Почему LLM, а не только go-ptn/регэкспы: контекст — вольный
человеческий текст с двойными названиями (
Рус / Eng), годом внутри скобок и тех. характеристиками; алгоритмически чисто вытащить «красивое» имя ненадёжно. go-ptn остаётся фолбеком (Решение 4). - Недоверенный вывод: результат — только ярлык в qBit, на пути и инварианты не влияет; жёсткая валидация пути здесь не нужна, но имя очищается от управляющих символов и переводов строк и обрезается по длине.
Решение 3: формат отображаемого имени
Рендер имени — чистая функция от структуры. Режиссёр и год — опциональные части скобки; внутри неё порядок «режиссёр, год»:
- оба:
Title (Director, Year)→Дюна: Часть вторая (Дени Вильнёв, 2024). - только год:
Title (Year); только режиссёр:Title (Director); без обоих:Title(скобка опускается). - series: к любому из вариантов добавляется
. Сезон N, если сезон есть.
Имя держим коротким и без тех. характеристик; original_title остаётся в
структуре, но в строку не добавляется. Длина ограничивается (напр. 200
символов).
Решение 4: три попытки LLM, затем алгоритмический фолбек
«Попытка» — получить от модели валидный JSON с непустым title. Бюджет —
существующий [llm].max_retries (по умолчанию 3; тот же, что у переразбора
схемы в recognize); транспортные ретраи (сеть/429/5xx) остаются внутри
llm.Provider и в этот счёт не входят. Исчерпали попытки или LLM
недоступна → алгоритмический фолбек:
первая содержательная строка контекста (та же логика, что в
tgbot.cleanContext: без ссылок, команд и UI-мусора), срез до тех.
характеристик (до [/( с годом), очистка и обрезка по длине. Фолбек —
без сетевых запросов.
Если и фолбек пуст (контекста нет/он бесполезен) → Rename не задаём,
qBittorrent оставляет своё имя. Поведение «без контекста» не меняется.
- Бюджет попыток: переиспользуем существующий
[llm].max_retries(default 3). Семантика чуть иная, чем у переразбора схемы, но для проекта такого размера отдельный параметр избыточен — разделим при необходимости.
Решение 5: интерфейс вывода имени и graceful-деградация
ingest зависит от узкого интерфейса (напр. Namer/функция
DeriveName(ctx, Context, magnetHint) string), реализованного поверх
llm.Provider + фолбек. Так вывод имени тестируется без сети, а при
отсутствии настроенного LLM работает только фолбек. Любая ошибка вывода
имени логируется и не прерывает Ingest: пустое имя → добавляем без
rename.
Решение 6: qbt.AddRequest.Rename
Добавляем поле Rename string; при непустом значении пишем form-field
rename. Пустое — поле не отправляется (поведение не меняется).
Risks / Trade-offs
- Латентность приёма из-за вызова LLM → вывод имени ограничен общим
[llm].timeout; по таймауту/ошибке — фолбек. Приём не должен зависать на медленной модели. - Стоимость токенов на каждую загрузку → промпт узкий и короткий;
расход уже снимается с провода (
llm.Response.Usage). При желании в будущем — кэш/выключатель, вне объёма. renameзатрагивает имя корневой папки многофайловой раздачи → downstream безопасен: jellybit всегда читает реальные пути из qBit API (Files,content_path), а не выводит их из имени. Инвариант «источник неприкосновенен» цел — переименование делает сам qBittorrent при добавлении. Перепроверить руками на реальном qBit (Open Questions).- Галлюцинация имени LLM → последствия минимальны (всего лишь ярлык); имя очищается и обрезается; на распознавание/раскладку не влияет.
Migration Plan
Чистое добавление, без миграций БД и слома API. Включается само (если LLM
настроен — работает LLM-путь, иначе только фолбек). Откат — снять
проброс Rename (старые задачи в qBit не затрагиваются).
Open Questions
- Подтвердить на реальном qBittorrent, что
renameменяет отображаемое имя (и поведение для многофайловой раздачи) ожидаемо. Решается верификацией на задаче 6.2; код от ответа не зависит (пути берутся из qBit API).
Решено: бюджет попыток — [llm].max_retries (default 3); таймаут вывода
имени — общий [llm].timeout. Отдельные параметры не вводим: провайдер LLM
один, паттерн обращения общий; разделим, если появится второй провайдер.