Слияние: режиссёр в карточке загрузки (T2)
# Conflicts: # docs/backlog/README.md
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
## Context
|
||||
|
||||
`display_name` и поля блоков «Распознано как» / «инфо источника» — косметика
|
||||
отображения, не влияющая на пути и раскладку. Слоистое разрешение полей ярлыка
|
||||
(первый непустой слой `override` → `recognition`+матч → **контекст**) уже
|
||||
реализовано, но заперто в неэкспортируемом `worker.effectiveDisplayName`
|
||||
(`internal/worker/review.go:1145`), который:
|
||||
|
||||
1. читает `naming.Fields` из `download.parsed_context` (нижний слой «контекст»);
|
||||
2. для каждого поля берёт первый непустой слой: `plan.Title→ctxf.Title`,
|
||||
`plan.Director→ctxf.Director`, `plan.Year→ctxf.Year`, сводка сезонов
|
||||
`recognize.SeasonSummary(plan)→ctxf.SeasonLabel()`;
|
||||
3. рендерит строку `naming.Label(...)`.
|
||||
|
||||
Билдеры вью строят режиссёра **в обход** этой логики — прямо `rd.Plan.Director`
|
||||
(`internal/httpapi/review.go:122`), а на странице загрузки
|
||||
(`internal/httpapi/download.go`) поле вообще захардкожено прочерком в шаблоне.
|
||||
Итог — слой контекста теряется, и в заголовке страницы режиссёр есть, а в поле —
|
||||
прочерк.
|
||||
|
||||
Constraints (инварианты): вывод имени НИКОГДА не валит приём (деградация к
|
||||
пустому); `parsed_context` недоверен (битый JSON → пустой слой, best-effort);
|
||||
`director` — недоверенное косметическое поле, чистится на рендере (`naming.Label`
|
||||
уже санитизирует); без сети и без изменения схемы БД.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Один источник истины для слоистого разрешения полей ярлыка — экспортируемая
|
||||
функция в `naming`, переиспользуемая в `effectiveDisplayName` и в обоих
|
||||
билдерах вью.
|
||||
- Режиссёр на `/download/{id}` согласован с режиссёром в заголовке страницы.
|
||||
- Поведение `display_name` не меняется (побайтно тот же результат) — существующие
|
||||
тесты `worker`/`naming` зелёные.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не меняем слои Title/Year/Season в блоке «Распознано как»: они остаются из
|
||||
плана (что распознано), как сейчас — расширение слоёв контекста на эти поля вне
|
||||
scope (это изменило бы отображение блока). Правится только режиссёр — поле,
|
||||
которое даже в норме приходит не из LLM, а из матча/override/контекста.
|
||||
- Не трогаем схему БД, метабазы, распознавание, раскладку.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Сигнатура: `naming.EffectiveFields(parsedContext string, plan recognize.Plan)`
|
||||
|
||||
Функция принимает **строку `parsed_context`**, а не `store.Download`. Так `naming`
|
||||
не тянет зависимость на `store` (пакет `naming` — низкоуровневый вывод имени; уже
|
||||
знает про `parsed_context` концептуально — его схема это `naming.Fields`). Импорт
|
||||
`naming`→`recognize` добавляется (нужен `recognize.Plan` и
|
||||
`recognize.SeasonSummary`); цикла нет — `recognize` не импортирует `naming`
|
||||
(проверено).
|
||||
|
||||
Возвращает не строку, а тип полей:
|
||||
|
||||
```go
|
||||
// LabelFields — эффективные скалярные поля ярлыка после слоистого разрешения.
|
||||
// Значения уже очищены (sanitize): управляющие символы вырезаны, пробелы
|
||||
// схлопнуты — те же, что попадут внутрь Label. Season — готовая строка-сводка
|
||||
// сезонов (для фильма пусто).
|
||||
type LabelFields struct {
|
||||
Title string
|
||||
Director string
|
||||
Year int
|
||||
Season string
|
||||
}
|
||||
|
||||
func (f LabelFields) Label() string { return Label(f.Title, f.Director, f.Year, f.Season) }
|
||||
|
||||
func EffectiveFields(parsedContext string, plan recognize.Plan) LabelFields
|
||||
```
|
||||
|
||||
`Season` — строка, а не `*int`: сводка плана многосезонна («Сезоны 1–3»,
|
||||
«Спецвыпуски»), в `*int` не выражается. Метод `Label()` даёт единый рендер, чтобы
|
||||
`effectiveDisplayName` остался тонкой обёрткой.
|
||||
|
||||
**Санитайзинг на возврате.** Выбор слоя (первый непустой) идёт по **сырым**
|
||||
значениям (`plan.Director != ""` и т.д. — как в исходном `effectiveDisplayName`),
|
||||
но выбранное значение возвращается уже прогнанным через `naming.sanitize` (в
|
||||
пакете `naming` он доступен). Причина — билдеры вью печатают `.Director` напрямую
|
||||
(`{{.Director}}`), в обход `Label`; без очистки на возврате (а) недоверенный
|
||||
`parsed_context.director` дошёл бы до страницы без санитайзинга, (б) режиссёр из
|
||||
одних управляющих символов дал бы `{{if .Director}}` истинным в поле, тогда как
|
||||
`Label` его выбросил бы из заголовка — то самое рассогласование «в поле есть, в
|
||||
шапке нет». `sanitize` идемпотентен, поэтому повторная очистка внутри `Label`
|
||||
ничего не меняет: `Label()` даёт **побайтно** прежний `display_name` (тесты
|
||||
`worker`/`naming` зелёные).
|
||||
|
||||
Альтернатива (вариант A из беклога — присвоить `view.Director = rd.Plan.Director`
|
||||
на странице загрузки) отклонена: оставляет ровно тот баг (нет слоя контекста →
|
||||
нестыковка с заголовком) и не чинит `review.go`.
|
||||
|
||||
### D2. `worker.effectiveDisplayName` — тонкая обёртка
|
||||
|
||||
```go
|
||||
func effectiveDisplayName(d store.Download, plan recognize.Plan) string {
|
||||
return naming.EffectiveFields(d.ParsedContext, plan).Label()
|
||||
}
|
||||
```
|
||||
|
||||
Логика перенесена дословно, поэтому результат идентичен. Обёртку и её тесты
|
||||
(`displayname_test.go`) сохраняем — они продолжают проверять инвариант «имя не
|
||||
изменилось».
|
||||
|
||||
### D3. Билдеры вью читают `.Director`
|
||||
|
||||
- `download.go`: новое поле `downloadDetailView.Director`; в `buildDownloadView`
|
||||
(внутри `if rd.Recognition != nil`) —
|
||||
`view.Director = naming.EffectiveFields(d.ParsedContext, rd.Plan).Director`.
|
||||
- `review.go`: `view.Director = naming.EffectiveFields(rd.Download.ParsedContext, rd.Plan).Director`
|
||||
вместо `rd.Plan.Director`.
|
||||
|
||||
Только `Director` берётся из слоистого разрешения; Title/Year/Season в блоке
|
||||
остаются как есть (см. Non-Goals).
|
||||
|
||||
### D4. Шаблон
|
||||
|
||||
`download_main.html:40` — прочерк заменяется на условный вывод по конвенции
|
||||
соседних полей: `{{if .Director}}{{.Director}}{{else}}<span class="faint">—</span>{{end}}`.
|
||||
Деградация без JS сохраняется (страница server-rendered, поле статично).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Импорт `naming`→`recognize` расширяет зависимости пакета `naming`. Приемлемо:
|
||||
цикла нет, `recognize` — доменный тип плана, а `naming` уже оперирует его
|
||||
сводкой сезонов косвенно (через worker). Альтернатива — дублировать логику
|
||||
разрешения в трёх местах — хуже (дрейф).
|
||||
- Незначительный: `EffectiveFields` парсит `parsed_context` на каждый рендер
|
||||
страницы. Это дешёвый `json.Unmarshal` короткой строки, страница и так
|
||||
server-rendered без БД на рендере блока — некритично.
|
||||
@@ -0,0 +1,60 @@
|
||||
## Why
|
||||
|
||||
На странице просмотра загрузки `/download/{id}` в блоке «Распознано как» поле
|
||||
«Режиссёр» всегда показано прочерком — оно захардкожено
|
||||
(`web/templates/partials/download_main.html:40`), в отличие от экрана ревью, где
|
||||
режиссёр уже выводится. Данные есть: `Plan.Director` персистится в
|
||||
`recognition.plan`, а страница получает эффективный план через
|
||||
`Reviewer.ReviewData`. Возникает видимая нестыковка: в заголовке страницы
|
||||
(`display_name`) режиссёр из контекста уже присутствует, а в поле блока — прочерк.
|
||||
|
||||
Причина глубже одного шаблона: полное **слоистое разрешение** полей ярлыка
|
||||
(override → распознавание+матч → **контекст**) заперто в неэкспортируемом
|
||||
`worker.effectiveDisplayName`, который отдаёт готовую строку, а не поля. Поэтому
|
||||
билдеры вью на странице загрузки и на экране ревью видят только слой
|
||||
«распознавание+матч» (`rd.Plan.Director`) без нижнего слоя контекста — та же дыра
|
||||
в `internal/httpapi/review.go`.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Слоистое разрешение полей ярлыка выносится в экспортируемую функцию**
|
||||
`naming.EffectiveFields(parsedContext, plan)`, возвращающую эффективные поля
|
||||
(в т.ч. `Director`), а не готовую строку. Логика — та же, что была в
|
||||
`effectiveDisplayName`: первый непустой слой `override`→`recognition`(+матч),
|
||||
уже свёрнутый в план, с фолбэком на извлечённый из контекста слой
|
||||
(`parsed_context`).
|
||||
- **`worker.effectiveDisplayName` переиспользует новую функцию** (тонкая обёртка
|
||||
`naming.EffectiveFields(...).Label()`) — поведение `display_name` не меняется,
|
||||
существующие тесты остаются зелёными.
|
||||
- **Страница загрузки** выводит режиссёра из слоистого разрешения — согласованно
|
||||
с режиссёром в заголовке (`display_name`); при отсутствии во всех слоях —
|
||||
прочерк.
|
||||
- **Экран ревью** берёт режиссёра из той же функции — закрывается та же дыра
|
||||
(ранее только `rd.Plan.Director`, без слоя контекста).
|
||||
|
||||
Всё перечисленное — косметика отображения: не влияет на пути файлов,
|
||||
распознавание или раскладку.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(нет — правка отображения в существующих capability)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `web-ui`: блок «Распознано как» на `/download/{id}` показывает режиссёра
|
||||
эффективного источника (слоистое разрешение), согласованно с заголовком.
|
||||
- `review`: инфо-часть выбранного источника показывает режиссёра из слоистого
|
||||
разрешения (с фолбэком на сохранённый контекст), а не только из плана.
|
||||
|
||||
## Impact
|
||||
|
||||
- Код: `internal/naming` (экспорт `EffectiveFields` + тип полей с методом
|
||||
`Label`), `internal/worker` (`effectiveDisplayName` делегирует в naming),
|
||||
`internal/httpapi` (`download.go`, `review.go` — режиссёр из слоистого
|
||||
разрешения), `web/templates/partials/download_main.html` (поле «Режиссёр»
|
||||
вместо прочерка).
|
||||
- БД: изменений схемы нет.
|
||||
- Внешние вызовы: нет.
|
||||
- Беклог: закрывается `docs/backlog/rezhisser-v-kartochke-zagruzki.md`.
|
||||
@@ -0,0 +1,49 @@
|
||||
# review Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Инфо и предпросмотр выбранного источника
|
||||
|
||||
В едином блоке выбора источника экран ревью SHALL показывать для **выбранного
|
||||
(активного)** источника две части: **инфо** — тип (read-only, movie/series),
|
||||
название, оригинальное название, год, режиссёра эффективного источника,
|
||||
разрешённого слоями (`override`/подтверждённый матч+кандидат → сохранённый при
|
||||
приёме контекст раздачи, `parsed_context`; когда режиссёр недоступен ни в одном
|
||||
слое — пусто/прочерк, не ломая вёрстку), для сериала — сводку сезонов (один
|
||||
сезон, диапазон/список для многосезонного пака или «Спецвыпуски»); и
|
||||
**предпросмотр раскладки** — целевые пути хардлинков этого источника. Обе части
|
||||
SHALL относиться именно к активному источнику и SHALL обновляться при смене
|
||||
выбора. Отрисовка блока (показ инфо и предпросмотра) MUST NOT создавать
|
||||
хардлинки: раскладка создаётся только явным действием «Применить». Совпадение
|
||||
целевых путей предпросмотра с результатом применения регулируется требованием
|
||||
«Превью раскладки через единую логику именования» (`web-ui`).
|
||||
|
||||
#### Scenario: Инфо и предпросмотр относятся к активному источнику
|
||||
|
||||
- **GIVEN** в списке активен кандидат метабазы
|
||||
- **WHEN** пользователь смотрит инфо-часть и предпросмотр раскладки
|
||||
- **THEN** показаны тип, название, ориг. название, год (и сводка сезонов для
|
||||
сериала) именно этого источника и предпросмотр его целевых путей
|
||||
|
||||
#### Scenario: Просмотр блока не создаёт раскладку
|
||||
|
||||
- **GIVEN** экран ревью с показанным блоком выбора источника
|
||||
- **WHEN** пользователь только просматривает инфо и предпросмотр, не нажимая
|
||||
«Применить»
|
||||
- **THEN** хардлинки не создаются, файлы под `paths.movies`/`series` не
|
||||
меняются
|
||||
|
||||
#### Scenario: Режиссёр показан, когда доступен
|
||||
|
||||
- **GIVEN** активный источник — подтверждённый матч, несущий режиссёра
|
||||
- **WHEN** отображается инфо-часть выбранного источника
|
||||
- **THEN** в ней показан режиссёр этого источника
|
||||
- **AND** при отсутствии режиссёра во всех слоях место остаётся пустым (или
|
||||
прочерком), не ломая вёрстку
|
||||
|
||||
#### Scenario: Режиссёр берётся из контекста, когда матч его не даёт
|
||||
|
||||
- **GIVEN** активный источник без режиссёра в плане, но с режиссёром в
|
||||
сохранённом контексте (`parsed_context`)
|
||||
- **WHEN** отображается инфо-часть выбранного источника
|
||||
- **THEN** в ней показан режиссёр из контекста (нижний слой разрешения)
|
||||
@@ -0,0 +1,40 @@
|
||||
# web-ui Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Режиссёр в блоке распознавания страницы загрузки
|
||||
|
||||
Страница просмотра `/download/{id}` в блоке «Распознано как» SHALL показывать
|
||||
режиссёра эффективного источника, разрешённого теми же слоями, что и
|
||||
отображаемое имя раздачи (`display_name`): первый непустой слой `override` →
|
||||
`recognition`+матч → сохранённый при приёме контекст (`parsed_context`).
|
||||
Разрешение режиссёра для поля блока и для отображаемого имени SHALL идти **единой
|
||||
логикой** (общий источник разрешения), а не расходящимися путями — прежняя
|
||||
захардкоженная в поле заглушка-прочерк при непустом режиссёре в заголовке
|
||||
устраняется. Показанное значение SHALL проходить ту же очистку (санитайзинг
|
||||
управляющих символов/пробелов), что и режиссёр внутри отображаемого имени, чтобы
|
||||
присутствие/отсутствие режиссёра в поле и в заголовке определялось одинаково.
|
||||
Когда режиссёр недоступен ни в одном слое, поле SHALL показывать прочерк, не
|
||||
ломая вёрстку.
|
||||
|
||||
#### Scenario: Режиссёр из распознавания показан в блоке
|
||||
|
||||
- **GIVEN** загрузка, чей эффективный план несёт режиссёра (из матча метабазы или
|
||||
закреплённого источника)
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** в блоке «Распознано как» в поле «Режиссёр» показан этот режиссёр
|
||||
|
||||
#### Scenario: Режиссёр из контекста при распознавании без матча
|
||||
|
||||
- **GIVEN** загрузка без режиссёра в плане, но с режиссёром в сохранённом
|
||||
контексте (`parsed_context`)
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** в поле «Режиссёр» показан режиссёр из контекста
|
||||
- **AND** он разрешён тем же нижним слоем контекста, что и режиссёр в
|
||||
отображаемом имени раздачи (единая логика, не расходящиеся пути)
|
||||
|
||||
#### Scenario: Режиссёр неизвестен — прочерк
|
||||
|
||||
- **GIVEN** загрузка, для которой режиссёр не разрешается ни одним слоем
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** поле «Режиссёр» показывает прочерк, а вёрстка блока не ломается
|
||||
@@ -0,0 +1,37 @@
|
||||
## 1. Экспортируемое слоистое разрешение (naming)
|
||||
|
||||
- [x] 1.1 `internal/naming`: тип `LabelFields` (Title/Director/Year/Season string)
|
||||
с методом `Label()`, зовущим существующий `Label(...)`
|
||||
- [x] 1.2 `internal/naming`: функция `EffectiveFields(parsedContext string, plan
|
||||
recognize.Plan) LabelFields` — перенести логику слоёв из
|
||||
`worker.effectiveDisplayName` (план → контекст; сводка сезонов плана →
|
||||
контекстный `SeasonLabel`); выбор слоя по сырым значениям, выбранные скаляры
|
||||
на возврате прогнать через `sanitize` (идемпотентно — `Label()` даёт прежний
|
||||
результат); битый `parsed_context` → пустой слой
|
||||
- [x] 1.3 Юнит-тесты `naming.EffectiveFields`: режиссёр/год/сезон из плана;
|
||||
фолбэк на контекст; матч бьёт контекст; битый `parsed_context`; пустые
|
||||
значения; режиссёр из управляющих символов/лишних пробелов → `.Director`
|
||||
очищен и `.Label()` совпадает с прежним `display_name`
|
||||
|
||||
## 2. Переиспользование (worker)
|
||||
|
||||
- [x] 2.1 `worker.effectiveDisplayName` → тонкая обёртка
|
||||
`naming.EffectiveFields(d.ParsedContext, plan).Label()`; поведение и тесты
|
||||
`displayname_test.go` не меняются
|
||||
|
||||
## 3. Вывод режиссёра в вью (httpapi)
|
||||
|
||||
- [x] 3.1 `download.go`: поле `downloadDetailView.Director`; в `buildDownloadView`
|
||||
заполнить из `naming.EffectiveFields(d.ParsedContext, rd.Plan).Director`
|
||||
- [x] 3.2 `review.go`: `view.Director` из `naming.EffectiveFields(rd.Download.
|
||||
ParsedContext, rd.Plan).Director` вместо `rd.Plan.Director`
|
||||
- [x] 3.3 `web/templates/partials/download_main.html`: поле «Режиссёр» —
|
||||
`{{if .Director}}…{{else}}<span class="faint">—</span>{{end}}` вместо прочерка
|
||||
|
||||
## 4. Проверки и закрытие
|
||||
|
||||
- [x] 4.1 (опц.) httpapi-тест: режиссёр присутствует во вью страницы загрузки,
|
||||
если инфраструктура тестов позволяет
|
||||
- [x] 4.2 `task test` и `task lint` зелёные; `openspec validate --strict`
|
||||
- [x] 4.3 Удалить `docs/backlog/rezhisser-v-kartochke-zagruzki.md` и строку в
|
||||
индексе беклога `docs/backlog/README.md`
|
||||
@@ -256,15 +256,17 @@ NOT проваливать команду ревью. Это согласует
|
||||
|
||||
В едином блоке выбора источника экран ревью SHALL показывать для **выбранного
|
||||
(активного)** источника две части: **инфо** — тип (read-only, movie/series),
|
||||
название, оригинальное название, год, режиссёра (из подтверждённого матча/
|
||||
кандидата, когда доступен; иначе пусто/прочерк, не ломая вёрстку), для сериала —
|
||||
сводку сезонов (один сезон, диапазон/список для многосезонного пака или
|
||||
«Спецвыпуски»); и **предпросмотр раскладки** — целевые пути хардлинков этого
|
||||
источника. Обе части SHALL относиться именно к активному источнику и SHALL
|
||||
обновляться при смене выбора. Отрисовка блока (показ инфо и предпросмотра) MUST
|
||||
NOT создавать хардлинки: раскладка создаётся только явным действием «Применить».
|
||||
Совпадение целевых путей предпросмотра с результатом применения регулируется
|
||||
требованием «Превью раскладки через единую логику именования» (`web-ui`).
|
||||
название, оригинальное название, год, режиссёра эффективного источника,
|
||||
разрешённого слоями (`override`/подтверждённый матч+кандидат → сохранённый при
|
||||
приёме контекст раздачи, `parsed_context`; когда режиссёр недоступен ни в одном
|
||||
слое — пусто/прочерк, не ломая вёрстку), для сериала — сводку сезонов (один
|
||||
сезон, диапазон/список для многосезонного пака или «Спецвыпуски»); и
|
||||
**предпросмотр раскладки** — целевые пути хардлинков этого источника. Обе части
|
||||
SHALL относиться именно к активному источнику и SHALL обновляться при смене
|
||||
выбора. Отрисовка блока (показ инфо и предпросмотра) MUST NOT создавать
|
||||
хардлинки: раскладка создаётся только явным действием «Применить». Совпадение
|
||||
целевых путей предпросмотра с результатом применения регулируется требованием
|
||||
«Превью раскладки через единую логику именования» (`web-ui`).
|
||||
|
||||
#### Scenario: Инфо и предпросмотр относятся к активному источнику
|
||||
|
||||
@@ -286,8 +288,15 @@ NOT создавать хардлинки: раскладка создаётся
|
||||
- **GIVEN** активный источник — подтверждённый матч, несущий режиссёра
|
||||
- **WHEN** отображается инфо-часть выбранного источника
|
||||
- **THEN** в ней показан режиссёр этого источника
|
||||
- **AND** при отсутствии режиссёра место остаётся пустым (или прочерком), не
|
||||
ломая вёрстку
|
||||
- **AND** при отсутствии режиссёра во всех слоях место остаётся пустым (или
|
||||
прочерком), не ломая вёрстку
|
||||
|
||||
#### Scenario: Режиссёр берётся из контекста, когда матч его не даёт
|
||||
|
||||
- **GIVEN** активный источник без режиссёра в плане, но с режиссёром в
|
||||
сохранённом контексте (`parsed_context`)
|
||||
- **WHEN** отображается инфо-часть выбранного источника
|
||||
- **THEN** в ней показан режиссёр из контекста (нижний слой разрешения)
|
||||
|
||||
### Requirement: Разделение труда транспортов в ревью
|
||||
|
||||
|
||||
@@ -511,7 +511,6 @@ htmx-поллингом (см. конвенцию веб-UI): по перехо
|
||||
(бейдж, имя, живой прогресс)
|
||||
- **AND** поллинг фазы `catched` завершается
|
||||
|
||||
|
||||
### Requirement: Загрузка .torrent-файла на форме добавления
|
||||
|
||||
Форма добавления загрузки веб-UI SHALL позволять выбрать локальный
|
||||
@@ -541,3 +540,41 @@ htmx-путь (список обновляется/происходит реди
|
||||
- **GIVEN** пользователь оставил файловое поле пустым и ввёл magnet/текст
|
||||
- **WHEN** форма отправлена
|
||||
- **THEN** выполняется приём по тексту источника, как прежде
|
||||
|
||||
### Requirement: Режиссёр в блоке распознавания страницы загрузки
|
||||
|
||||
Страница просмотра `/download/{id}` в блоке «Распознано как» SHALL показывать
|
||||
режиссёра эффективного источника, разрешённого теми же слоями, что и
|
||||
отображаемое имя раздачи (`display_name`): первый непустой слой `override` →
|
||||
`recognition`+матч → сохранённый при приёме контекст (`parsed_context`).
|
||||
Разрешение режиссёра для поля блока и для отображаемого имени SHALL идти **единой
|
||||
логикой** (общий источник разрешения), а не расходящимися путями — прежняя
|
||||
захардкоженная в поле заглушка-прочерк при непустом режиссёре в заголовке
|
||||
устраняется. Показанное значение SHALL проходить ту же очистку (санитайзинг
|
||||
управляющих символов/пробелов), что и режиссёр внутри отображаемого имени, чтобы
|
||||
присутствие/отсутствие режиссёра в поле и в заголовке определялось одинаково.
|
||||
Когда режиссёр недоступен ни в одном слое, поле SHALL показывать прочерк, не
|
||||
ломая вёрстку.
|
||||
|
||||
#### Scenario: Режиссёр из распознавания показан в блоке
|
||||
|
||||
- **GIVEN** загрузка, чей эффективный план несёт режиссёра (из матча метабазы или
|
||||
закреплённого источника)
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** в блоке «Распознано как» в поле «Режиссёр» показан этот режиссёр
|
||||
|
||||
#### Scenario: Режиссёр из контекста при распознавании без матча
|
||||
|
||||
- **GIVEN** загрузка без режиссёра в плане, но с режиссёром в сохранённом
|
||||
контексте (`parsed_context`)
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** в поле «Режиссёр» показан режиссёр из контекста
|
||||
- **AND** он разрешён тем же нижним слоем контекста, что и режиссёр в
|
||||
отображаемом имени раздачи (единая логика, не расходящиеся пути)
|
||||
|
||||
#### Scenario: Режиссёр неизвестен — прочерк
|
||||
|
||||
- **GIVEN** загрузка, для которой режиссёр не разрешается ни одним слоем
|
||||
- **WHEN** клиент открывает `GET /download/{id}`
|
||||
- **THEN** поле «Режиссёр» показывает прочерк, а вёрстка блока не ломается
|
||||
|
||||
|
||||
Reference in New Issue
Block a user