Единый источник полей отображаемого имени и один рендер полного ярлыка на всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный «Название (режиссёр, год). Сезон N» но выбрасывал структуру, а перелив по распознаванию — усечённый «Title (Year)». - Слоистое разрешение скаляров имени: override → recognition(+match) → новый базовый слой «контекст» (download.parsed_context, JSON naming.Fields). - naming: публичные Fields/Label/Derive, вынесен единый рендер; удалён FormatTitleYear. Сводка сезонов вынесена в recognize.SeasonSummary. - Режиссёр из метабазы (решение A2): TMDB/TVDB credits через опциональный metadata.DirectorProvider; авто-матч кладёт в plan.Director, ручной выбор кандидата тянет credits и пиннит ovrDirector. Метабаза бьёт контекст. - refreshDisplayNameLocked строит полный ярлык из эффективных полей; инфо-панель ревью показывает загруженного режиссёра. - Миграция 0011_parsed_context + ER-схема. Всё косметика: на пути/раскладку не влияет, приём/вывод имени не валятся (best-effort). Закрывает беклог-задачу «Кнопка „Обновить имя“: полный формат ярлыка». OpenSpec: archive/2026-07-11-field-resolution-display-name (ingest, recognition, metadata-match, review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 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-RU, со значением, настраиваемым конфигом [metadata.tmdb].language.
Это влияет только на локализованное поле Title/Name; поле
original_title/original_name остаётся на языке оригинала, поэтому
оригинальная сторона сравнения не затрагивается.
Scenario: Локализованный заголовок приходит по-русски
- GIVEN TMDB включён,
languageне задан в конфиге - WHEN выполняется поиск фильма с русской локализацией
- THEN запрос содержит
language=ru-RU - AND в кандидате
Titleприходит на русском, аOriginalTitle— на языке оригинала
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