## 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 один, паттерн обращения общий; разделим, если появится второй провайдер.