display_name: слоистое разрешение полей + сохранение режиссёра из контекста
Единый источник полей отображаемого имени и один рендер полного ярлыка на всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный «Название (режиссёр, год). Сезон N» но выбрасывал структуру, а перелив по распознаванию — усечённый «Title (Year)». - Слоистое разрешение скаляров имени: override → recognition(+match) → новый базовый слой «контекст» (download.parsed_context, JSON naming.Fields). - naming: публичные Fields/Label/Derive, вынесен единый рендер; удалён FormatTitleYear. Сводка сезонов вынесена в recognize.SeasonSummary. - Режиссёр из метабазы (решение A2): TMDB/TVDB credits через опциональный metadata.DirectorProvider; авто-матч кладёт в plan.Director, ручной выбор кандидата тянет credits и пиннит ovrDirector. Метабаза бьёт контекст. - refreshDisplayNameLocked строит полный ярлык из эффективных полей; инфо-панель ревью показывает загруженного режиссёра. - Миграция 0011_parsed_context + ER-схема. Всё косметика: на пути/раскладку не влияет, приём/вывод имени не валятся (best-effort). Закрывает беклог-задачу «Кнопка „Обновить имя“: полный формат ярлыка». OpenSpec: archive/2026-07-11-field-resolution-display-name (ingest, recognition, metadata-match, review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
# ingest Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Сохранение извлечённой из контекста структуры имени
|
||||
|
||||
Система SHALL сохранять структуру имени, извлечённую LLM со структурированным
|
||||
выводом на шаге добавления (тип, название, оригинальное название, год, режиссёр,
|
||||
сезон), у загрузки (`download.parsed_context`, JSON), чтобы её поля могли
|
||||
переиспользоваться при последующем выводе имени без повторного вызова LLM.
|
||||
Алгоритмический фолбек структуры не даёт (его выход — только строка имени), тогда
|
||||
`parsed_context` остаётся пустым — это штатно (нижний слой отсутствует). Сохранённая структура SHALL
|
||||
быть **базовым (наименее доверенным) слоем** источника полей имени: её значения
|
||||
берутся, только если более доверенный слой (распознавание/матч, ручные правки)
|
||||
соответствующего поля не дал.
|
||||
|
||||
Сохранение SHALL быть best-effort и косметическим: неудача записи `parsed_context`
|
||||
SHALL NOT проваливать добавление загрузки, а сама структура SHALL влиять только на
|
||||
отображаемое имя и SHALL NOT влиять на пути файлов, распознавание или раскладку.
|
||||
Пустая/невыведенная структура (нет контекста и подсказки) SHALL приводить к
|
||||
пустому `parsed_context` (нечего сохранять).
|
||||
|
||||
#### Scenario: Извлечённый режиссёр сохраняется у загрузки
|
||||
|
||||
- **GIVEN** контекст загрузки, из которого LLM извлёк режиссёра и год
|
||||
- **WHEN** система выводит имя на шаге добавления
|
||||
- **THEN** извлечённая структура (в т.ч. режиссёр) сохраняется в
|
||||
`download.parsed_context`
|
||||
- **AND** отображаемое имя формируется как и прежде (полный ярлык)
|
||||
|
||||
#### Scenario: Сбой сохранения структуры не валит добавление
|
||||
|
||||
- **GIVEN** запись `parsed_context` завершается ошибкой
|
||||
- **WHEN** идёт шаг добавления загрузки
|
||||
- **THEN** загрузка всё равно добавляется в qBittorrent (с `rename`, если имя
|
||||
выведено)
|
||||
- **AND** ошибка логируется, приём/добавление не проваливается
|
||||
|
||||
#### Scenario: Пустой вход — пустая структура
|
||||
|
||||
- **GIVEN** пойманная загрузка без контекста и без подсказки из полей источника
|
||||
- **WHEN** выполняется шаг добавления
|
||||
- **THEN** структура не выводится, `download.parsed_context` пуст
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Обновление отображаемого имени по распознаванию
|
||||
|
||||
Система SHALL уметь обновлять отображаемое имя загрузки после того, как
|
||||
распознавание дало каноническое название, — переливая уже вычисленное имя (без
|
||||
нового вызова LLM) в `download.display_name` и в имя раздачи qBittorrent.
|
||||
|
||||
Источником имени SHALL быть **эффективные поля имени**, разрешённые по слоям
|
||||
сверху вниз (берётся первый непустой слой): (1) ручные правки `override`;
|
||||
(2) распознавание с вложенным подтверждённым матчем (`recognition`, куда матч
|
||||
метабазы уже вложил каноничные название/год/режиссёра); (3) сохранённая
|
||||
структура из контекста (`download.parsed_context`). Так каждое поле берётся из
|
||||
самого доверенного доступного источника, а данные из контекста (например
|
||||
режиссёр) не теряются, если распознавание/матч их не дали. Название из слоя
|
||||
распознавания SHALL совпадать с тем, что использует раскладка (эффективный
|
||||
`title`), чтобы отображаемое имя не расходилось с целевыми путями.
|
||||
|
||||
Формат SHALL быть тем же полным детерминированным ярлыком, что и на шаге
|
||||
добавления: «Название (режиссёр, год)», где режиссёр и год опциональны, а для
|
||||
сериала добавляется сводка сезонов («. Сезон N» для одного сезона; «. Сезоны …»
|
||||
для многосезонного пака; отметка спецвыпусков) — согласованная со сводкой сезонов
|
||||
на экране просмотра. Применяются та же очистка от управляющих символов и обрезка
|
||||
по ограничению длины. Пустой источник (нет ни распознавания, ни сохранённой
|
||||
структуры, дающих непустое название) 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` устанавливается в полный ярлык
|
||||
«Название (режиссёр, год)» (для сериала — со сводкой сезонов)
|
||||
- **AND** раздача в qBittorrent переименовывается в то же имя (по infohash своей
|
||||
раздачи)
|
||||
|
||||
#### Scenario: Режиссёр из контекста переживает распознавание без матча
|
||||
|
||||
- **GIVEN** загрузка, где режиссёр был извлечён из контекста, а распознавание
|
||||
прошло без подтверждённого матча (режиссёр из метабазы недоступен)
|
||||
- **WHEN** запускается обновление отображаемого имени
|
||||
- **THEN** в ярлыке используется режиссёр из сохранённого контекста
|
||||
- **AND** название/год берутся из распознавания
|
||||
|
||||
#### Scenario: Режиссёр из матча бьёт контекстного
|
||||
|
||||
- **GIVEN** загрузка, где режиссёр есть и в контексте, и в подтверждённом матче
|
||||
- **WHEN** формируется ярлык
|
||||
- **THEN** используется режиссёр из матча (более доверенный слой)
|
||||
|
||||
#### Scenario: qBittorrent недоступен — имя у загрузки всё равно обновлено
|
||||
|
||||
- **GIVEN** обновление отображаемого имени с выведенным непустым именем
|
||||
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой (недоступен
|
||||
или раздача удалена)
|
||||
- **THEN** `download.display_name` всё равно обновлён
|
||||
- **AND** ошибка внешнего вызова qBittorrent логируется, операция не проваливается
|
||||
|
||||
#### Scenario: Нет источника имени — обновление ничего не делает
|
||||
|
||||
- **GIVEN** загрузка без распознанного названия и без сохранённой структуры
|
||||
(пустой источник имени)
|
||||
- **WHEN** запускается обновление отображаемого имени
|
||||
- **THEN** ни `download.display_name`, ни имя раздачи не меняются (no-op)
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# metadata-match Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Подтверждение матча и каноническое имя
|
||||
|
||||
При единичном сильном матче система SHALL брать из записи базы официальный
|
||||
`provider` (`tmdb`|`tvdb`|`tvmaze`) и `provider_id`, а также каноническое название
|
||||
и год, и подменять ими соответствующие поля плана (для сериала — с учётом внешнего
|
||||
тега TVDB/IMDb из `externals`, идущего в имя папки). Матч SHALL считаться
|
||||
подтверждённым только при ровно одном сильном кандидате; при нуле или нескольких
|
||||
кандидатах подтверждённого матча быть SHALL NOT (авто-раскладка не разрешается,
|
||||
кандидаты уходят в review). Работа с базами опциональна: при выключенных базах
|
||||
сверка не выполняется и подтверждённого матча нет.
|
||||
|
||||
При подтверждённом матче система SHALL дополнительно попытаться получить из базы
|
||||
**режиссёра** (TMDB/TVDB credits) и вложить его в план (`director`) как
|
||||
недоверенное косметическое значение для вывода отображаемого имени. Тот же способ
|
||||
выборки режиссёра по `provider:id` SHALL быть доступен при закреплении вручную
|
||||
выбранного в ревью кандидата (см. `review`), т.к. основной путь подтверждения
|
||||
матча — ручной выбор, а не авто. Выборка режиссёра SHALL быть best-effort: её
|
||||
недоступность, отсутствие в базе или провайдер без режиссёра (напр. TVMaze) SHALL
|
||||
NOT проваливать распознавание/матч/выбор — `director` остаётся пустым, а имя
|
||||
выводится без режиссёра или из более низкого слоя (сохранённый контекст). Режиссёр
|
||||
из метабазы SHALL иметь приоритет над режиссёром из контекста (более проверенный
|
||||
источник).
|
||||
|
||||
Режиссёр — недоверенное человекочитаемое поле: он SHALL NOT участвовать в
|
||||
структурной валидации/гейте авто-раскладки, а его очистка (управляющие символы,
|
||||
пробелы, лимит длины) применяется при рендере отображаемого имени, а не в
|
||||
plan-санитайзинге.
|
||||
|
||||
#### Scenario: Единичный матч даёт id и каноническое имя
|
||||
|
||||
- **GIVEN** поиск вернул ровно одного сильного кандидата TMDB для фильма
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** план получает `provider`=`tmdb`, `provider_id`, каноническое название и год
|
||||
|
||||
#### Scenario: Матч подтягивает режиссёра
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч TMDB для фильма, у которого в credits
|
||||
указан режиссёр
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** в план вкладывается `director` из credits
|
||||
- **AND** отображаемое имя может использовать этого режиссёра
|
||||
|
||||
#### Scenario: Режиссёр недоступен — матч не ломается
|
||||
|
||||
- **GIVEN** подтверждённый матч, для которого выборка режиссёра недоступна или
|
||||
провайдер режиссёра не отдаёт
|
||||
- **WHEN** матч подтверждается
|
||||
- **THEN** `director` остаётся пустым
|
||||
- **AND** матч подтверждён, распознавание не проваливается
|
||||
|
||||
#### Scenario: Несколько кандидатов — матч не подтверждён
|
||||
|
||||
- **GIVEN** поиск вернул более одного подходящего кандидата
|
||||
- **WHEN** оценивается матч
|
||||
- **THEN** подтверждённого матча нет, кандидаты собираются для выбора в review
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# recognition Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Разбор сигналов LLM в структурированный план
|
||||
|
||||
Система SHALL передавать LLM недоверенные сигналы (имя торрента, дерево файлов с
|
||||
размерами, текстовый контекст и накопленные подсказки, пред-парс) и получать
|
||||
структурированный план в схеме: `type` (`movie`|`series`), `title`,
|
||||
`original_title`, `year`, `provider_hint`, `files[]` и `confidence`. Каждый
|
||||
элемент `files[]` SHALL нести `src`, `role`
|
||||
(`main`|`episode`|`subtitle`|`extra`|`sample`|`ignore`) и, для сериала,
|
||||
per-file `season`/`episode` (отдельного скалярного `season` быть SHALL NOT — так
|
||||
выражаются мультисезонные паки и спецвыпуски). План SHALL приниматься только
|
||||
если каждый `files[].src` совпадает с реальным файлом торрента.
|
||||
|
||||
План MAY дополнительно нести опциональное скалярное поле `director` (режиссёр).
|
||||
Это поле НЕ требуется от LLM и НЕ участвует в структурной валидации или гейте
|
||||
авто-раскладки; его заполняют подтверждённый матч метабазы (авто) или закреплённый
|
||||
в ревью выбранный источник (через override, см. `metadata-match`/`review`) как
|
||||
недоверенное косметическое значение для вывода отображаемого имени. Как недоверенное
|
||||
человекочитаемое поле, `director` SHALL NOT входить в plan-санитайзинг (он чистит
|
||||
`title`/`original_title`/`provider_hint`); очистка режиссёра применяется при рендере
|
||||
имени. Пустой `director` SHALL быть штатным (режиссёр неизвестен).
|
||||
|
||||
#### Scenario: План сериала с per-file нумерацией
|
||||
|
||||
- **GIVEN** сезон-пак из 10 видеофайлов
|
||||
- **WHEN** LLM возвращает план
|
||||
- **THEN** `type` = `series`, а каждый видеофайл несёт свои `season`/`episode`
|
||||
|
||||
#### Scenario: Несуществующий src отклоняется
|
||||
|
||||
- **GIVEN** ответ LLM, где `files[].src` не совпадает ни с одним файлом торрента
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** такой план не принимается как валидный
|
||||
|
||||
#### Scenario: Режиссёр не требуется от LLM и не влияет на гейт
|
||||
|
||||
- **GIVEN** ответ LLM без поля `director`
|
||||
- **WHEN** план разбирается и оценивается
|
||||
- **THEN** разбор успешен, `director` пуст
|
||||
- **AND** отсутствие режиссёра не влияет на структурную валидацию и решение
|
||||
auto/review
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# review Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Подтверждение матча обновляет отображаемое имя
|
||||
|
||||
Система SHALL при подтверждении матча в ревью запускать обновление отображаемого
|
||||
имени загрузки по подтверждённому распознаванию (см. capability `ingest`):
|
||||
переливать **полный ярлык** имени — «Название (режиссёр, год)», для сериала со
|
||||
сводкой сезонов — в `download.display_name` и в имя раздачи qBittorrent, без
|
||||
нового вызова LLM. Имя строится из эффективных полей (override → распознавание с
|
||||
вложенным матчем → сохранённый контекст). Подтверждением матча SHALL
|
||||
считаться как выбор кандидата из списка совпадений, так и ручное добавление
|
||||
источника по id/URL (оба закрепляют провайдера и каноническое название).
|
||||
|
||||
При закреплении выбранного/добавленного источника система SHALL best-effort
|
||||
получить режиссёра этого источника из метабазы (credits по `provider:id`, см.
|
||||
`metadata-match`) и закрепить его как override, чтобы он попал в эффективные поля
|
||||
и в ярлык. Недоступность credits или отсутствие режиссёра SHALL NOT проваливать
|
||||
выбор источника: режиссёр остаётся из более низкого слоя (сохранённый контекст)
|
||||
или пустым. Так режиссёр из метабазы появляется и на **основном** пути
|
||||
подтверждения — ручном выборе кандидата, а не только при авто-матче.
|
||||
|
||||
Обновление SHALL выполняться после успешного закрепления выбора кандидата и
|
||||
SHALL быть best-effort по отношению к qBittorrent: недоступность клиента SHALL
|
||||
NOT проваливать команду ревью. Это согласуется с инвариантом «авто-действие
|
||||
только при подтверждённом матче».
|
||||
|
||||
#### Scenario: Выбор кандидата обновляет имя
|
||||
|
||||
- **GIVEN** загрузка в ревью с пустым или неинформативным `display_name`
|
||||
(например, «Unknown») и списком кандидатов
|
||||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||||
- **THEN** выбор кандидата закрепляется как и прежде
|
||||
- **AND** `download.display_name` обновляется полным ярлыком
|
||||
«Название (режиссёр, год)» (для сериала — со сводкой сезонов)
|
||||
- **AND** раздача в qBittorrent переименовывается в то же имя
|
||||
|
||||
#### Scenario: Ручное добавление источника обновляет имя
|
||||
|
||||
- **GIVEN** загрузка в ревью без совпадений в списке
|
||||
- **WHEN** пользователь вручную добавляет источник по id/URL, подтверждая матч
|
||||
- **THEN** источник закрепляется как и прежде
|
||||
- **AND** `download.display_name` и имя раздачи в qBittorrent обновляются
|
||||
полным ярлыком подтверждённого источника
|
||||
|
||||
#### Scenario: Выбор кандидата подтягивает режиссёра в ярлык
|
||||
|
||||
- **GIVEN** загрузка в ревью, у выбранного кандидата в credits метабазы указан
|
||||
режиссёр
|
||||
- **WHEN** пользователь выбирает кандидата, подтверждая матч
|
||||
- **THEN** режиссёр best-effort извлекается из метабазы и закрепляется override
|
||||
- **AND** `download.display_name` получает полный ярлык с этим режиссёром
|
||||
|
||||
#### Scenario: Режиссёр кандидата недоступен — выбор не ломается
|
||||
|
||||
- **GIVEN** выбор кандидата, для которого credits недоступны или режиссёра нет
|
||||
- **WHEN** пользователь подтверждает матч
|
||||
- **THEN** выбор источника выполнен, режиссёр берётся из сохранённого контекста
|
||||
или остаётся пустым
|
||||
- **AND** команда ревью не возвращает ошибку
|
||||
|
||||
#### Scenario: Недоступность qBittorrent не ломает выбор кандидата
|
||||
|
||||
- **GIVEN** выбор кандидата в ревью
|
||||
- **WHEN** переименование раздачи в qBittorrent завершается ошибкой
|
||||
- **THEN** выбор кандидата и обновление `download.display_name` выполнены
|
||||
- **AND** команда ревью не возвращает ошибку
|
||||
|
||||
### Requirement: Инфо и предпросмотр выбранного источника
|
||||
|
||||
В едином блоке выбора источника экран ревью SHALL показывать для **выбранного
|
||||
(активного)** источника две части: **инфо** — тип (read-only, movie/series),
|
||||
название, оригинальное название, год, режиссёра (из подтверждённого матча/
|
||||
кандидата, когда доступен; иначе пусто/прочерк, не ломая вёрстку), для сериала —
|
||||
сводку сезонов (один сезон, диапазон/список для многосезонного пака или
|
||||
«Спецвыпуски»); и **предпросмотр раскладки** — целевые пути хардлинков этого
|
||||
источника. Обе части SHALL относиться именно к активному источнику и SHALL
|
||||
обновляться при смене выбора. Отрисовка блока (показ инфо и предпросмотра) MUST
|
||||
NOT создавать хардлинки: раскладка создаётся только явным действием «Применить».
|
||||
Совпадение целевых путей предпросмотра с результатом применения регулируется
|
||||
требованием «Превью раскладки через единую логику именования» (`web-ui`).
|
||||
|
||||
#### Scenario: Инфо и предпросмотр относятся к активному источнику
|
||||
|
||||
- **GIVEN** в списке активен кандидат метабазы
|
||||
- **WHEN** пользователь смотрит инфо-часть и предпросмотр раскладки
|
||||
- **THEN** показаны тип, название, ориг. название, год (и сводка сезонов для
|
||||
сериала) именно этого источника и предпросмотр его целевых путей
|
||||
|
||||
#### Scenario: Просмотр блока не создаёт раскладку
|
||||
|
||||
- **GIVEN** экран ревью с показанным блоком выбора источника
|
||||
- **WHEN** пользователь только просматривает инфо и предпросмотр, не нажимая
|
||||
«Применить»
|
||||
- **THEN** хардлинки не создаются, файлы под `paths.movies`/`series` не
|
||||
меняются
|
||||
|
||||
#### Scenario: Режиссёр показан, когда доступен
|
||||
|
||||
- **GIVEN** активный источник — подтверждённый матч, несущий режиссёра
|
||||
- **WHEN** отображается инфо-часть выбранного источника
|
||||
- **THEN** в ней показан режиссёр этого источника
|
||||
- **AND** при отсутствии режиссёра место остаётся пустым (или прочерком), не
|
||||
ломая вёрстку
|
||||
Reference in New Issue
Block a user