## 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)