// Package service — TMDb discovery (trending / popular). // // DiscoverService surfaces curated lists from TMDb so the React home // page can show "Trending" and "Popular" rails alongside the user's own // library. All methods gracefully no-op when the TMDb provider is // disabled. package service import ( "context" "encoding/json" "fmt" "net/http" "net/url" "time" "go.uber.org/zap" ) // DiscoverService talks to TMDb's /trending and /movie/popular endpoints. type DiscoverService struct { log *zap.Logger tmdb *TMDbProvider client *http.Client } // NewDiscoverService is the constructor. func NewDiscoverService(log *zap.Logger, tmdb *TMDbProvider) *DiscoverService { return &DiscoverService{ log: log, tmdb: tmdb, client: &http.Client{Timeout: 15 * time.Second}, } } // Trending returns the daily trending movies (TMDb /trending/movie/day). func (d *DiscoverService) Trending(ctx context.Context) ([]Match, error) { return d.fetch(ctx, "/trending/movie/day") } // Popular returns the popular movies list (TMDb /movie/popular). func (d *DiscoverService) Popular(ctx context.Context) ([]Match, error) { return d.fetch(ctx, "/movie/popular") } // fetch is the shared helper that paginates page=1 only — that's all the // home page needs and it keeps us under TMDb's 50 rps limit. func (d *DiscoverService) fetch(ctx context.Context, path string) ([]Match, error) { if d.tmdb == nil || !d.tmdb.Enabled() { return nil, nil } q := url.Values{} q.Set("api_key", d.tmdb.cfg.Secrets.TMDbAPIKey) q.Set("language", "zh-CN") q.Set("page", "1") u := d.tmdb.base + path + "?" + q.Encode() type result struct { ID int `json:"id"` Title string `json:"title"` Name string `json:"name"` Overview string `json:"overview"` PosterPath string `json:"poster_path"` BackdropPath string `json:"backdrop_path"` ReleaseDate string `json:"release_date"` FirstAirDate string `json:"first_air_date"` VoteAverage float32 `json:"vote_average"` } type page struct { Results []result `json:"results"` } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, err } resp, err := d.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("tmdb %s: %d", path, resp.StatusCode) } var p page if err := json.NewDecoder(resp.Body).Decode(&p); err != nil { return nil, err } out := make([]Match, 0, len(p.Results)) for _, r := range p.Results { title := r.Title if title == "" { title = r.Name } m := Match{ TMDbID: r.ID, Title: title, Overview: r.Overview, Rating: r.VoteAverage, } if r.PosterPath != "" { m.PosterURL = d.tmdb.imgCDN + "/w500" + r.PosterPath } if r.BackdropPath != "" { m.BackdropURL = d.tmdb.imgCDN + "/w1280" + r.BackdropPath } date := r.ReleaseDate if date == "" { date = r.FirstAirDate } if len(date) >= 4 { fmt.Sscanf(date[:4], "%d", &m.Year) } out = append(out, m) } return out, nil }