Clone
1
TMDB
Haelnorr edited this page 2026-01-13 21:22:53 +11:00

TMDB - v0.9.2

Go client library for The Movie Database (TMDB) API with automatic rate limiting, retry logic, and convenient helper functions.

Overview

TMDB provides a clean interface for interacting with TMDB's REST API, including:

  • Movie search functionality
  • Detailed movie information retrieval
  • Cast and crew data
  • Automatic rate limiting with exponential backoff
  • Retry logic for rate limit errors
  • Image URL helpers
  • Environment variable configuration
  • EZConf integration

Installation

go get git.haelnorr.com/h/golib/tmdb

Configuration

Environment Variables

// TMDB_TOKEN: TMDB API access token (required)
TMDB_TOKEN=your_api_token_here

Get your API token from: https://www.themoviedb.org/settings/api

Using ConfigFromEnv

import "git.haelnorr.com/h/golib/tmdb"

// Create API connection using environment variable
api, err := tmdb.NewAPIConnection()
if err != nil {
    log.Fatal(err)
}

Using EZConf Integration

import (
    "git.haelnorr.com/h/golib/ezconf"
    "git.haelnorr.com/h/golib/tmdb"
)

// Register TMDB with EZConf
loader := ezconf.New()
err := loader.RegisterIntegration(tmdb.NewEZConfIntegration())
if err != nil {
    log.Fatal(err)
}

// Load all configurations
if err := loader.Load(); err != nil {
    log.Fatal(err)
}

// Get the API connection
api, ok := loader.GetConfig("tmdb").(*tmdb.APIConnection)
if !ok {
    log.Fatal("tmdb config not found")
}

API Reference

Creating an API Connection

// NewAPIConnection creates a new TMDB API connection
api, err := tmdb.NewAPIConnection()
if err != nil {
    log.Fatal(err)
}

Searching for Movies

// SearchMovies searches for movies by title
// Parameters:
//   - query: search term
//   - includeAdult: whether to include adult content
//   - page: page number (1-indexed)
results, err := api.SearchMovies("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("Overview: %s\n", movie.Overview)
    fmt.Printf("Rating: %.1f/10\n", movie.VoteAverage)
}

Getting Movie Details

// GetMovie retrieves detailed information about a specific movie
// Parameter: movieID (TMDB movie ID)
movie, err := api.GetMovie(550) // Fight Club
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Title: %s\n", movie.Title)
fmt.Printf("Original Title: %s\n", movie.OriginalTitle)
fmt.Printf("Overview: %s\n", movie.Overview)
fmt.Printf("Release Date: %s\n", movie.ReleaseDate)
fmt.Printf("Runtime: %d minutes\n", movie.Runtime)
fmt.Printf("Budget: $%d\n", movie.Budget)
fmt.Printf("Revenue: $%d\n", movie.Revenue)
fmt.Printf("Rating: %.1f/10 (%d votes)\n", movie.VoteAverage, movie.VoteCount)
fmt.Printf("IMDb ID: %s\n", movie.IMDbID)
fmt.Printf("Homepage: %s\n", movie.Homepage)
fmt.Printf("Status: %s\n", movie.Status)
fmt.Printf("Tagline: %s\n", movie.Tagline)

// Genres
for _, genre := range movie.Genres {
    fmt.Printf("Genre: %s\n", genre.Name)
}

// Production companies
for _, company := range movie.ProductionCompanies {
    fmt.Printf("Company: %s\n", company.Name)
}

Getting Cast and Crew

// GetCredits retrieves cast and crew information for a movie
credits, err := api.GetCredits(550)
if err != nil {
    log.Fatal(err)
}

// Display cast
fmt.Println("Cast:")
for i, actor := range credits.Cast {
    if i >= 10 { // Limit to top 10
        break
    }
    fmt.Printf("  %s as %s\n", actor.Name, actor.Character)
    
    // Get profile image
    profileURL := actor.GetProfile(&api.Image, "w185")
    fmt.Printf("    Profile: %s\n", profileURL)
}

// Display crew (directors, writers, etc.)
fmt.Println("\nDirector:")
for _, member := range credits.Crew {
    if member.Job == "Director" {
        fmt.Printf("  %s\n", member.Name)
    }
}

fmt.Println("\nWriters:")
for _, member := range credits.Crew {
    if member.Department == "Writing" {
        fmt.Printf("  %s (%s)\n", member.Name, member.Job)
    }
}

