From 1a099a37243d03e79af4ba0f61eab7adb667016c Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Tue, 13 Jan 2026 19:11:17 +1100 Subject: [PATCH] updated tmdb --- tmdb/api.go | 26 ++++ tmdb/config.go | 16 +- tmdb/credits.go | 11 +- tmdb/doc.go | 160 +++++++++++++++++++ tmdb/go.mod | 5 +- tmdb/go.sum | 2 + tmdb/movie.go | 11 +- tmdb/request.go | 122 +++++++++++++-- tmdb/request_test.go | 360 +++++++++++++++++++++++++++++++++++++++++++ tmdb/search.go | 22 +-- 10 files changed, 691 insertions(+), 44 deletions(-) create mode 100644 tmdb/api.go create mode 100644 tmdb/doc.go create mode 100644 tmdb/request_test.go diff --git a/tmdb/api.go b/tmdb/api.go new file mode 100644 index 0000000..b80911a --- /dev/null +++ b/tmdb/api.go @@ -0,0 +1,26 @@ +package tmdb + +import ( + "git.haelnorr.com/h/golib/env" + "github.com/pkg/errors" +) + +type API struct { + *Config + token string // ENV TMDB_TOKEN: API token for TMDB (required) +} + +func NewAPIConnection() (*API, error) { + token := env.String("TMDB_TOKEN", "") + if token == "" { + return nil, errors.New("No TMDB API Token provided") + } + api := &API{ + token: token, + } + err := api.getConfig() + if err != nil { + return nil, errors.Wrap(err, "api.getConfig") + } + return api, nil +} diff --git a/tmdb/config.go b/tmdb/config.go index b58df22..3816086 100644 --- a/tmdb/config.go +++ b/tmdb/config.go @@ -20,13 +20,17 @@ type Image struct { StillSizes []string `json:"still_sizes"` } -func GetConfig(token string) (*Config, error) { - url := "https://api.themoviedb.org/3/configuration" - data, err := tmdbGet(url, token) +func (api *API) getConfig() error { + url := requestURL("configuration") + data, err := api.get(url) if err != nil { - return nil, errors.Wrap(err, "tmdbGet") + return errors.Wrap(err, "api.get") } config := Config{} - json.Unmarshal(data, &config) - return &config, nil + err = json.Unmarshal(data, &config) + if err != nil { + return errors.Wrap(err, "json.Unmarshal") + } + api.Config = &config + return nil } diff --git a/tmdb/credits.go b/tmdb/credits.go index 86d9a23..1f91dab 100644 --- a/tmdb/credits.go +++ b/tmdb/credits.go @@ -2,7 +2,7 @@ package tmdb import ( "encoding/json" - "fmt" + "strconv" "github.com/pkg/errors" ) @@ -42,11 +42,12 @@ type Crew struct { Job string `json:"job"` } -func GetCredits(movieid int32, token string) (*Credits, error) { - url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v/credits?language=en-US", movieid) - data, err := tmdbGet(url, token) +func (api *API) GetCredits(movieid int64) (*Credits, error) { + path := []string{"movie", strconv.FormatInt(movieid, 10), "credits"} + url := buildURL(path, nil) + data, err := api.get(url) if err != nil { - return nil, errors.Wrap(err, "tmdbGet") + return nil, errors.Wrap(err, "api.get") } credits := Credits{} json.Unmarshal(data, &credits) diff --git a/tmdb/doc.go b/tmdb/doc.go new file mode 100644 index 0000000..f666cb3 --- /dev/null +++ b/tmdb/doc.go @@ -0,0 +1,160 @@ +// Package tmdb provides a client for The Movie Database (TMDB) API. +// +// This package offers a clean interface for interacting with TMDB's REST API, +// including automatic rate limiting, retry logic, and convenient URL building utilities. +// +// # Getting Started +// +// First, create an API connection using your TMDB API token: +// +// api, err := tmdb.NewAPIConnection() +// if err != nil { +// log.Fatal(err) +// } +// +// The token is read from the TMDB_TOKEN environment variable. +// +// # Making Requests +// +// The package provides clean URL building functions to construct API requests: +// +// // Simple endpoint +// url := tmdb.requestURL("movie", "550") +// // Result: "https://api.themoviedb.org/3/movie/550" +// +// // With query parameters +// url := tmdb.buildURL([]string{"search", "movie"}, map[string]string{ +// "query": "Inception", +// "page": "1", +// }) +// // Result: "https://api.themoviedb.org/3/search/movie?language=en-US&page=1&query=Inception" +// +// All requests made with buildURL automatically include "language=en-US" by default. +// +// # Rate Limiting +// +// TMDB has rate limits around 40 requests per second. This package implements +// automatic retry logic with exponential backoff: +// +// - Initial backoff: 1 second +// - Exponential growth: 1s → 2s → 4s → 8s → 16s → 32s (max) +// - Maximum retries: 3 attempts +// - Respects Retry-After header when provided by the API +// +// Example of rate-limited request: +// +// data, err := api.get(url) +// if err != nil { +// // Will return error only after exhausting all retries +// log.Printf("Request failed: %v", err) +// } +// +// # Searching for Movies +// +// Search for movies by title: +// +// results, err := tmdb.SearchMovies(token, "Fight Club", false, 1) +// if err != nil { +// log.Fatal(err) +// } +// +// for _, movie := range results.Results { +// fmt.Printf("%s %s\n", movie.Title, movie.ReleaseYear()) +// fmt.Printf("Poster: %s\n", movie.GetPoster(&api.Image, "w500")) +// } +// +// # Getting Movie Details +// +// Fetch detailed information about a specific movie: +// +// movie, err := tmdb.GetMovie(550, token) +// if err != nil { +// log.Fatal(err) +// } +// +// fmt.Printf("Title: %s\n", movie.Title) +// fmt.Printf("Overview: %s\n", movie.Overview) +// fmt.Printf("Release Date: %s\n", movie.ReleaseDate) +// fmt.Printf("IMDb ID: %s\n", movie.IMDbID) +// +// # Getting Credits +// +// Retrieve cast and crew information: +// +// credits, err := tmdb.GetCredits(550, token) +// if err != nil { +// log.Fatal(err) +// } +// +// fmt.Println("Cast:") +// for _, actor := range credits.Cast { +// fmt.Printf(" %s as %s\n", actor.Name, actor.Character) +// } +// +// fmt.Println("\nCrew:") +// for _, member := range credits.Crew { +// if member.Job == "Director" { +// fmt.Printf(" Director: %s\n", member.Name) +// } +// } +// +// # Image URLs +// +// The API configuration includes base URLs for images. Use helper methods to +// construct full image URLs: +// +// posterURL := movie.GetPoster(&api.Image, "w500") +// // Available sizes: "w92", "w154", "w185", "w342", "w500", "w780", "original" +// +// # Error Handling +// +// The package returns wrapped errors for easy debugging: +// +// data, err := api.get(url) +// if err != nil { +// if strings.Contains(err.Error(), "rate limit exceeded") { +// // Handle rate limiting +// } else if strings.Contains(err.Error(), "unexpected status code") { +// // Handle HTTP errors +// } else { +// // Handle network errors +// } +// } +// +// Common error scenarios: +// - "rate limit exceeded: maximum retries reached" - All retry attempts exhausted +// - "unexpected status code: 401" - Invalid API token +// - "unexpected status code: 404" - Resource not found +// - Network errors for connectivity issues +// +// # Environment Variables +// +// The package uses the following environment variable: +// +// - TMDB_TOKEN: Your TMDB API access token (required) +// +// Obtain an API token from: https://www.themoviedb.org/settings/api +// +// # Best Practices +// +// 1. Reuse the API connection instead of creating new ones for each request +// 2. Use buildURL for consistency and automatic language parameter injection +// 3. Handle rate limit errors gracefully - they indicate temporary service issues +// 4. Cache API responses when appropriate to reduce API calls +// 5. Use specific image sizes instead of "original" to save bandwidth +// +// # API Documentation +// +// For complete TMDB API documentation, visit: +// https://developer.themoviedb.org/docs +// +// # Rate Limiting Details +// +// From TMDB's documentation: +// "While our legacy rate limits have been disabled for some time, we do still +// have some upper limits to help mitigate needlessly high bulk scraping. They +// sit somewhere in the 40 requests per second range." +// +// This package automatically handles rate limiting with exponential backoff to +// ensure respectful API usage. +package tmdb diff --git a/tmdb/go.mod b/tmdb/go.mod index 5c4412a..3886446 100644 --- a/tmdb/go.mod +++ b/tmdb/go.mod @@ -2,4 +2,7 @@ module git.haelnorr.com/h/golib/tmdb go 1.25.5 -require github.com/pkg/errors v0.9.1 +require ( + git.haelnorr.com/h/golib/env v0.9.1 + github.com/pkg/errors v0.9.1 +) diff --git a/tmdb/go.sum b/tmdb/go.sum index 7c401c3..6f63fe5 100644 --- a/tmdb/go.sum +++ b/tmdb/go.sum @@ -1,2 +1,4 @@ +git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY= +git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/tmdb/movie.go b/tmdb/movie.go index a3b77f7..8e450f8 100644 --- a/tmdb/movie.go +++ b/tmdb/movie.go @@ -2,7 +2,7 @@ package tmdb import ( "encoding/json" - "fmt" + "strconv" "github.com/pkg/errors" ) @@ -33,11 +33,12 @@ type Movie struct { Video bool `json:"video"` } -func GetMovie(id int32, token string) (*Movie, error) { - url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v?language=en-US", id) - data, err := tmdbGet(url, token) +func (api *API) GetMovie(movieid int64) (*Movie, error) { + path := []string{"movie", strconv.FormatInt(movieid, 10)} + url := buildURL(path, nil) + data, err := api.get(url) if err != nil { - return nil, errors.Wrap(err, "tmdbGet") + return nil, errors.Wrap(err, "api.get") } movie := Movie{} json.Unmarshal(data, &movie) diff --git a/tmdb/request.go b/tmdb/request.go index 6c454b4..0ab1f85 100644 --- a/tmdb/request.go +++ b/tmdb/request.go @@ -4,25 +4,113 @@ import ( "fmt" "io" "net/http" + "net/url" + "strings" + "time" "github.com/pkg/errors" ) -func tmdbGet(url string, token string) ([]byte, error) { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, errors.Wrap(err, "http.NewRequest") - } - req.Header.Add("accept", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, errors.Wrap(err, "http.DefaultClient.Do") - } - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, errors.Wrap(err, "io.ReadAll") - } - return body, nil +const baseURL string = "https://api.themoviedb.org" +const apiVer string = "3" + +const ( + maxRetries = 3 // Maximum number of retry attempts for 429 responses + initialBackoff = 1 * time.Second // Initial backoff duration + maxBackoff = 32 * time.Second // Maximum backoff duration +) + +// requestURL builds a clean API URL from path segments. +// Example: requestURL("movie", "550") -> "https://api.themoviedb.org/3/movie/550" +// Example: requestURL("search", "movie") -> "https://api.themoviedb.org/3/search/movie" +func requestURL(pathSegments ...string) string { + path := strings.Join(pathSegments, "/") + return fmt.Sprintf("%s/%s/%s", baseURL, apiVer, path) +} + +// buildURL is a convenience function that builds a URL with query parameters. +// Example: buildURL([]string{"search", "movie"}, map[string]string{"query": "Inception", "page": "1"}) +func buildURL(pathSegments []string, params map[string]string) string { + baseURL := requestURL(pathSegments...) + if params == nil { + params = map[string]string{} + } + params["language"] = "en-US" + values := url.Values{} + for key, val := range params { + values.Add(key, val) + } + + return fmt.Sprintf("%s?%s", baseURL, values.Encode()) +} + +// get performs a GET request to the TMDB API with proper authentication headers +// and automatic retry logic with exponential backoff for rate limiting (429 responses). +// +// The TMDB API has rate limits around 40 requests per second. This function +// implements a courtesy backoff mechanism that: +// - Retries up to maxRetries times on 429 responses +// - Uses exponential backoff: 1s, 2s, 4s, 8s, etc. (up to maxBackoff) +// - Returns an error if max retries are exceeded +// +// The url parameter should be the full URL (can be built using requestURL or buildURL). +func (api *API) get(url string) ([]byte, error) { + backoff := initialBackoff + + for attempt := 0; attempt <= maxRetries; attempt++ { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrap(err, "http.NewRequest") + } + req.Header.Add("accept", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", api.token)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "http.DefaultClient.Do") + } + + // Check for rate limiting (429 Too Many Requests) + if res.StatusCode == http.StatusTooManyRequests { + res.Body.Close() + + // If we've exhausted retries, return an error + if attempt >= maxRetries { + return nil, errors.New("rate limit exceeded: maximum retries reached") + } + + // Check for Retry-After header first (respect server's guidance) + if retryAfter := res.Header.Get("Retry-After"); retryAfter != "" { + if duration, err := time.ParseDuration(retryAfter + "s"); err == nil { + backoff = duration + } + } + + // Apply exponential backoff: 1s, 2s, 4s, 8s, etc. + if backoff > maxBackoff { + backoff = maxBackoff + } + + time.Sleep(backoff) + + // Double the backoff for next iteration + backoff *= 2 + + continue + } + + // For other error status codes, return an error + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("unexpected status code: %d", res.StatusCode) + } + + // Success - read and return body + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, errors.Wrap(err, "io.ReadAll") + } + return body, nil + } + + return nil, errors.Errorf("max retries (%d) exceeded due to rate limiting (HTTP 429)", maxRetries) } diff --git a/tmdb/request_test.go b/tmdb/request_test.go new file mode 100644 index 0000000..197a330 --- /dev/null +++ b/tmdb/request_test.go @@ -0,0 +1,360 @@ +package tmdb + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestRequestURL(t *testing.T) { + tests := []struct { + name string + segments []string + want string + }{ + { + name: "single segment", + segments: []string{"configuration"}, + want: "https://api.themoviedb.org/3/configuration", + }, + { + name: "two segments", + segments: []string{"search", "movie"}, + want: "https://api.themoviedb.org/3/search/movie", + }, + { + name: "movie with id", + segments: []string{"movie", "550"}, + want: "https://api.themoviedb.org/3/movie/550", + }, + { + name: "movie with id and credits", + segments: []string{"movie", "550", "credits"}, + want: "https://api.themoviedb.org/3/movie/550/credits", + }, + { + name: "no segments", + segments: []string{}, + want: "https://api.themoviedb.org/3/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := requestURL(tt.segments...) + if got != tt.want { + t.Errorf("requestURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBuildURL(t *testing.T) { + tests := []struct { + name string + segments []string + params map[string]string + want string + }{ + { + name: "no params", + segments: []string{"movie", "550"}, + params: nil, + want: "https://api.themoviedb.org/3/movie/550?language=en-US", + }, + { + name: "with query param", + segments: []string{"search", "movie"}, + params: map[string]string{ + "query": "Inception", + }, + want: "https://api.themoviedb.org/3/search/movie?language=en-US&query=Inception", + }, + { + name: "multiple params", + segments: []string{"search", "movie"}, + params: map[string]string{ + "query": "Fight Club", + "page": "2", + "include_adult": "false", + }, + // Note: URL params can be in any order, so we check contains instead + want: "https://api.themoviedb.org/3/search/movie?", + }, + { + name: "params with special characters", + segments: []string{"search", "movie"}, + params: map[string]string{ + "query": "The Matrix", + }, + want: "https://api.themoviedb.org/3/search/movie?", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildURL(tt.segments, tt.params) + if !strings.HasPrefix(got, tt.want) { + t.Errorf("buildURL() = %v, want prefix %v", got, tt.want) + } + // Check that all params are present (checking keys, values may be URL encoded) + for key := range tt.params { + if !strings.Contains(got, key+"=") { + t.Errorf("buildURL() missing param key %s in %v", key, got) + } + } + // Check that language is always added + if !strings.Contains(got, "language=en-US") { + t.Errorf("buildURL() missing default language param in %v", got) + } + }) + } +} + +func TestAPIGet_Success(t *testing.T) { + // Create a test server that returns 200 OK + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify headers + if r.Header.Get("accept") != "application/json" { + t.Errorf("missing or incorrect accept header") + } + if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + t.Errorf("missing or incorrect Authorization header") + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"success": true}`)) + })) + defer server.Close() + + api := &API{token: "test-token"} + body, err := api.get(server.URL) + if err != nil { + t.Errorf("get() unexpected error: %v", err) + } + + expected := `{"success": true}` + if string(body) != expected { + t.Errorf("get() = %v, want %v", string(body), expected) + } +} + +func TestAPIGet_RateLimitRetry(t *testing.T) { + attemptCount := 0 + + // Create a test server that returns 429 twice, then 200 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attemptCount++ + if attemptCount <= 2 { + w.WriteHeader(http.StatusTooManyRequests) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"success": true}`)) + })) + defer server.Close() + + api := &API{token: "test-token"} + start := time.Now() + body, err := api.get(server.URL) + elapsed := time.Since(start) + + if err != nil { + t.Errorf("get() unexpected error: %v", err) + } + + if attemptCount != 3 { + t.Errorf("expected 3 attempts, got %d", attemptCount) + } + + // Should have waited at least 1s + 2s = 3s total + if elapsed < 3*time.Second { + t.Errorf("expected backoff delay, got %v", elapsed) + } + + expected := `{"success": true}` + if string(body) != expected { + t.Errorf("get() = %v, want %v", string(body), expected) + } +} + +func TestAPIGet_RateLimitExceeded(t *testing.T) { + attemptCount := 0 + + // Create a test server that always returns 429 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attemptCount++ + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + api := &API{token: "test-token"} + _, err := api.get(server.URL) + + if err == nil { + t.Error("get() expected error, got nil") + } + + if !strings.Contains(err.Error(), "rate limit exceeded") { + t.Errorf("get() expected rate limit error, got: %v", err) + } + + // Should have attempted maxRetries + 1 times (initial + retries) + expectedAttempts := maxRetries + 1 + if attemptCount != expectedAttempts { + t.Errorf("expected %d attempts, got %d", expectedAttempts, attemptCount) + } +} + +func TestAPIGet_RetryAfterHeader(t *testing.T) { + attemptCount := 0 + + // Create a test server that returns 429 with Retry-After header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attemptCount++ + if attemptCount == 1 { + w.Header().Set("Retry-After", "2") + w.WriteHeader(http.StatusTooManyRequests) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"success": true}`)) + })) + defer server.Close() + + api := &API{token: "test-token"} + start := time.Now() + body, err := api.get(server.URL) + elapsed := time.Since(start) + + if err != nil { + t.Errorf("get() unexpected error: %v", err) + } + + // Should have waited at least 2s as specified in Retry-After + if elapsed < 2*time.Second { + t.Errorf("expected at least 2s delay from Retry-After header, got %v", elapsed) + } + + expected := `{"success": true}` + if string(body) != expected { + t.Errorf("get() = %v, want %v", string(body), expected) + } +} + +func TestAPIGet_NonOKStatus(t *testing.T) { + tests := []struct { + name string + statusCode int + }{ + {"bad request", http.StatusBadRequest}, + {"unauthorized", http.StatusUnauthorized}, + {"forbidden", http.StatusForbidden}, + {"not found", http.StatusNotFound}, + {"internal server error", http.StatusInternalServerError}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + })) + defer server.Close() + + api := &API{token: "test-token"} + _, err := api.get(server.URL) + + if err == nil { + t.Error("get() expected error, got nil") + } + + expectedError := fmt.Sprintf("unexpected status code: %d", tt.statusCode) + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("get() expected error containing %q, got: %v", expectedError, err) + } + }) + } +} + +func TestAPIGet_NetworkError(t *testing.T) { + api := &API{token: "test-token"} + _, err := api.get("http://invalid-domain-that-does-not-exist.local") + + if err == nil { + t.Error("get() expected error for invalid domain, got nil") + } + + if !strings.Contains(err.Error(), "http.DefaultClient.Do") { + t.Errorf("get() expected network error, got: %v", err) + } +} + +func TestAPIGet_InvalidURL(t *testing.T) { + api := &API{token: "test-token"} + _, err := api.get("://invalid-url") + + if err == nil { + t.Error("get() expected error for invalid URL, got nil") + } + + if !strings.Contains(err.Error(), "http.NewRequest") { + t.Errorf("get() expected URL parse error, got: %v", err) + } +} + +func TestAPIGet_ReadBodyError(t *testing.T) { + // Create a test server that closes connection before body is read + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "100") + w.WriteHeader(http.StatusOK) + // Don't write anything, causing a read error + })) + defer server.Close() + + api := &API{token: "test-token"} + + // Note: This test may not always fail as expected due to how httptest works + // In real scenarios, network issues would cause io.ReadAll to fail + _, err := api.get(server.URL) + + // Just verify we got a response (this test is mainly for coverage) + if err != nil && !strings.Contains(err.Error(), "io.ReadAll") { + t.Logf("get() error (expected in some cases): %v", err) + } +} + +// Benchmark tests +func BenchmarkRequestURL(b *testing.B) { + for i := 0; i < b.N; i++ { + requestURL("movie", "550", "credits") + } +} + +func BenchmarkBuildURL(b *testing.B) { + params := map[string]string{ + "query": "Inception", + "page": "1", + } + for i := 0; i < b.N; i++ { + buildURL([]string{"search", "movie"}, params) + } +} + +func BenchmarkAPIGet(b *testing.B) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + io.WriteString(w, `{"success": true}`) + })) + defer server.Close() + + api := &API{token: "test-token"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + api.get(server.URL) + } +} diff --git a/tmdb/search.go b/tmdb/search.go index 701ff04..256044c 100644 --- a/tmdb/search.go +++ b/tmdb/search.go @@ -2,9 +2,9 @@ package tmdb import ( "encoding/json" - "fmt" "net/url" "path" + "strconv" "github.com/pkg/errors" ) @@ -63,17 +63,19 @@ func (movie *ResultMovie) ReleaseYear() string { // return genres[:len(genres)-2] // } -func SearchMovies(token string, query string, adult bool, page int) (*ResultMovies, error) { - url := "https://api.themoviedb.org/3/search/movie" + - fmt.Sprintf("?query=%s", url.QueryEscape(query)) + - fmt.Sprintf("&include_adult=%t", adult) + - fmt.Sprintf("&page=%v", page) + - "&language=en-US" - response, err := tmdbGet(url, token) +func (api *API) SearchMovies(query string, adult bool, page int64) (*ResultMovies, error) { + path := []string{"searc", "movie"} + params := map[string]string{ + "query": url.QueryEscape(query), + "include_adult": strconv.FormatBool(adult), + "page": strconv.FormatInt(page, 10), + } + url := buildURL(path, params) + data, err := api.get(url) if err != nil { - return nil, errors.Wrap(err, "tmdbGet") + return nil, errors.Wrap(err, "api.get") } var results ResultMovies - json.Unmarshal(response, &results) + json.Unmarshal(data, &results) return &results, nil }