Голый magnet без dn/контекста заставлял namer звать LLM на пустом входе, и модель галлюцинировала мусорное имя («Unknown»), которое писалось и в display_name, и в rename qBittorrent, а заодно ломало UI-фолбэк на распознанное название. Верное каноническое имя, вычисляемое позже при распознавании, никуда не переливалось. - naming: гард пустого входа в DeriveName (нет контекста и подсказки → "" без вызова LLM) + детерминированный форматтер FormatTitleYear. - qbt: операция RenameTorrent (переименование существующей раздачи). - store: SetDisplayName — обновление имени постфактум без гарда состояния. - worker: refreshDisplayNameLocked/RefreshDisplayName — перелив канонического имени (эффективный план) в display_name + best-effort rename раздачи по реальному t.Hash; авто-триггер при подтверждении матча (choose/manual add). - web-ui: кнопка «Обновить имя» на странице загрузки (htmx-своп заголовка, деградация без JS), видимая при наличии распознавания (вкл. done/orphaned). Спека: дельты ingest/review/web-ui влиты в openspec/specs; change refresh-display-name заархивирован. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
7.4 KiB
Markdown
101 lines
7.4 KiB
Markdown
## MODIFIED Requirements
|
||
|
||
### Requirement: Вывод имени через LLM со структурированным выводом
|
||
|
||
Система SHALL строить отображаемое имя с помощью LLM (структурированный
|
||
JSON-вывод), извлекая из контекста тип (movie/series), название, год,
|
||
режиссёра и (для сериала) номер сезона. Год и режиссёр — опциональные поля.
|
||
|
||
Если и контекст, и подсказка из полей источника (`dn` magnet / имя `.torrent`)
|
||
пусты, система SHALL считать имя не выведенным и SHALL NOT вызывать LLM (выводить
|
||
имя не из чего — вызов на пустом входе способен лишь галлюцинировать). В этом
|
||
случае загрузка добавляется без `rename`, а `download.display_name` остаётся
|
||
пустым.
|
||
|
||
Название SHALL быть на русском языке для российского контента и на
|
||
английском (оригинальном) — для остального.
|
||
|
||
Система SHALL предпринять ограниченное число попыток получить от LLM валидный
|
||
результат (корректный JSON с непустым названием); бюджет попыток —
|
||
`[llm].max_retries` (по умолчанию 3). Транспортные ретраи провайдера LLM
|
||
(сетевые сбои, 429, 5xx) в этот счёт не входят.
|
||
|
||
Недоступность или ошибка LLM SHALL NOT прерывать приём загрузки: система
|
||
переходит к алгоритмическому фолбеку.
|
||
|
||
#### Scenario: LLM возвращает структурированное имя
|
||
|
||
- **WHEN** LLM по контексту возвращает валидный JSON с непустым названием
|
||
- **THEN** система формирует отображаемое имя из его полей (название, год,
|
||
для сериала — сезон)
|
||
|
||
#### Scenario: Российский контент — название на русском
|
||
|
||
- **WHEN** контент распознан как российский
|
||
- **THEN** в отображаемом имени используется русское название
|
||
|
||
#### Scenario: Исчерпан бюджет попыток LLM
|
||
|
||
- **WHEN** LLM за отведённые попытки (`[llm].max_retries`) не вернул валидный
|
||
результат либо недоступен
|
||
- **THEN** система не прерывает приём и переходит к алгоритмическому фолбеку
|
||
|
||
#### Scenario: Пустой вход — LLM не вызывается
|
||
|
||
- **GIVEN** пойманная загрузка без пользовательского контекста и без подсказки
|
||
из полей источника (голый magnet без `dn`)
|
||
- **WHEN** система выводит отображаемое имя на шаге добавления
|
||
- **THEN** LLM не вызывается, имя считается не выведенным
|
||
- **AND** загрузка добавляется без `rename`, а `download.display_name` пуст
|
||
(заголовок в веб-UI берётся из фолбека)
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: Обновление отображаемого имени по распознаванию
|
||
|
||
Система SHALL уметь обновлять отображаемое имя загрузки после того, как
|
||
распознавание дало каноническое название, — переливая уже вычисленное имя (без
|
||
нового вызова LLM) в `download.display_name` и в имя раздачи qBittorrent.
|
||
|
||
Источником имени SHALL быть **эффективное** распознанное название: пины
|
||
`title`/`year` (если матч подтверждён), иначе `recognition.title`/`year` — то
|
||
же название, что использует раскладка. Формат SHALL быть коротким детерминированным
|
||
ярлыком `Title (Year)` (год опционален), с той же очисткой от управляющих
|
||
символов и обрезкой по длине, что и вывод имени на шаге добавления. Пустой
|
||
источник (нет распознавания или пустое название) SHALL приводить к отсутствию
|
||
изменений (no-op).
|
||
|
||
Переименование раздачи в qBittorrent SHALL адресоваться по infohash своей
|
||
раздачи и SHALL быть best-effort: сбой (раздача удалена, qBittorrent недоступен)
|
||
SHALL NOT проваливать обновление — `download.display_name` обновляется в любом
|
||
случае, ошибка внешнего вызова логируется. Как и на шаге добавления,
|
||
отображаемое имя SHALL влиять только на отображение и SHALL NOT влиять на пути
|
||
файлов, распознавание или раскладку.
|
||
|
||
Обновление имени SHALL иметь две точки входа: **авто** — при подтверждённом
|
||
матче (см. capability `review`); **ручную** — по явному действию пользователя.
|
||
Ручное действие SHALL перезаписывать текущее имя всегда; авто SHALL перезаписывать,
|
||
когда выведенное имя непусто.
|
||
|
||
#### Scenario: Перелив имени в загрузку и раздачу
|
||
|
||
- **GIVEN** загрузка с распознанным непустым каноническим названием
|
||
- **WHEN** запускается обновление отображаемого имени
|
||
- **THEN** `download.display_name` устанавливается в `Title (Year)`
|
||
- **AND** раздача в qBittorrent переименовывается в то же имя (по infohash своей
|
||
раздачи)
|
||
|
||
#### Scenario: qBittorrent недоступен — имя у загрузки всё равно обновлено
|
||
|
||
- **GIVEN** обновление отображаемого имени с выведенным непустым именем
|
||
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой (недоступен
|
||
или раздача удалена)
|
||
- **THEN** `download.display_name` всё равно обновлён
|
||
- **AND** ошибка внешнего вызова qBittorrent логируется, операция не проваливается
|
||
|
||
#### Scenario: Нет распознавания — обновление ничего не делает
|
||
|
||
- **GIVEN** загрузка без распознанного названия (пустой источник имени)
|
||
- **WHEN** запускается обновление отображаемого имени
|
||
- **THEN** ни `download.display_name`, ни имя раздачи не меняются (no-op)
|