package providers import ( "context" "fmt" "net/url" "strconv" "strings" ) // tmdb searches The Movie Database. It accepts either a v3 API key or a v4 // "API Read Access Token". type tmdb struct { r *Registry key string kind string // "movie" or "tv" } func (t *tmdb) Name() string { return "tmdb" } func (t *tmdb) Label() string { return "TMDB" } func (t *tmdb) get(ctx context.Context, path string, q url.Values, out any) error { if q == nil { q = url.Values{} } header := map[string]string{} if strings.HasPrefix(t.key, "eyJ") { header["Authorization"] = "Bearer " + t.key } else { q.Set("api_key", t.key) } err := t.r.getJSON(ctx, "https://api.themoviedb.org/3"+path+"?"+q.Encode(), header, out) if isStatus(err, 401) { return fmt.Errorf("TMDB rejected the API key; check it under Settings → Integrations") } return err } func tmdbImage(path string) string { if path == "" { return "" } return "https://image.tmdb.org/t/p/w780" + path } type tmdbHit struct { ID int64 `json:"id"` Title string `json:"title"` Name string `json:"name"` ReleaseDate string `json:"release_date"` FirstAirDate string `json:"first_air_date"` PosterPath string `json:"poster_path"` Overview string `json:"overview"` } func (t *tmdb) Search(ctx context.Context, query string) ([]Result, error) { var resp struct { Results []tmdbHit `json:"results"` } q := url.Values{"query": {query}, "include_adult": {"false"}} if err := t.get(ctx, "/search/"+t.kind, q, &resp); err != nil { return nil, err } out := make([]Result, 0, len(resp.Results)) for _, h := range resp.Results { title, date := h.Title, h.ReleaseDate if t.kind == "tv" { title, date = h.Name, h.FirstAirDate } id := strconv.FormatInt(h.ID, 10) out = append(out, Result{ Provider: "tmdb", ProviderID: t.kind + ":" + id, Title: title, Year: yearOf(date), ImageURL: tmdbImage(h.PosterPath), ExternalURL: "https://www.themoviedb.org/" + t.kind + "/" + id, Overview: truncate(h.Overview, maxDescription), }) } return out, nil } func (t *tmdb) Details(ctx context.Context, res Result) (Result, error) { if res.Provider != "tmdb" { return res, nil } _, id, ok := strings.Cut(res.ProviderID, ":") if !ok { return res, nil } var d struct { Genres []struct { Name string `json:"name"` } `json:"genres"` Runtime int `json:"runtime"` Networks []struct { Name string `json:"name"` } `json:"networks"` CreatedBy []struct { Name string `json:"name"` } `json:"created_by"` Credits struct { Crew []struct { Name string `json:"name"` Job string `json:"job"` } `json:"crew"` } `json:"credits"` } q := url.Values{} if t.kind == "movie" { q.Set("append_to_response", "credits") } if err := t.get(ctx, "/"+t.kind+"/"+id, q, &d); err != nil { return res, err } var names []string if t.kind == "movie" { for _, c := range d.Credits.Crew { if c.Job == "Director" { names = append(names, c.Name) } } } else { for _, n := range d.Networks { names = append(names, n.Name) } } if len(names) > 0 { res.Subtitle = strings.Join(names, ", ") } var genres []string for _, g := range d.Genres { genres = append(genres, g.Name) } if res.Extra == nil { res.Extra = map[string]string{} } if len(genres) > 0 { res.Extra["genres"] = strings.Join(genres, ", ") } if d.Runtime > 0 { res.Extra["runtime"] = fmt.Sprintf("%dh %02dm", d.Runtime/60, d.Runtime%60) } return res, nil } // Lookup refreshes one movie or show ("movie:123" / "tv:456") by its TMDB ID. func (t *tmdb) Lookup(ctx context.Context, providerID string) (Result, error) { kind, id, _ := strings.Cut(providerID, ":") if kind != "movie" && kind != "tv" { return Result{}, ErrGone } if _, err := strconv.ParseInt(id, 10, 64); err != nil { return Result{}, ErrGone } var h tmdbHit err := t.get(ctx, "/"+kind+"/"+id, nil, &h) if isStatus(err, 404) { return Result{}, ErrGone } if err != nil { return Result{}, err } title, date := h.Title, h.ReleaseDate if kind == "tv" { title, date = h.Name, h.FirstAirDate } res := Result{ Provider: "tmdb", ProviderID: providerID, Title: title, Year: yearOf(date), ImageURL: tmdbImage(h.PosterPath), ExternalURL: "https://www.themoviedb.org/" + kind + "/" + id, Overview: truncate(h.Overview, maxDescription), } d := &tmdb{r: t.r, key: t.key, kind: kind} return d.Details(ctx, res) }