- новое поле [general].language (ru|en, дефолт en) — единый источник языка локализованного title детектора и локали запросов к метабазам; original_title всегда на языке оригинала - локаль TMDB (поиск + credits) выводится из него, [metadata.tmdb].language убран - промпт LLM явно задаёт язык title с fallback на оригинал
20 KiB
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): ru → ru-RU, en → en-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 id269613 - WHEN клиент TVMaze формирует Candidate
- THEN
URL=https://www.tvmaze.com/shows/169 - AND
TagProvider/TagIDостаютсяtvdb/269613(тег папки Jellyfin не меняется)
Scenario: URL сохраняется в БД
- GIVEN результат поиска с кандидатами
- WHEN кандидаты сохраняются в таблицу
metadata_candidate - THEN значение
urlSHALL быть записано в колонкуurl - AND при последующей загрузке данных ревью url доступен без повторной генерации
Requirement: Безгодовой второй проход сверки как fallback
Система SHALL выполнять второй проход сверки без года как fallback: когда поиск с
годом не дал подтверждённого единичного сильного матча, а год плана известен
(year > 0), выполняется тот же перебор названий (в порядке original_title →
title → provider_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