Поиск по нескольким названиям при сверке с базой (recognition)
Сверка с метабазой промахивалась на иностранных фильмах с русским релиз-именем (кейс «Тёмный рыцарь»): поиск шёл по одной строке provider_hint||title и игнорировал original_title, а базы индексированы прежде всего по оригинальным названиям. - matchMetadata ищет по ключам original_title → title → provider_hint с ранним стопом на первом единичном сильном матче; пустые и нормализованно-дублирующие ключи пропускаются, кандидаты для review копятся из всех заходов. - Промпт требует всегда заполнять title и original_title (дублировать при отсутствии оригинала / российском контенте; при неуверенности дублировать, не выдумывать). Разбор остаётся мягким к пустому original_title. - TMDB-поиск передаёт language (по умолчанию ru-RU, настраивается [metadata.tmdb].language); original_title не зависит от локали. - Нормализация названий сводит ё→е. Инварианты не ослаблены: авто только при подтверждённом единичном матче + структурной валидации + согласованности сигналов. Capability recognition впервые перенесена в OpenSpec; change архивирован. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-11
@@ -13,22 +13,27 @@ import (
|
||||
"git.vakhrushev.me/av/jellybit/internal/logging"
|
||||
)
|
||||
|
||||
const tmdbDefaultBaseURL = "https://api.themoviedb.org/3"
|
||||
const (
|
||||
tmdbDefaultBaseURL = "https://api.themoviedb.org/3"
|
||||
tmdbDefaultLanguage = "ru-RU"
|
||||
)
|
||||
|
||||
// TMDBConfig — настройки клиента TMDB.
|
||||
type TMDBConfig struct {
|
||||
APIKey string
|
||||
Proxy string
|
||||
Timeout time.Duration
|
||||
BaseURL string // пусто → api.themoviedb.org; задаётся в тестах
|
||||
APIKey string
|
||||
Proxy string
|
||||
Timeout time.Duration
|
||||
BaseURL string // пусто → api.themoviedb.org; задаётся в тестах
|
||||
Language string // локаль возвращаемых названий; пусто → ru-RU
|
||||
}
|
||||
|
||||
// TMDB — клиент The Movie Database (API v3, авторизация по api_key).
|
||||
type TMDB struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
hc *http.Client
|
||||
log *slog.Logger
|
||||
apiKey string
|
||||
baseURL string
|
||||
language string
|
||||
hc *http.Client
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewTMDB собирает клиент TMDB. logger nil → slog.Default().
|
||||
@@ -44,10 +49,14 @@ func NewTMDB(cfg TMDBConfig, logger *slog.Logger) (*TMDB, error) {
|
||||
if base == "" {
|
||||
base = tmdbDefaultBaseURL
|
||||
}
|
||||
lang := cfg.Language
|
||||
if lang == "" {
|
||||
lang = tmdbDefaultLanguage
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &TMDB{apiKey: cfg.APIKey, baseURL: strings.TrimRight(base, "/"), hc: hc, log: logger}, nil
|
||||
return &TMDB{apiKey: cfg.APIKey, baseURL: strings.TrimRight(base, "/"), language: lang, hc: hc, log: logger}, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) Name() string { return "tmdb" }
|
||||
@@ -67,7 +76,7 @@ type tmdbSearchResp struct {
|
||||
// Search ищет фильм/сериал по названию и году.
|
||||
func (t *TMDB) Search(ctx context.Context, q Query) ([]Candidate, error) {
|
||||
var path string
|
||||
params := url.Values{"api_key": {t.apiKey}, "query": {q.Title}, "include_adult": {"false"}}
|
||||
params := url.Values{"api_key": {t.apiKey}, "query": {q.Title}, "include_adult": {"false"}, "language": {t.language}}
|
||||
switch q.Type {
|
||||
case Movie:
|
||||
path = "/search/movie"
|
||||
|
||||
@@ -102,6 +102,45 @@ func TestTMDB_ErrorStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTMDB_SearchDefaultLanguage(t *testing.T) {
|
||||
// Язык не задан в конфиге → дефолт ru-RU; original_title не зависит от локали.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("language"); got != "ru-RU" {
|
||||
t.Errorf("language = %q, want ru-RU", got)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"results":[
|
||||
{"id":155,"title":"Тёмный рыцарь","original_title":"The Dark Knight","release_date":"2008-07-18"}
|
||||
]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
got, err := newTMDB(t, srv.URL).Search(context.Background(), Query{Type: Movie, Title: "The Dark Knight", Year: 2008})
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if got[0].Title != "Тёмный рыцарь" || got[0].OriginalTitle != "The Dark Knight" {
|
||||
t.Errorf("candidate = %+v", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTMDB_SearchLanguageOverride(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("language"); got != "en-US" {
|
||||
t.Errorf("language = %q, want en-US", got)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"results":[]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := NewTMDB(TMDBConfig{APIKey: "k", BaseURL: srv.URL, Language: "en-US"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTMDB: %v", err)
|
||||
}
|
||||
if _, err := c.Search(context.Background(), Query{Type: Movie, Title: "X"}); err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTMDB_RequiresKey(t *testing.T) {
|
||||
if _, err := NewTMDB(TMDBConfig{}, nil); err == nil {
|
||||
t.Fatal("want error without api_key")
|
||||
|
||||
Reference in New Issue
Block a user