Files
av a2d8140e22 recognition: глобальный переключатель языка вывода [general].language
- новое поле [general].language (ru|en, дефолт en) — единый источник языка
  локализованного title детектора и локали запросов к метабазам; original_title
  всегда на языке оригинала
- локаль TMDB (поиск + credits) выводится из него, [metadata.tmdb].language убран
- промпт LLM явно задаёт язык title с fallback на оригинал
2026-07-24 14:09:04 +03:00

20 KiB
Raw Permalink Blame History

metadata-match Specification

Purpose

Сверка распознанного плана с внешними базами метаданных (TMDB/TVDB/TVMaze): поиск записи по нескольким названиям с нормализацией, локаль запроса, подтверждение единичного сильного матча (официальный provider_id + каноническое имя/год) и сбор кандидатов с URL для ручного выбора в review. Разбор сигналов моделью — в recognition.

Requirements

Requirement: Сверка с базой по нескольким названиям

При сверке плана с включёнными базами метаданных система SHALL искать по нескольким названиям в порядке убывания силы ключа: сначала по original_title, затем по локализованному title, затем по provider_hint. Этот перебор названий SHALL выполняться в рамках каждого прохода сверки — проходы (с годом и безгодовой fallback) определяет требование «Безгодовой второй проход сверки как fallback». Поиск SHALL останавливаться, как только очередной запрос дал единичный сильный матч (ровно один кандидат с совпадением названия и года); ранняя остановка действует в пределах прохода. Запрос с названием, нормализованно совпадающим с уже выполненным, система SHALL пропускать, чтобы не обращаться к базе повторно с тем же ключом.

Кандидаты для ручного выбора в review система SHALL собирать из всех выполненных заходов обоих проходов с дедупликацией по provider:id и общим потолком.

Scenario: Иностранный фильм находится по оригинальному названию

  • GIVEN план с title «Тёмный рыцарь», original_title «The Dark Knight», год 2008
  • WHEN выполняется сверка с базой
  • THEN первый запрос идёт по «The Dark Knight»
  • AND при единичном сильном матче дальнейшие запросы (по title, provider_hint) не выполняются

Scenario: Фолбэк на локализованное название

  • GIVEN план, для которого запрос по original_title не дал единичного сильного матча
  • WHEN продолжается сверка
  • THEN выполняется запрос по локализованному title
  • AND при отсутствии матча и там — запрос по provider_hint

Scenario: Дублирующий запрос пропускается

  • GIVEN план, у которого original_title нормализованно совпадает с title
  • WHEN выполняется сверка
  • THEN база запрашивается этим названием один раз, повторный заход по title не делается

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

Requirement: Локаль запроса к TMDB

Локаль запросов к TMDB SHALL выводиться из глобальной настройки language (ru|en, дефолт en): ruru-RU, enen-US. Отдельной настройки локали у TMDB быть SHALL NOT — глобальный language единственный источник.

Эту локаль система SHALL передавать параметром language как в запрос поиска, так и в запрос credits (режиссёр). На стороне поиска локаль влияет ТОЛЬКО на локализованное поле Title/Name; поле original_title/original_name остаётся на языке оригинала, поэтому оригинальная сторона сравнения не затрагивается. На стороне credits передача локали — best-effort: имена людей провайдер локализует не всегда, при отсутствии перевода имя остаётся на языке оригинала, и это не проваливает выборку режиссёра.

Scenario: Локаль по умолчанию — английская

  • GIVEN TMDB включён, глобальный language не задан в конфиге
  • WHEN выполняется поиск фильма
  • THEN запрос содержит language=en-US
  • AND в кандидате Title приходит на английском, а OriginalTitle — на языке оригинала

Scenario: language=ru даёт русскую локаль

  • GIVEN TMDB включён, глобальный language = ru
  • WHEN выполняется поиск фильма с русской локализацией
  • THEN запрос содержит language=ru-RU
  • AND в кандидате Title приходит на русском, а OriginalTitle — на языке оригинала

Scenario: Локаль передаётся и в запрос режиссёра

  • GIVEN подтверждённый матч TMDB и глобальный language = ru
  • WHEN выполняется запрос credits за режиссёром
  • THEN запрос содержит language=ru-RU
  • AND при отсутствии локализованного имени режиссёр остаётся на языке оригинала, выборка не проваливается

Requirement: Нормализация названий при сравнении

Нормализация названий для гейта сильного матча SHALL сводить букву ё к е, чтобы написания, различающиеся только ё/е, считались одним названием.

Нормализация SHALL дополнительно сворачивать кирилло-латинские homoglyph-двойники по той же курируемой таблице, что применяется при санитайзинге плана (capability recognition), чтобы визуально совпадающие название плана и кандидата базы, различающиеся лишь скриптом отдельных букв, считались одним названием. Это defense-in-depth на случай двойников со стороны кандидата: гейт — точка безопасно-критичного решения об авто-матче и SHALL быть устойчив к двойникам независимо от предшествующего санитайзинга.

Scenario: «Тёмный» и «Темный» совпадают

  • GIVEN план с названием «Тёмный рыцарь» и кандидат базы «Темный рыцарь»
  • WHEN сравниваются нормализованные названия
  • THEN они считаются совпадающими

