117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package tmdb
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
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)
|
|
}
|