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:
av
2026-08-07 15:17:05 +03:00
parent 0c83385098
commit fdbc781197
19 changed files with 1444 additions and 21 deletions
@@ -0,0 +1,78 @@
## Why
Глобальная настройка `[general].language` правит только промпт LLM и клиент TMDB;
до клиента TVDB она не доезжает вовсе. TVDB отдаёт primary name — название на
языке оригинала, — и оно попадает в карточку ревью и в имя папки Jellyfin как
есть: у [movie 131155](https://www.thetvdb.com/movies/131155-) это `哪吒之魔童降世`
вместо «Нэчжа», хотя русский перевод у записи есть. Плюс `Candidate.OriginalTitle`
у TVDB не заполняется вовсе, хотя это ось сравнения при матче: сегодня TVDB
сравнивается только по одному названию из двух возможных.
## What Changes
- `TVDBConfig` получает поле `Language` — абстрактный код (`ru`|`en`) из
`cfg.ContentLanguage()`, как уже сделано для TMDB. Диалект (у TVDB это
трёхбуквенный код ISO 639-2: `rus`/`eng`) выводит сам `internal/metadata/tvdb.go`
тотальным `switch` с default-веткой — по решению 2 архивного
[design content-language-switch](../archive/2026-07-24-content-language-switch/design.md).
- Разбор ответа `/search` дополняется картой `translations` (код языка → название).
`Candidate.Title` берётся из `translations[код]`, при отсутствии перевода —
из primary `name` (молчаливой пустоты быть не должно).
- `Candidate.OriginalTitle` заполняется primary `name`.
- **Параметр `language` в запрос `/search` НЕ добавляется.** По
[swagger TVDB v4 4.7.10](https://raw.githubusercontent.com/thetvdb/v4-api/main/docs/swagger.yml)
этот параметр — фильтр («Restrict results to a specific primary language»), а не
селектор перевода: он сузил бы выдачу и отрезал именно те записи, ради которых
задача заводилась. Разбор ведётся по ответу, а не по запросу. Расхождение с
критерием приёмки задачи вынесено вопросом человеку — см. `## Открытый вопрос`.
- **Форма ответа TVDB живым API не сверялась** — только по публичной документации
(см. `docs/research/tvdb-search-translations.md`). Интеграционный тест за
env-гейтом `TVDB_API_KEY` написан, но не запускался.
Ломающих изменений нет: конфиг не меняется, новых настроек не появляется.
## Capabilities
### New Capabilities
Новых нет.
### Modified Capabilities
- `metadata-match`: требование «Локаль запроса к TMDB» получает соседа —
«Локализованное название кандидата TVDB». Обобщать существующее требование на
всех провайдеров нельзя: механика у TMDB и TVDB разная (параметр запроса против
разбора ответа), и одно требование на двоих скрыло бы эту разницу.
## Impact
- `internal/metadata/tvdb.go``TVDBConfig.Language`, вывод кода языка, разбор
`translations` в `tvdbSearchResp`, заполнение `Title`/`OriginalTitle`.
- `internal/metadata/tvdb_test.go` — стенд `httptest` с блоком `translations`,
табличный тест на перевод/фолбэк/`OriginalTitle`.
- `internal/metadata/integration_test.go` — интеграционный тест за `TVDB_API_KEY`
печатает `Title` и `OriginalTitle`. **Пишется, но не запускается.**
- `cmd/jellybit/serve.go` — сборка провайдера TVDB получает `Language`.
- `internal/config/config.go` — в `validate` живёт единственный перечень
потребителей кода языка («при добавлении кода синхронно расширь мапперы
`metadata.tmdbLocale` и `recognize.languageDirective`»); третий маппер
дописывается в этот перечень.
- `docs/research/tvdb-search-translations.md` — новая записка о форме ответа
`/search` с честным провенансом.
- Поведение матча: `strongMatches` сравнивает план и с `Title`, и с
`OriginalTitle`. У TVDB сегодня `OriginalTitle` пуст, поэтому сравнение идёт по
одному названию; после изменения их станет два (локализованное + оригинальное).
Логика гейта не трогается, но его **вход** меняется, и следствие двустороннее:
запись, которую отсекал иероглифический primary name, теперь может пройти по
переводу (задача уйдёт в авто вместо review) — и наоборот, две разные записи
могут совпасть с планом разными названиями, тогда единичный сильный матч
станет двумя и задача уйдёт в review вместо авто. Второе направление —
в сторону человека, то есть безопасную. Инвариант «авто-раскладка только при
подтверждённом матче» не двигается.
- Внешний контракт: TheTVDB API v4 `/search`. Живых запросов в рамках задачи не
делалось (запрет `CLAUDE.md` → «Запреты»: лимиты метабаз не расходуем).
## Открытый вопрос
Форма ответа `/search` не сверена живым API. Вопрос человеку записан в
`docs/tasks/items/tvdb-title-locale.md` → раздел «Вопросы».