fmt.Println("\nProducers:")
for _, member := range credits.Crew {
    if member.Department == "Production" {
        fmt.Printf("  %s (%s)\n", member.Name, member.Job)
    }
}

Working with Images

TMDB provides various image URLs for posters, backdrops, and profile pictures.

Image Sizes

Poster Sizes:

  • w92 - Small thumbnail
  • w154 - Thumbnail
  • w185 - Small poster
  • w342 - Medium poster
  • w500 - Large poster (recommended for most uses)
  • w780 - Extra large poster
  • original - Original resolution (very large)

Backdrop Sizes:

  • w300 - Small backdrop
  • w780 - Medium backdrop
  • w1280 - Large backdrop (recommended)
  • original - Original resolution

Profile Sizes:

  • w45 - Tiny profile pic
  • w185 - Standard profile pic (recommended)
  • h632 - Large profile pic
  • original - Original resolution

Getting Image URLs

// Movie poster
posterURL := movie.GetPoster(&api.Image, "w500")
fmt.Printf("Poster: %s\n", posterURL)

// Movie backdrop
backdropURL := movie.GetBackdrop(&api.Image, "w1280")
fmt.Printf("Backdrop: %s\n", backdropURL)

// Actor profile
for _, actor := range credits.Cast {
    profileURL := actor.GetProfile(&api.Image, "w185")
    fmt.Printf("%s's profile: %s\n", actor.Name, profileURL)
}

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

All API calls automatically handle rate limiting, so you don't need to worry about implementing your own retry logic.

How It Works

When a rate limit is hit (HTTP 429), the library:

  1. Checks if a Retry-After header is present and uses that value
  2. Otherwise, uses exponential backoff starting at 1 second
  3. Retries up to 3 times
  4. Returns an error if all retries are exhausted
// This is handled automatically - no special code needed
results, err := api.SearchMovies("Inception", false, 1)
if err != nil {
    // Will only error after all retries are exhausted
    log.Printf("Search failed: %v", err)
}

Error Handling

The package returns wrapped errors with context:

results, err := api.SearchMovies("Inception", false, 1)
if err != nil {
    if strings.Contains(err.Error(), "rate limit exceeded") {
        // All retry attempts exhausted
        log.Println("Rate limit reached, try again later")
    } else if strings.Contains(err.Error(), "unexpected status code: 401") {
        // Invalid API token
        log.Println("Invalid TMDB_TOKEN - check your API token")
    } else if strings.Contains(err.Error(), "unexpected status code: 404") {
        // Resource not found
        log.Println("Movie not found")
    } else {
        // Network or other errors
        log.Printf("Request failed: %v", err)
    }
}

Data Structures

Movie

type Movie struct {
    Adult               bool                  `json:"adult"`
    BackdropPath        string                `json:"backdrop_path"`
    Budget              int                   `json:"budget"`
    Genres              []Genre               `json:"genres"`
    Homepage            string                `json:"homepage"`
    ID                  int                   `json:"id"`
    IMDbID              string                `json:"imdb_id"`
    OriginalLanguage    string                `json:"original_language"`
    OriginalTitle       string                `json:"original_title"`
    Overview            string                `json:"overview"`
    Popularity          float64               `json:"popularity"`
    PosterPath          string                `json:"poster_path"`
    ProductionCompanies []ProductionCompany   `json:"production_companies"`
    ProductionCountries []ProductionCountry   `json:"production_countries"`
    ReleaseDate         string                `json:"release_date"`
    Revenue             int64                 `json:"revenue"`
    Runtime             int                   `json:"runtime"`
    SpokenLanguages     []SpokenLanguage      `json:"spoken_languages"`
    Status              string                `json:"status"`
    Tagline             string                `json:"tagline"`
    Title               string                `json:"title"`
    Video               bool                  `json:"video"`
    VoteAverage         float64               `json:"vote_average"`
    VoteCount           int                   `json:"vote_count"`
}

SearchResponse

type SearchResponse struct {
    Page         int     `json:"page"`
    Results      []Movie `json:"results"`
    TotalPages   int     `json:"total_pages"`
    TotalResults int     `json:"total_results"`
}

Credits

type Credits struct {
    ID   int    `json:"id"`
    Cast []Cast `json:"cast"`
    Crew []Crew `json:"crew"`
}

