metadata: TVDB отдаёт локализованное название и оригинал
- локаль из [general].language применяется при разборе ответа /search, а в запрос не уходит: параметр language у TVDB — фильтр выдачи, а не селектор перевода (ADR-2026-08-07) - Title берётся из блока translations с тотальным фолбэком на primary name, OriginalTitle — из primary name; форма ответа сверена по документации и живым прогоном не подтверждена (docs/research) - неожиданная форма ответа даёт WARN: признак — отсутствие во всей выдаче ключей языка ожидаемого вида, а не неудача разбора блока
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Локаль TVDB читается из ответа поиска, а не передаётся в запрос
|
||||
|
||||
- **Дата:** 2026-08-07
|
||||
- **Источник:** [openspec/changes/archive/2026-08-07-tvdb-title-locale/design.md](../../openspec/changes/archive/2026-08-07-tvdb-title-locale/design.md),
|
||||
решение 1 и решение 1a
|
||||
|
||||
## Контекст
|
||||
|
||||
Глобальная настройка `[general].language` правит промпт LLM и клиент TMDB
|
||||
([ADR решения 2](../../openspec/changes/archive/2026-07-24-content-language-switch/design.md)),
|
||||
но до клиента TVDB не доезжала. TVDB отдавал primary name — название на языке
|
||||
оригинала, — и оно попадало в карточку ревью и в имя папки Jellyfin как есть.
|
||||
|
||||
Очевидный подход, записанный прямо в постановке задачи и в её критерии приёмки:
|
||||
добавить параметр языка в запрос `/search`, как это сделано для TMDB. От него
|
||||
отказались.
|
||||
|
||||
## Решение
|
||||
|
||||
**Параметр языка в запрос поиска TVDB не передаётся. Локаль применяется только
|
||||
при разборе ответа: `Candidate.Title` берётся из блока переводов, `OriginalTitle`
|
||||
— из primary name.**
|
||||
|
||||
Цитата решения 1 архивного `design.md`:
|
||||
|
||||
> По [swagger TVDB v4, версия 4.7.10] у `/search` есть параметр `language` с
|
||||
> описанием «Restrict results to a specific primary language. Should include the
|
||||
> 3 character language code» — это **фильтр выдачи**, а не селектор перевода.
|
||||
> Передача `language=rus` отсекла бы записи, основной язык которых не русский,
|
||||
> то есть ровно наблюдаемый случай (`Ne Zha`, основной язык `zho`). Сужение
|
||||
> выдачи — это изменение входа гейта матча, а задача такое явно запретила.
|
||||
|
||||
Тем самым два провайдера намеренно устроены по-разному: у TMDB локаль едет в
|
||||
запрос, у TVDB читается из ответа. Асимметрия оставлена в клиентах, а не поднята
|
||||
в общий тип: у TMDB карты названий в ответе нет вовсе, и общий тип пришлось бы
|
||||
заполнять единственным ключом (решение 1a, форма B).
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
- **Слать `language` и мириться с сужением выдачи.** Ломает основной сценарий:
|
||||
иноязычные записи, ради которых задача заводилась, пропадут из поиска.
|
||||
- **Отдельный запрос `/movies/{id}/translations/{lang}` на каждого кандидата.**
|
||||
Цена в лимитах ключа не окупает косметическое поле.
|
||||
- **Заголовок `Accept-Language`.** Для v4 не документирован — была бы догадка.
|
||||
- **`Candidate` несёт карту названий, выбор делает потребитель** (форма B). Язык
|
||||
у потребителя уже есть, плюмбинг не нужен, но у TMDB карты в ответе нет —
|
||||
внутри одного доменного типа завелись бы две формы.
|
||||
- **TVDB не локализуется вовсе, заполняется только `OriginalTitle`** (форма C).
|
||||
Тогда наблюдаемый случай чинится только при включённом TMDB, давшем матч, —
|
||||
поведение молча зависело бы от набора включённых провайдеров.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Критерий приёмки задачи «запрос поиска содержит параметр языка» выполнен быть
|
||||
не может и отменён этим решением. Расхождение вынесено вопросом человеку —
|
||||
разведка [tvdb-search-response-live-check](../tasks/items/tvdb-search-response-live-check.md).
|
||||
- **Решение опирается на документацию, а не на замер.** Семантика параметра и
|
||||
форма блока переводов живым API не подтверждены —
|
||||
[research/tvdb-search-translations.md](../research/tvdb-search-translations.md).
|
||||
Если ручной прогон под ключом покажет иное, эта запись пересматривается новой,
|
||||
а не правится.
|
||||
- Заполнение `OriginalTitle` дало кандидату TVDB две оси сравнения вместо одной.
|
||||
Логика гейта не менялась, но его вход изменился в обе стороны: запись, которую
|
||||
отсекал иероглифический primary name, теперь может пройти по переводу, а две
|
||||
разные записи могут совпасть с планом разными названиями и увести задачу в
|
||||
review. Инвариант «авто-раскладка только при подтверждённом матче» не двигается.
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-07 | [Локаль TVDB читается из ответа поиска, а не передаётся в запрос](ADR-2026-08-07-tvdb-locale-reads-response.md) | — |
|
||||
| 2026-08-06 | [Спека следует за кодом, когда гарантия недостижима, а окно узкое](ADR-2026-08-06-spec-follows-code-on-narrow-window.md) | — |
|
||||
| 2026-08-04 | [Конвейер ревью и пайплайн задачи переезжают в плагины](ADR-2026-08-04-review-pipeline-to-plugin.md) | — |
|
||||
| 2026-07-24 | [Локальная сборка образа + доставка docker save/load](ADR-2026-07-24-local-image-build.md) | — |
|
||||
|
||||
@@ -35,3 +35,7 @@ LLM-эндпоинта на живых раздачах. Автоматичес
|
||||
- [torrent-bencode-limits.md](torrent-bencode-limits.md) — границы разбора
|
||||
`.torrent` в `anacrolix/torrent`: аллокация по объявленной длине строки,
|
||||
паники разбора, отсутствие «имени-заглушки». Проверено на `v1.61.0`.
|
||||
- [tvdb-search-translations.md](tvdb-search-translations.md) — переводы в ответе
|
||||
поиска TheTVDB v4: карта `translations`, параметр `language` как фильтр
|
||||
выдачи, вырожденные значения. Сверено по swagger `4.7.10`, **живым прогоном
|
||||
не подтверждено**.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Переводы в ответе поиска TheTVDB v4
|
||||
|
||||
Клиент TVDB (`internal/metadata/tvdb.go`) берёт локализованное название кандидата
|
||||
из ответа `/search`. Здесь записано, откуда взята форма этого ответа и чего в ней
|
||||
не подтверждено.
|
||||
|
||||
**Провенанс — и он слабый.** Всё ниже сверено **по публичной документации**
|
||||
TheTVDB API v4, файл `docs/swagger.yml` репозитория `thetvdb/v4-api`, поле
|
||||
`info.version` = `4.7.10` (прочитано 2026-08-07). **Живым прогоном не
|
||||
подтверждено ни одно наблюдение**: `CLAUDE.md` → «Запреты» запрещает ходить в
|
||||
боевые метабазы из отладочных прогонов и расходовать лимиты ключа. Всё
|
||||
дальнейшее — **условие, а не замер**. Оракул, который это закроет, написан и ждёт
|
||||
человека:
|
||||
|
||||
```
|
||||
TVDB_API_KEY=… go test ./internal/metadata/ -run Integration -v
|
||||
```
|
||||
|
||||
Он печатает `Title` и `OriginalTitle` первых кандидатов.
|
||||
|
||||
## Параметр `language` у `/search` — фильтр, а не селектор перевода
|
||||
|
||||
Дословно из swagger, параметр `language` эндпоинта `/search`:
|
||||
|
||||
> Restrict results to a specific primary language. Should include the 3 character
|
||||
> language code.
|
||||
|
||||
То есть он **сужает выдачу** по основному языку записи, а не выбирает, на каком
|
||||
языке вернуть название. Форум TheTVDB подтверждает направление: маршруты
|
||||
`/search` не возвращают записи, которых нет на указанном языке.
|
||||
|
||||
Следствие для нас: передача `language=rus` отсекла бы ровно те записи, ради
|
||||
которых заводилась задача, — у `Ne Zha` основной язык `zho`. Поэтому запрос
|
||||
поиска параметром языка **не параметризуется**, а локаль работает только на
|
||||
стороне разбора ответа. Это заказано спекой (`openspec/specs/metadata-match/`,
|
||||
требование «Локализованное название кандидата TVDB»).
|
||||
|
||||
Расхождение с TMDB намеренное: у TMDB `language` — именно селектор
|
||||
локализованного поля, и там он в запрос уходит.
|
||||
|
||||
## Поля `SearchResult`, относящиеся к названию
|
||||
|
||||
Из схемы `SearchResult` того же swagger:
|
||||
|
||||
| Поле | Тип по схеме | Что берём |
|
||||
|---|---|---|
|
||||
| `name` | `string` | primary name записи — идёт в `Candidate.OriginalTitle` и служит фолбэком для `Title` |
|
||||
| `translations` | `TranslationSimple` | карта «код языка → название»; из неё берём `Candidate.Title` |
|
||||
| `name_translated` | `string` | **не используем** |
|
||||
| `overviews`, `overview_translated` | описания | не используем |
|
||||
| `primary_language` | `string` | не используем |
|
||||
| `translationsWithLang` | массив строк | не используем |
|
||||
|
||||
`TranslationSimple` в самом файле swagger описан как открытая карта (свободные
|
||||
ключи со строковыми значениями); **полного текста этой схемы вычитать не
|
||||
удалось** — документ в местах чтения обрывался. Форма «карта кода языка в
|
||||
строку» принята по описанию поля и по обсуждениям в трекере `thetvdb/v4-api`,
|
||||
где встречаются фрагменты вида `"translations": {"eng": "…"}`. Это самое слабое
|
||||
место записки: если реальная форма иная (список объектов, двухбуквенные ключи),
|
||||
разбор молча уйдёт в фолбэк.
|
||||
|
||||
**Почему не `name_translated`.** Семантика поля в документации не описана
|
||||
вовсе — не сказано ни на каком языке оно приходит, ни от чего зависит.
|
||||
Правдоподобно, что заполняет его поисковый индекс при заданном фильтре
|
||||
`language`, которого мы не шлём. Взять его в фолбэк значило бы получить название
|
||||
на неизвестном языке молча; карта `translations` самодостаточна.
|
||||
|
||||
## Что известно про вырожденные значения
|
||||
|
||||
В трекере `thetvdb/v4-api` есть подтверждённый случай, когда `name` приезжает
|
||||
**пустой строкой** при непустом блоке переводов (issue про `"name":"" must not be
|
||||
empty`). Отсюда два следствия для разбора, оба заказаны спекой:
|
||||
|
||||
- пустая строка в этих полях реальна, поэтому пустота значения перевода
|
||||
проверяется после обрезки пробелов;
|
||||
- блок переводов может нести ключ с пустым значением — это не «перевод есть».
|
||||
|
||||
## Как мы защищаемся от того, что запись неверна
|
||||
|
||||
Наблюдение не подтверждено, поэтому разбор устроен так, чтобы ошибка записки
|
||||
стоила как можно меньше:
|
||||
|
||||
- блок переводов разбирается **отдельно от остального ответа** и его негодная
|
||||
форма гасится в фолбэк: косметическое поле не получает права уронить выдачу
|
||||
поиска целиком;
|
||||
- ключ ищется регистронезависимо;
|
||||
- если в выдаче не разобрался **ни один** блок переводов, клиент пишет строку
|
||||
DEBUG. Это единственный сигнал, отличающий «форма ответа не та, что здесь
|
||||
записана» от штатного «перевода на этот язык нет»: без него неверное
|
||||
предположение жило бы в бою неограниченно долго при зелёном гейте.
|
||||
|
||||
## Условие пересмотра
|
||||
|
||||
Записка протухает от смены версии API TheTVDB (сегодня v4, swagger 4.7.10) и от
|
||||
любого ручного прогона интеграционного теста: первый же живой ответ обязан
|
||||
заменить здесь предположения на наблюдения, а слова «живым прогоном не
|
||||
подтверждено» — на дату и результат прогона.
|
||||
@@ -12,6 +12,18 @@ Go-сервиса и что здесь уже проскакивало. Устр
|
||||
[CLAUDE.md](../CLAUDE.md) → «Гейт». Вопрос про уже проверенное вытесняет вопрос
|
||||
про непроверенное — места в прогоне столько же.
|
||||
|
||||
**В worktree гейт краснеет ложно, и это не находка.** Ветки задач живут в
|
||||
`tmp/wt-<задача>` — внутри самого репозитория. Два следствия, оба наблюдались:
|
||||
|
||||
- кэш `golangci-lint` переживает смену каталога и отдаёт результаты прошлого
|
||||
прогона из **основного** дерева. Признак — пути в `tmp/gate/lint.log`
|
||||
начинаются с `../../internal/`, то есть указывают наружу worktree, и жалобы
|
||||
приходят на файлы, которых дифф не касался. Лечится
|
||||
`golangci-lint cache clean` перед прогоном;
|
||||
- пробы проходов ревью, оставленные в `tmp/`, линтуются вместе с проектом:
|
||||
`.go`-файл со `fmt.Printf` в `tmp/` краснит шаг `lint` через `forbidigo`.
|
||||
Проход обязан за собой убирать, а оркестратор — сверять `tmp/` перед гейтом.
|
||||
|
||||
**Severity не выводится проходом заново.** Она стоит рядом с формулировкой
|
||||
инварианта в [CLAUDE.md](../CLAUDE.md) → «Инварианты», обратимость — там же в
|
||||
«Работа» → «Необратимое». Шкала ущерба берётся оттуда, а порядок ценностей —
|
||||
|
||||
Reference in New Issue
Block a user