Files
jellybit/internal/metadata/tmdb_test.go
T
avandClaude Opus 4.8 4fc8c41b3a Поиск по нескольким названиям при сверке с базой (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>
2026-06-29 12:16:47 +03:00

149 lines
4.5 KiB
Go

package metadata
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func newTMDB(t *testing.T, url string) *TMDB {
t.Helper()
c, err := NewTMDB(TMDBConfig{APIKey: "k", BaseURL: url}, nil)
if err != nil {
t.Fatalf("NewTMDB: %v", err)
}
return c
}
func TestTMDB_SearchMovie(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/search/movie" {
t.Errorf("path = %q", r.URL.Path)
}
q := r.URL.Query()
if q.Get("api_key") != "k" || q.Get("query") != "The Matrix" || q.Get("year") != "1999" {
t.Errorf("query = %v", q)
}
_, _ = w.Write([]byte(`{"results":[
{"id":603,"title":"The Matrix","original_title":"The Matrix","release_date":"1999-03-31"}
]}`))
}))
defer srv.Close()
got, err := newTMDB(t, srv.URL).Search(context.Background(), Query{Type: Movie, Title: "The Matrix", Year: 1999})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(got) != 1 {
t.Fatalf("got %d candidates", len(got))
}
c := got[0]
if c.Provider != "tmdb" || c.ID != "603" || c.Title != "The Matrix" || c.Year != 1999 {
t.Errorf("candidate = %+v", c)
}
}
func TestTMDB_SearchSeries(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/search/tv" {
t.Errorf("path = %q", r.URL.Path)
}
if r.URL.Query().Get("first_air_date_year") != "2015" {
t.Errorf("year param = %q", r.URL.Query().Get("first_air_date_year"))
}
_, _ = w.Write([]byte(`{"results":[
{"id":60622,"name":"Fargo","original_name":"Fargo","first_air_date":"2014-04-15"}
]}`))
}))
defer srv.Close()
got, err := newTMDB(t, srv.URL).Search(context.Background(), Query{Type: Series, Title: "Fargo", Year: 2015})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(got) != 1 || got[0].ID != "60622" || got[0].Title != "Fargo" || got[0].Year != 2014 {
t.Errorf("candidate = %+v", got[0])
}
}
func TestTMDB_SeasonEpisodeCounts(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tv/60622" {
t.Errorf("path = %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{"seasons":[
{"season_number":0,"episode_count":2},
{"season_number":1,"episode_count":10},
{"season_number":2,"episode_count":10}
]}`))
}))
defer srv.Close()
counts, err := newTMDB(t, srv.URL).SeasonEpisodeCounts(context.Background(), "60622")
if err != nil {
t.Fatalf("SeasonEpisodeCounts: %v", err)
}
if counts[1] != 10 || counts[2] != 10 || counts[0] != 2 {
t.Errorf("counts = %v", counts)
}
}
func TestTMDB_ErrorStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"status_message":"invalid key"}`))
}))
defer srv.Close()
_, err := newTMDB(t, srv.URL).Search(context.Background(), Query{Type: Movie, Title: "X"})
if err == nil {
t.Fatal("want error on 401")
}
}
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")
}
}