type Cast struct {
    Adult              bool    `json:"adult"`
    Gender             int     `json:"gender"`
    ID                 int     `json:"id"`
    KnownForDepartment string  `json:"known_for_department"`
    Name               string  `json:"name"`
    OriginalName       string  `json:"original_name"`
    Popularity         float64 `json:"popularity"`
    ProfilePath        string  `json:"profile_path"`
    CastID             int     `json:"cast_id"`
    Character          string  `json:"character"`
    CreditID           string  `json:"credit_id"`
    Order              int     `json:"order"`
}

type Crew struct {
    Adult              bool    `json:"adult"`
    Gender             int     `json:"gender"`
    ID                 int     `json:"id"`
    KnownForDepartment string  `json:"known_for_department"`
    Name               string  `json:"name"`
    OriginalName       string  `json:"original_name"`
    Popularity         float64 `json:"popularity"`
    ProfilePath        string  `json:"profile_path"`
    CreditID           string  `json:"credit_id"`
    Department         string  `json:"department"`
    Job                string  `json:"job"`
}

Complete Example

package main

import (
    "fmt"
    "log"
    "git.haelnorr.com/h/golib/tmdb"
)

func main() {
    // Create API connection
    api, err := tmdb.NewAPIConnection()
    if err != nil {
        log.Fatal(err)
    }

    // Search for a movie
    fmt.Println("Searching for movies...")
    results, err := api.SearchMovies("The Matrix", false, 1)
    if err != nil {
        log.Fatal(err)
    }

    if len(results.Results) == 0 {
        log.Fatal("No results found")
    }

    // Get the first result
    movieID := results.Results[0].ID
    fmt.Printf("\nFound: %s (%s)\n", results.Results[0].Title, results.Results[0].ReleaseYear())

    // Get detailed movie information
    fmt.Println("\nFetching movie details...")
    movie, err := api.GetMovie(movieID)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("\nMovie Details:\n")
    fmt.Printf("Title: %s\n", movie.Title)
    fmt.Printf("Tagline: %s\n", movie.Tagline)
    fmt.Printf("Overview: %s\n", movie.Overview)
    fmt.Printf("Release Date: %s\n", movie.ReleaseDate)
    fmt.Printf("Runtime: %d minutes\n", movie.Runtime)
    fmt.Printf("Rating: %.1f/10 (%d votes)\n", movie.VoteAverage, movie.VoteCount)
    fmt.Printf("Budget: $%d\n", movie.Budget)
    fmt.Printf("Revenue: $%d\n", movie.Revenue)
    fmt.Printf("IMDb: https://www.imdb.com/title/%s/\n", movie.IMDbID)

    // Get poster URL
    posterURL := movie.GetPoster(&api.Image, "w500")
    fmt.Printf("Poster: %s\n", posterURL)

    // Get cast and crew
    fmt.Println("\nFetching cast and crew...")
    credits, err := api.GetCredits(movieID)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\nMain Cast:")
    for i, actor := range credits.Cast {
        if i >= 5 { // Show top 5
            break
        }
        fmt.Printf("  %s as %s\n", actor.Name, actor.Character)
    }

    fmt.Println("\nDirector:")
    for _, member := range credits.Crew {
        if member.Job == "Director" {
            fmt.Printf("  %s\n", member.Name)
        }
    }

    fmt.Printf("\nGenres: ")
    for i, genre := range movie.Genres {
        if i > 0 {
            fmt.Print(", ")
        }
        fmt.Print(genre.Name)
    }
    fmt.Println()
}

Best Practices

  1. Reuse API Connections

    • Create one APIConnection and reuse it for all requests
    • Don't create a new connection for each API call
  2. Cache Responses

    • Cache movie details and search results to reduce API calls
    • Consider using an in-memory cache or database
  3. Use Appropriate Image Sizes

    • Use w500 for posters in most cases
    • Use w1280 for backdrops
    • Avoid original unless you specifically need high resolution
  4. Handle Rate Limits Gracefully

    • The library handles this automatically with retries
    • Be aware that retries may introduce delays (up to ~60 seconds total)
  5. Use Context with Timeout

    • For production applications, consider adding timeout handling
    • The library's automatic retry can take time
  6. Paginate Search Results

    • Search results are paginated (20 results per page)
    • Use the page parameter to get additional results

Testing

The package includes comprehensive tests with 94.1% coverage. To run tests:

# Set your TMDB token
export TMDB_TOKEN=your_api_token_here

# Run all tests
go test -v ./...

# Run specific test
go test -v -run TestSearchMovies

# Run with coverage
go test -cover ./...
  • ezconf - Unified configuration management
  • hlog - Structured logging

External Resources

License

MIT License - see LICENSE file for details