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