Имя: восстановление display_name после распознавания + гард пустого входа

Голый 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>
This commit is contained in:
av
2026-07-10 16:51:04 +03:00
co-authored by Claude Opus 4.8
parent e2ea1840c9
commit 0c9421f4c1
26 changed files with 1179 additions and 21 deletions
+63
View File
@@ -61,6 +61,12 @@ SHALL обрезаться по ограничению длины.
JSON-вывод), извлекая из контекста тип (movie/series), название, год,
режиссёра и (для сериала) номер сезона. Год и режиссёр — опциональные поля.
Если и контекст, и подсказка из полей источника (`dn` magnet / имя `.torrent`)
пусты, система SHALL считать имя не выведенным и SHALL NOT вызывать LLM (выводить
имя не из чего — вызов на пустом входе способен лишь галлюцинировать). В этом
случае загрузка добавляется без `rename`, а `download.display_name` остаётся
пустым.
Название SHALL быть на русском языке для российского контента и на
английском (оригинальном) — для остального.
@@ -89,6 +95,15 @@ JSON-вывод), извлекая из контекста тип (movie/series)
результат либо недоступен
- **THEN** система не прерывает приём и переходит к алгоритмическому фолбеку
#### Scenario: Пустой вход — LLM не вызывается
- **GIVEN** пойманная загрузка без пользовательского контекста и без подсказки
из полей источника (голый magnet без `dn`)
- **WHEN** система выводит отображаемое имя на шаге добавления
- **THEN** LLM не вызывается, имя считается не выведенным
- **AND** загрузка добавляется без `rename`, а `download.display_name` пуст
(заголовок в веб-UI берётся из фолбека)
### Requirement: Алгоритмический фолбек вывода имени без сети
При неудаче LLM система SHALL выводить имя алгоритмически, без сетевых
@@ -111,6 +126,54 @@ JSON-вывод), извлекая из контекста тип (movie/series)
- **WHEN** ни LLM, ни алгоритмический фолбек не дали непустого имени
- **THEN** система добавляет загрузку без параметра `rename`
### 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)
### Requirement: Приём источника и заведение загрузки
Приём SHALL быть единым **быстрым** use-case, общим для всех транспортов (HTTP,