- чистка стоит на каждой точке входа значения метабазы в план — сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение: гарантия, поставленная только на запись, обходится данными, сохранёнными прежними версиями - название, непригодное как имя каталога (пустое или без единой буквы и цифры), не подставляется — раздача уходит в review с названной причиной - гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям провайдера, чистится только копия, уходящая дальше
39 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 применять к нему тот же санитайзинг
человекочитаемых полей, что и к выводу LLM (см. recognition, требование
«Санитайзинг человекочитаемых полей плана»). Подстановка несанитизированного
значения SHALL NOT выполняться: план — источник имени каталога библиотеки, и
значение, не прошедшее чистку, уходит в путь на диске.
Если после санитайзинга каноническое название оказывается непригодным как
компонент пути — пустым либо вырожденным, то есть не содержащим ни одной буквы
и ни одной цифры (., .., -, только пунктуация), — система SHALL сохранить в
плане прежнее название и SHALL NOT разрешать авто-раскладку: причина уходит в
перечень причин решения, раздача попадает в review. Проверка пригодности SHALL
стоять после санитайзинга, а не до него. Ронять распознавание или раскладку
такой матч SHALL NOT — год, провайдер и provider_id при этом подставляются как
обычно.
Санитайзинг, изменивший каноническое название, но оставивший его пригодным, авто-раскладку SHALL NOT блокировать: в план идёт очищенное значение, и оно предсказуемо — ради этого чистка и стоит.
При подтверждённом матче система 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 подтверждённый единичный матч, каноническое название которого содержит zero-width символ, перевод строки или кириллический двойник внутри латинского слова
- WHEN каноническое название подставляется в план
- THEN в плане оказывается санитизированное значение
- AND авто-раскладка остаётся разрешённой, если прочие условия выполнены
- AND когда база имени папки печатается из распознавания (живой папки-якоря
того же тайтла нет — см.
file-layout, «Сходимость базы папки при подтверждённом матче»), в имя каталога уходит очищенное значение
Scenario: Название базы непригодно как компонент пути — раздача уходит в review
- GIVEN подтверждённый единичный матч, каноническое название которого после
санитайзинга пусто (целиком состояло из zero-width символов) либо не содержит ни
одной буквы и ни одной цифры (
.,...,-) - WHEN каноническое название подставляется в план
- THEN название плана остаётся прежним (подстановки не происходит)
- AND решение auto/review содержит причину «название из базы непригодно как имя каталога», авто-раскладка не разрешается
- AND распознавание не проваливается, год и провайдер подставлены
Scenario: Нормальное название матча не меняется
- GIVEN подтверждённый единичный матч с обычным каноническим названием
- WHEN каноническое название подставляется в план
- THEN значение в плане совпадает с тем, что отдала база (санитайзинг на чистом значении ничего не меняет)
- AND авто-раскладка остаётся разрешённой, если прочие условия выполнены
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
Requirement: Локализованное название кандидата TVDB
Кандидат TVDB SHALL нести локализованное название в поле Title и название на
языке оригинала в поле OriginalTitle. Язык локализации задаёт та же глобальная
настройка language, что и для TMDB, — правило единственного источника языка
живёт в требовании «Локаль запроса к TMDB» и здесь не переписывается.
Источник локализованного названия — блок переводов в ответе поиска TVDB (карта
«код языка → название»). OriginalTitle SHALL брать primary name записи — это
название на языке оригинала и ось сравнения при матче.
Фолбэк SHALL быть тотальным: если перевода на нужный язык нет, его значение
пусто после обрезки пробелов или блока переводов нет вовсе, Title SHALL быть
равен primary name. Пустого Title при непустом primary name быть SHALL NOT.
В Title SHALL попадать значение перевода после обрезки пробелов.
Ключ языка SHALL искаться регистронезависимо: молчаливый фолбэк из-за регистра
ключа неотличим от отсутствия перевода и в эксплуатации не диагностируется. Если
условию отвечает несколько ключей, выбор SHALL быть детерминированным — один и
тот же ответ провайдера обязан давать один и тот же Title.
Локаль TVDB SHALL влиять только на разбор ответа и SHALL NOT сужать выдачу
поиска: параметр языка в запрос поиска не передаётся. Причина — в
docs/research/tvdb-search-translations.md; здесь заказано поведение, а не
устройство чужого API. Тем самым разница с TMDB намеренна: у TMDB локаль едет в
запрос, у TVDB читается из ответа.
Негодная форма блока переводов (блок пришёл не картой, значения не строки) SHALL приводить к тому же тотальному фолбэку, а не проваливать разбор ответа поиска целиком: ответ метабазы — недоверенный вход.
Подозрение на иную форму ответа SHALL оставлять диагностический след. Признаком
служит отсутствие во всей выдаче хотя бы одного ключа языка ожидаемого вида,
а не неудача разбора блока: null, пустая карта и словарь кодов другого вида
разбираются без ошибки и потому признаком быть SHALL NOT. Штатное «перевода на
этот язык нет» (ключи ожидаемого вида есть, нужного среди них нет) следа
оставлять SHALL NOT — иначе сигнал неотличим от рутины. Уровень следа задают
конвенции логирования проекта и здесь не нормируются; требуется наблюдаемость,
а не конкретный уровень.
Настоящее требование не изменяет условий подтверждения матча — они заданы
требованиями «Подтверждение матча и каноническое имя» и «Безгодовой второй
проход сверки как fallback». Заполнение OriginalTitle расширяет множество
названий кандидата, по которым идёт сравнение, с одного до двух. Исход гейта при
этом монотонным SHALL NOT считаться: там, где сильный кандидат был один, их
может стать двое, и тогда подтверждённого матча нет, а записи уходят кандидатами
в review — по действующему требованию, без исключений для TVDB.
Scenario: Перевод на язык настройки есть
- GIVEN TVDB включён, глобальный
language=ru - WHEN поиск возвращает запись с primary name
哪吒之魔童降世и переводомrus= «Нэчжа» - THEN
Candidate.Title= «Нэчжа» - AND
Candidate.OriginalTitle=哪吒之魔童降世
Scenario: Перевода на язык настройки нет — фолбэк на оригинал
- GIVEN TVDB включён, глобальный
language=ru - WHEN поиск возвращает запись с primary name
Fargoи переводами без ключаrus - THEN
Candidate.Title=Fargo - AND
Candidate.OriginalTitle=Fargo
Scenario: Перевод есть, но пустой
- GIVEN TVDB включён, глобальный
language=ru - WHEN поиск возвращает запись с primary name
Fargoи переводомrusиз одних пробелов - THEN
Candidate.Title=Fargo - AND
Candidate.OriginalTitle=Fargo
Scenario: Язык по умолчанию — английский
- GIVEN TVDB включён, глобальный
languageне задан в конфиге - WHEN поиск возвращает запись с переводами
engиrus - THEN
Candidate.Titleберётся из переводаeng
Scenario: Ключ перевода в другом регистре
- GIVEN TVDB включён, глобальный
language=ru - WHEN поиск возвращает запись с переводом под ключом
RUSвместоrus - THEN
Candidate.Titleберётся из этого перевода
Scenario: Блок переводов отсутствует или пришёл негодной формой
- GIVEN TVDB включён с любым значением
language - WHEN поиск возвращает записи без блока переводов либо с блоком, который не разбирается картой
- THEN
Candidate.Title= primary name записи - AND
Candidate.OriginalTitle= primary name записи - AND разбор ответа поиска не проваливается, кандидаты возвращаются
- AND остаётся диагностический след о подозрении на иную форму ответа
Scenario: Коды языка не того вида — след остаётся
- GIVEN TVDB включён, глобальный
language=ru - WHEN вся выдача поиска несёт блоки переводов с ключами другого вида (например, двухбуквенными), либо
null, либо пустые - THEN
Candidate.Title= primary name у каждой записи - AND остаётся диагностический след о подозрении на иную форму ответа
Scenario: Перевода на язык нет — следа не остаётся
- GIVEN TVDB включён, глобальный
language=ru - WHEN выдача несёт блоки переводов с ключами ожидаемого вида, но без нужного языка
- THEN
Candidate.Title= primary name у каждой записи - AND диагностического следа не остаётся: это штатный исход, а не подозрение
Scenario: Запрос поиска не сужается языком
- GIVEN TVDB включён, глобальный
language=ru - WHEN выполняется поиск
- THEN строка запроса поиска не содержит параметра языка
- AND строка запроса совпадает с той, что уходит при
language=en
Scenario: Перевод сделал сильных кандидатов двумя — матч не подтверждён
- GIVEN план с
title«Нэчжа» иoriginal_titleNe Zha, год известен - WHEN поиск TVDB возвращает две записи в пределах года ±1: одну с primary
name
哪吒之魔童降世и переводомrus«Нэчжа», другую с primary nameNe Zha - THEN гейт даёт двух сильных кандидатов вместо одного
- AND подтверждённого матча нет, обе записи уходят кандидатами в review
Requirement: Санитайзинг названий кандидатов, уходящих в ревью
Система SHALL санитизировать названия кандидата (Title, OriginalTitle) тем же
санитайзингом человекочитаемых полей плана (см. recognition) перед тем, как
унести их из сверки дальше: в список кандидатов для ревью, в хранилище, на экран
и в закрепляемое человеком значение. Причина та же, что у
канонического названия: выбор кандидата человеком — полноправный путь
подтверждения матча, и закреплённое им название становится именем каталога
библиотеки в тех же условиях, что и название авто-матча.
Условия подтверждения сильного матча система SHALL проверять на значениях, как их отдал провайдер: санитайзинг кандидатов SHALL NOT влиять на эти значения. Порядок операций требование не нормирует — нормирует исход: гейт матча этой правкой не двигается, иначе кандидат, чьё название отличается от плана невидимым символом, начал бы совпадать там, где прежде уходил в review. Полнота списка кандидатов тоже SHALL остаться прежней.
Если название кандидата после санитайзинга непригодно как компонент пути (пусто
либо без единой буквы и цифры), система SHALL сохранить кандидата в списке — он
остаётся выбором человека и несёт provider_id и URL для внешней проверки, — но
закрепление такого названия SHALL приводить к тому же исходу, что и у
канонического: подстановки не происходит, в плане остаётся название
распознавания.
OriginalTitle кандидата чистится наравне с Title, хотя в хранилище и на экран
сегодня доходит только второе: первое уезжает в результат распознавания и в
диагностику сухого прогона, и держать в одной структуре одно чистое поле и одно
грязное — источник будущей ошибки.
Scenario: Название кандидата чистится перед показом и закреплением
- GIVEN провайдер вернул кандидата, название которого содержит zero-width символ и кириллический двойник внутри латинского слова
- WHEN кандидат попадает в список для ревью
- THEN его название очищено тем же санитайзингом, что и поля плана
- AND человек, выбравший этого кандидата, закрепляет очищенное название
- AND в имя каталога библиотеки оно уходит в тех же условиях, что и название
авто-матча (живой папки-якоря того же тайтла нет — см.
file-layout, «Сходимость базы папки при подтверждённом матче»)
Scenario: Сравнение с планом идёт по значению провайдера
- GIVEN кандидат, название которого отличается от названия плана только невидимым символом внутри слова
- WHEN проверяются условия подтверждения сильного матча
- THEN сравнение идёт по значению, как его отдал провайдер
- AND исход подтверждения матча тот же, что был до этого изменения