Добавил спеки для выведения имени загрузки при добавлении торрента
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
## 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
|
||||
один, паттерн обращения общий; разделим, если появится второй провайдер.
|
||||
Reference in New Issue
Block a user