Единый источник полей отображаемого имени и один рендер полного ярлыка на всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный «Название (режиссёр, год). Сезон 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>
215 lines
6.5 KiB
Go
215 lines
6.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)
|
|
}
|
|
if c.URL != "https://www.themoviedb.org/movie/603" {
|
|
t.Errorf("URL = %q", c.URL)
|
|
}
|
|
}
|
|
|
|
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])
|
|
}
|
|
if got[0].URL != "https://www.themoviedb.org/tv/60622" {
|
|
t.Errorf("URL = %q", got[0].URL)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestTMDB_DirectorMovie(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/movie/603/credits" {
|
|
t.Errorf("path = %q", r.URL.Path)
|
|
}
|
|
_, _ = w.Write([]byte(`{"crew":[
|
|
{"job":"Producer","name":"Joel Silver"},
|
|
{"job":"Director","name":"Lana Wachowski"}
|
|
]}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
got, err := newTMDB(t, srv.URL).Director(context.Background(), Movie, "603")
|
|
if err != nil {
|
|
t.Fatalf("Director: %v", err)
|
|
}
|
|
if got != "Lana Wachowski" {
|
|
t.Errorf("director = %q, want «Lana Wachowski»", got)
|
|
}
|
|
}
|
|
|
|
func TestTMDB_DirectorSeriesCreatedBy(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(`{"created_by":[{"name":"Noah Hawley"}]}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
got, err := newTMDB(t, srv.URL).Director(context.Background(), Series, "60622")
|
|
if err != nil {
|
|
t.Fatalf("Director: %v", err)
|
|
}
|
|
if got != "Noah Hawley" {
|
|
t.Errorf("director = %q, want «Noah Hawley»", got)
|
|
}
|
|
}
|
|
|
|
// Нет режиссёра в crew → пустая строка (best-effort, не ошибка).
|
|
func TestTMDB_DirectorAbsent(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`{"crew":[{"job":"Producer","name":"X"}]}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
got, err := newTMDB(t, srv.URL).Director(context.Background(), Movie, "1")
|
|
if err != nil {
|
|
t.Fatalf("Director: %v", err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("director = %q, want пусто", got)
|
|
}
|
|
}
|
|
|
|
// TMDB реализует опциональный DirectorProvider.
|
|
func TestTMDB_ImplementsDirectorProvider(t *testing.T) {
|
|
var _ DirectorProvider = (*TMDB)(nil)
|
|
}
|