Scenario: Двойник у кандидата не мешает матчу

  • GIVEN план с латинским названием Harold и кандидат базы, где то же слово содержит кириллический символ-двойник
  • WHEN сравниваются нормализованные названия
  • THEN они считаются совпадающими

Requirement: Кандидат несёт URL для внешней проверки

Каждый кандидат внешней базы метаданных (metadata.Candidate) SHALL нести поле URL — ссылку на страницу элемента (фильма/сериала) на сайте провайдера. URL SHALL формироваться клиентом провайдера при поиске (Search) и сохраняться в таблице metadata_candidate. Отображение этой ссылки на экране ревью — забота review/web-ui, не данного требования.

Формат URL для каждого провайдера:

  • TMDB: https://www.themoviedb.org/movie/{id} (фильм) или https://www.themoviedb.org/tv/{id} (сериал) — тип контента известен из запроса Query.Type
  • TVDB: https://www.thetvdb.com/dereferrer/movie/{id} (фильм) или https://www.thetvdb.com/dereferrer/series/{id} (сериал) — тип контента известен из запроса Query.Type
  • TVMaze: https://www.tvmaze.com/shows/{id} — URL SHALL использовать нативный id TVMaze, а не внешний тег (TVDB/IMDb), чтобы ссылка вела на TVMaze-страницу

Scenario: Кандидат TMDB с корректной ссылкой

  • GIVEN TMDB найден кандидат-фильм с id 603 («Матрица»)
  • WHEN клиент TMDB формирует Candidate
  • THEN URL = https://www.themoviedb.org/movie/603

Scenario: Кандидат TVMaze с нативной ссылкой

  • GIVEN TVMaze найден сериал с id 169 («Фарго»), внешний тег — TVDB id 269613
  • WHEN клиент TVMaze формирует Candidate
  • THEN URL = https://www.tvmaze.com/shows/169
  • AND TagProvider/TagID остаются tvdb/269613 (тег папки Jellyfin не меняется)

Scenario: URL сохраняется в БД

  • GIVEN результат поиска с кандидатами
  • WHEN кандидаты сохраняются в таблицу metadata_candidate
  • THEN значение url SHALL быть записано в колонку url
  • AND при последующей загрузке данных ревью url доступен без повторной генерации

Requirement: Безгодовой второй проход сверки как fallback

Система SHALL выполнять второй проход сверки без года как fallback: когда поиск с годом не дал подтверждённого единичного сильного матча, а год плана известен (year > 0), выполняется тот же перебор названий (в порядке original_titletitleprovider_hint) и провайдеров, но с запросом БЕЗ года. Второй проход SHALL выполняться только как fallback: если первый проход подтвердил матч или год плана неизвестен, второго прохода быть SHALL NOT.

Гейт сильного матча (нормализованное совпадение названия и год кандидата в пределах ±1, ровно один кандидат) при этом SHALL оставаться неизменным — безгодовой проход расширяет только выдачу запроса, но не ослабляет условие подтверждения, поэтому инвариант «авто-раскладка только при подтверждённом единичном сильном матче» не затрагивается. Дополнительно в безгодовом проходе система SHALL требовать известный год кандидата: запись с неизвестным годом (год подтвердить нечем) во втором проходе подтверждённым матчем быть SHALL NOT и уходит кандидатом в review (в первом, с-годом, проходе прежняя leniency к неизвестному году сохраняется). Кандидаты для ручного выбора в review система SHALL собирать из обоих проходов с той же дедупликацией по provider:id и общим потолком.

Scenario: Год плана off-by-one — авто-матч восстанавливается без года

  • GIVEN запись есть в базе, но год плана отличается от её года ровно на 1 (жёсткий exact-year фильтр запроса отсёк её в первом проходе)
  • WHEN первый проход (с годом) не дал сильного матча
  • THEN выполняется второй проход без года
  • AND единичный кандидат проходит гейт (название совпадает, год бьётся ±1) — матч подтверждается

Scenario: Год плана расходится больше чем на 1 — кандидат только в review

  • GIVEN запись есть в базе, но год плана отличается от её года больше чем на 1
  • WHEN второй проход без года возвращает эту запись единственным кандидатом
  • THEN гейт отклоняет её по году (расхождение больше ±1) — подтверждённого матча нет
  • AND кандидат собирается для выбора в review (человек получает кандидата вместо пустого списка)

Scenario: Первый проход подтвердил матч — второго нет

  • GIVEN первый проход (с годом) дал единичный сильный матч
  • WHEN завершается сверка
  • THEN второй (безгодовой) проход не выполняется

Scenario: Год неизвестен — второго прохода нет

  • GIVEN план без года (year = 0)
  • WHEN первый проход не дал матча
  • THEN второй проход не выполняется (он был бы идентичен первому)

Scenario: Кандидат с неизвестным годом в безгодовом проходе — только review

  • GIVEN год плана известен, а pass 1 (с годом) не дал матча
  • WHEN безгодовой проход возвращает единственного кандидата с совпадающим названием, но неизвестным годом
  • THEN подтверждённого матча нет (год кандидата не подтверждён) — авто-раскладка не делается
  • AND кандидат собирается для выбора в review

Scenario: Несколько кандидатов из безгодового прохода — матч не подтверждён

  • GIVEN безгодовой проход вернул более одного кандидата, проходящего гейт
  • WHEN оценивается матч
  • THEN подтверждённого матча нет, кандидаты собираются для выбора в review