From da86a1d4197a36e46d73ff51cbb453a5f7bdf886 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 3 Mar 2026 18:44:56 +0000 Subject: [PATCH] feat: fallback to TMDB external_ids when Trakt rejects tmdbId When Trakt returns a non-404 error (e.g. 403) for a tmdbId lookup, fetch the imdbId from TMDB's /external_ids endpoint and retry the Trakt call using the imdbId. Trakt often knows a movie/show by its imdbId even when it does not recognise the tmdbId. Co-Authored-By: Claude Sonnet 4.6 --- media/movie.go | 20 +++++++++++++++++++- media/show.go | 20 +++++++++++++++++++- media/tmdb/media.go | 42 ++++++++++++++++++++++++++++++++++++++++++ media/tmdb/struct.go | 4 ++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/media/movie.go b/media/movie.go index a94f2d8..bd3c0bd 100644 --- a/media/movie.go +++ b/media/movie.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/l3uddz/nabarr/media/trakt" @@ -22,7 +23,24 @@ func (c *Client) GetMovieInfo(item *FeedItem) (*Item, error) { if errors.Is(err, trakt.ErrItemNotFound) { return nil, fmt.Errorf("trakt: get movie: movie with %sId %q: %w", mdp, mdi, ErrItemNotFound) } - return nil, fmt.Errorf("trakt: get movie: movie with %sId %q: %w", mdp, mdi, err) + + // fallback: if we queried by tmdbId and have TMDB configured, try fetching + // the imdbId from TMDB and retry Trakt — Trakt may know the movie by imdbId + // even when it doesn't recognise the tmdbId (returns 403). + if c.tmdb != nil && mdp == "tmdb" { + if tmdbIdInt, convErr := strconv.Atoi(mdi); convErr == nil { + if imdbId, extErr := c.tmdb.GetMovieExternalIds(tmdbIdInt); extErr == nil && strings.HasPrefix(imdbId, "tt") { + t, err = c.trakt.GetMovie("imdb", imdbId) + } + } + } + + if err != nil { + if errors.Is(err, trakt.ErrItemNotFound) { + return nil, fmt.Errorf("trakt: get movie: movie with %sId %q: %w", mdp, mdi, ErrItemNotFound) + } + return nil, fmt.Errorf("trakt: get movie: movie with %sId %q: %w", mdp, mdi, err) + } } // transform trakt info diff --git a/media/show.go b/media/show.go index 8630ace..2b18835 100644 --- a/media/show.go +++ b/media/show.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "github.com/l3uddz/nabarr/media/trakt" ) @@ -21,7 +22,24 @@ func (c *Client) GetShowInfo(item *FeedItem) (*Item, error) { if errors.Is(err, trakt.ErrItemNotFound) { return nil, fmt.Errorf("trakt: get show: show with %sId %q: %w", mdp, mdi, ErrItemNotFound) } - return nil, fmt.Errorf("trakt: get show: show with %sId %q: %w", mdp, mdi, err) + + // fallback: if we queried by tmdbId and have TMDB configured, try fetching + // the imdbId from TMDB and retry Trakt — Trakt may know the show by imdbId + // even when it doesn't recognise the tmdbId (returns 403). + if c.tmdb != nil && mdp == "tmdb" { + if tmdbIdInt, convErr := strconv.Atoi(mdi); convErr == nil { + if imdbId, extErr := c.tmdb.GetShowExternalIds(tmdbIdInt); extErr == nil && strings.HasPrefix(imdbId, "tt") { + t, err = c.trakt.GetShow("imdb", imdbId) + } + } + } + + if err != nil { + if errors.Is(err, trakt.ErrItemNotFound) { + return nil, fmt.Errorf("trakt: get show: show with %sId %q: %w", mdp, mdi, ErrItemNotFound) + } + return nil, fmt.Errorf("trakt: get show: show with %sId %q: %w", mdp, mdi, err) + } } // transform trakt info to MediaItem diff --git a/media/tmdb/media.go b/media/tmdb/media.go index 1002152..7f52ae9 100644 --- a/media/tmdb/media.go +++ b/media/tmdb/media.go @@ -50,6 +50,48 @@ func (c *Client) SearchMovies(title string, year int) (int, error) { return b.Results[0].Id, nil } +func (c *Client) GetMovieExternalIds(tmdbId int) (string, error) { + reqUrl := util.JoinURL(c.apiURL, "movie", strconv.Itoa(tmdbId), "external_ids") + "?api_key=" + c.apiKey + + resp, err := rek.Get(reqUrl, rek.Client(c.http)) + if err != nil { + return "", fmt.Errorf("request movie external ids: %w", err) + } + defer resp.Body().Close() + + if resp.StatusCode() != 200 { + return "", fmt.Errorf("validate movie external ids response: %s", resp.Status()) + } + + b := new(externalIdsResponse) + if err := json.NewDecoder(resp.Body()).Decode(b); err != nil { + return "", fmt.Errorf("decode movie external ids response: %w", err) + } + + return b.ImdbId, nil +} + +func (c *Client) GetShowExternalIds(tmdbId int) (string, error) { + reqUrl := util.JoinURL(c.apiURL, "tv", strconv.Itoa(tmdbId), "external_ids") + "?api_key=" + c.apiKey + + resp, err := rek.Get(reqUrl, rek.Client(c.http)) + if err != nil { + return "", fmt.Errorf("request show external ids: %w", err) + } + defer resp.Body().Close() + + if resp.StatusCode() != 200 { + return "", fmt.Errorf("validate show external ids response: %s", resp.Status()) + } + + b := new(externalIdsResponse) + if err := json.NewDecoder(resp.Body()).Decode(b); err != nil { + return "", fmt.Errorf("decode show external ids response: %w", err) + } + + return b.ImdbId, nil +} + func (c *Client) SearchShows(title string, year int) (int, error) { vals := url.Values{ "api_key": []string{c.apiKey}, diff --git a/media/tmdb/struct.go b/media/tmdb/struct.go index c6b1eb7..bc6427d 100644 --- a/media/tmdb/struct.go +++ b/media/tmdb/struct.go @@ -11,3 +11,7 @@ type tvSearchResponse struct { Id int `json:"id"` } `json:"results"` } + +type externalIdsResponse struct { + ImdbId string `json:"imdb_id"` +}