From 736340583eb6722d971817a75a8e9bb9fb3de446 Mon Sep 17 00:00:00 2001 From: Silo Server Migration Date: Sun, 24 May 2026 18:33:20 -0400 Subject: [PATCH] feat(tmdb): add GetCompany and GetNetwork for logo path resolution --- internal/metadata/tmdb/client.go | 26 +++++++++++ internal/metadata/tmdb/client_test.go | 65 +++++++++++++++++++++++++++ internal/metadata/tmdb/types.go | 14 ++++++ 3 files changed, 105 insertions(+) diff --git a/internal/metadata/tmdb/client.go b/internal/metadata/tmdb/client.go index 5e79a55e..edefaeb1 100644 --- a/internal/metadata/tmdb/client.go +++ b/internal/metadata/tmdb/client.go @@ -544,6 +544,32 @@ func (c *Client) DiscoverPage(ctx context.Context, mediaType string, params Disc return normalizeMoviePage(resp), nil } +// GetCompany fetches a TMDB company (production studio) by ID. Used to +// resolve logo paths for the bundled discovery studios. +func (c *Client) GetCompany(ctx context.Context, id int) (*Company, error) { + if id <= 0 { + return nil, fmt.Errorf("tmdb: invalid company id: %d", id) + } + var company Company + if err := c.doGet(ctx, fmt.Sprintf("/company/%d", id), &company); err != nil { + return nil, err + } + return &company, nil +} + +// GetNetwork fetches a TMDB TV network by ID. Used to resolve logo paths +// for the bundled discovery networks. +func (c *Client) GetNetwork(ctx context.Context, id int) (*Network, error) { + if id <= 0 { + return nil, fmt.Errorf("tmdb: invalid network id: %d", id) + } + var network Network + if err := c.doGet(ctx, fmt.Sprintf("/network/%d", id), &network); err != nil { + return nil, err + } + return &network, nil +} + // buildDiscoverQuery composes the TMDB discover query string (without the // leading "?" and without page or api_key — doGet handles those). func buildDiscoverQuery(mediaType string, params DiscoverParams) string { diff --git a/internal/metadata/tmdb/client_test.go b/internal/metadata/tmdb/client_test.go index 86e91eb6..1990b8d2 100644 --- a/internal/metadata/tmdb/client_test.go +++ b/internal/metadata/tmdb/client_test.go @@ -379,6 +379,71 @@ func TestDiscoverPageDefaultsToPage1(t *testing.T) { } } +func TestGetCompany(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/company/420" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":420,"name":"Marvel Studios","logo_path":"/hUze.png"}`)) + })) + defer server.Close() + + client := NewClient("test-key", 1000) + client.SetBaseURL(server.URL) + + company, err := client.GetCompany(context.Background(), 420) + if err != nil { + t.Fatalf("GetCompany: %v", err) + } + if company.ID != 420 || company.Name != "Marvel Studios" || company.LogoPath != "/hUze.png" { + t.Errorf("company = %+v", company) + } +} + +func TestGetCompanyMissingLogoReturnsEmpty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":420,"name":"Marvel Studios","logo_path":null}`)) + })) + defer server.Close() + + client := NewClient("test-key", 1000) + client.SetBaseURL(server.URL) + + company, err := client.GetCompany(context.Background(), 420) + if err != nil { + t.Fatalf("GetCompany: %v", err) + } + if company.LogoPath != "" { + t.Errorf("logo_path = %q, want empty for null", company.LogoPath) + } +} + +func TestGetNetwork(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/network/213" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":213,"name":"Netflix","logo_path":"/wuU9.png"}`)) + })) + defer server.Close() + + client := NewClient("test-key", 1000) + client.SetBaseURL(server.URL) + + network, err := client.GetNetwork(context.Background(), 213) + if err != nil { + t.Fatalf("GetNetwork: %v", err) + } + if network.ID != 213 || network.Name != "Netflix" || network.LogoPath != "/wuU9.png" { + t.Errorf("network = %+v", network) + } +} + func TestSearchMediaMovie(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/search/movie" { diff --git a/internal/metadata/tmdb/types.go b/internal/metadata/tmdb/types.go index eefcce92..e9ecfc22 100644 --- a/internal/metadata/tmdb/types.go +++ b/internal/metadata/tmdb/types.go @@ -86,6 +86,20 @@ type ExternalIDs struct { TVDBID int `json:"tvdb_id"` } +// Company is the decoded payload of TMDB's /company/{id} endpoint. +type Company struct { + ID int `json:"id"` + Name string `json:"name"` + LogoPath string `json:"logo_path"` +} + +// Network is the decoded payload of TMDB's /network/{id} endpoint. +type Network struct { + ID int `json:"id"` + Name string `json:"name"` + LogoPath string `json:"logo_path"` +} + // Collection is the decoded payload of TMDB's /collection/{id} endpoint. // TMDB collections only contain movies (franchises, sagas), so each Part is // implicitly a movie. The Parts slice preserves TMDB's ordering, which is