added new modules

2026-01-13 21:22:53 +11:00
parent 0ccfd91f56
commit 65c020c133
6 changed files with 1291 additions and 4 deletions

624
EZConf.md Normal file

@@ -0,0 +1,624 @@
# EZConf - v0.1.0
A unified configuration management system for loading and managing environment-based configurations across multiple Go packages.
## Installation
```bash
go get git.haelnorr.com/h/golib/ezconf
```
## Key Concepts and Features
### Unified Configuration Management
EZConf provides a centralized way to manage configurations from multiple packages that follow the ConfigFromEnv pattern. Instead of calling each package's configuration function individually, you can register them all with EZConf and load them together.
### Source Code Parsing
EZConf parses Go source files to extract environment variable documentation directly from struct field comments. This eliminates the need to maintain separate documentation for environment variables.
### .env File Management
EZConf can generate new .env files or update existing ones, preserving existing values while adding new variables. This makes it easy to maintain environment configuration across different deployment environments.
### Environment Variable Documentation
EZConf supports a standard comment format for documenting environment variables:
```go
// ENV VAR_NAME: Description (modifiers)
```
Supported modifiers:
- `(required)` - marks variable as required
- `(required if condition)` - marks variable as conditionally required
- `(default: value)` - specifies default value
## Quick Start
### Basic Usage with Built-in Integration (Recommended)
```go
package main
import (
"log"
"os"
"git.haelnorr.com/h/golib/ezconf"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
)
func main() {
// Create loader
loader := ezconf.New()
// Register packages using built-in integrations
loader.RegisterIntegrations(
hlog.NewEZConfIntegration(),
hws.NewEZConfIntegration(),
hwsauth.NewEZConfIntegration(),
)
// Load everything
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get configuration
hlogCfg, _ := loader.GetConfig("hlog")
cfg := hlogCfg.(*hlog.Config)
// Use configuration
logger, _ := hlog.NewLogger(cfg, os.Stdout)
logger.Info().Msg("Application started")
}
```
### Manual Registration
```go
package main
import (
"log"
"os"
"git.haelnorr.com/h/golib/ezconf"
"git.haelnorr.com/h/golib/hlog"
)
func main() {
// Create loader
loader := ezconf.New()
// Add package path
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")
// Add config function
loader.AddConfigFunc("hlog", func() (interface{}, error) {
return hlog.ConfigFromEnv()
})
// Load everything
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get configuration
hlogCfg, _ := loader.GetConfig("hlog")
cfg := hlogCfg.(*hlog.Config)
// Use configuration
logger, _ := hlog.NewLogger(cfg, os.Stdout)
logger.Info().Msg("Application started")
}
```
## Configuration
EZConf uses ConfigFromEnv pattern - no environment variables needed for ezconf itself. It reads environment variables as documented in the packages you register.
### Built-in Integrations
All golib packages that support configuration include built-in ezconf integration. These packages provide a `NewEZConfIntegration()` function that returns an `Integration` object:
- **hlog** - `hlog.NewEZConfIntegration()`
- **hws** - `hws.NewEZConfIntegration()`
- **hwsauth** - `hwsauth.NewEZConfIntegration()`
- **tmdb** - `tmdb.NewEZConfIntegration()`
Using built-in integrations is the recommended approach as it automatically configures the package path and configuration function.
### Adding Packages
```go
loader := ezconf.New()
// Add package paths to parse for ENV comments
loader.AddPackagePath("/path/to/golib/hlog")
loader.AddPackagePath("/path/to/golib/hws")
loader.AddPackagePath("/path/to/your/custom/package")
```
### Adding Config Functions
```go
// Register configuration loaders
loader.AddConfigFunc("hlog", func() (interface{}, error) {
return hlog.ConfigFromEnv()
})
loader.AddConfigFunc("hws", func() (interface{}, error) {
return hws.ConfigFromEnv()
})
loader.AddConfigFunc("myapp", func() (interface{}, error) {
return myapp.ConfigFromEnv()
})
```
### Adding Custom Environment Variables
```go
// Add environment variables that aren't in a package config
loader.AddEnvVar(ezconf.EnvVar{
Name: "DATABASE_URL",
Description: "PostgreSQL connection string",
Required: true,
Default: "postgres://localhost/mydb",
})
```
## Detailed Usage
### Using Built-in Integrations
The easiest way to use ezconf is with the built-in integrations:
```go
loader := ezconf.New()
// Register a single integration
loader.RegisterIntegration(hlog.NewEZConfIntegration())
// Or register multiple at once
loader.RegisterIntegrations(
hlog.NewEZConfIntegration(),
hws.NewEZConfIntegration(),
hwsauth.NewEZConfIntegration(),
tmdb.NewEZConfIntegration(),
)
loader.Load()
```
The `Integration` interface requires three methods:
- `Name() string` - returns the config name (e.g., "hlog")
- `PackagePath() string` - returns the path to parse for ENV comments
- `ConfigFunc() func() (interface{}, error)` - returns the ConfigFromEnv function
### Creating Custom Integrations
You can create integrations for your own packages:
```go
package myapp
import "runtime"
type EZConfIntegration struct{}
func (e EZConfIntegration) Name() string {
return "myapp"
}
func (e EZConfIntegration) PackagePath() string {
_, filename, _, _ := runtime.Caller(0)
return filename[:len(filename)-len("/ezconf.go")]
}
func (e EZConfIntegration) ConfigFunc() func() (interface{}, error) {
return func() (interface{}, error) {
return ConfigFromEnv()
}
}
func NewEZConfIntegration() EZConfIntegration {
return EZConfIntegration{}
}
```
Then use it:
```go
loader.RegisterIntegration(myapp.NewEZConfIntegration())
```
### Loading Configurations
```go
// Load all registered configurations and parse packages
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get a specific configuration
cfg, ok := loader.GetConfig("hlog")
if !ok {
log.Fatal("hlog config not found")
}
// Type assert to the correct type
hlogCfg := cfg.(*hlog.Config)
```
### Getting All Configurations
```go
allConfigs := loader.GetAllConfigs()
for name, cfg := range allConfigs {
fmt.Printf("Config %s: %+v\n", name, cfg)
}
```
### Printing Environment Variables
```go
// Print variable names and descriptions (no values)
if err := loader.PrintEnvVarsStdout(false); err != nil {
log.Fatal(err)
}
// Output:
// LOG_LEVEL # Log level for the logger (default: info)
// LOG_OUTPUT # Output destination for logs (default: console)
// DATABASE_URL # Database connection string (required)
// Print with current values
if err := loader.PrintEnvVarsStdout(true); err != nil {
log.Fatal(err)
}
// Output:
// LOG_LEVEL=debug # Log level for the logger (default: info)
// LOG_OUTPUT=console # Output destination for logs (default: console)
// DATABASE_URL=postgres://localhost/db # Database connection string (required)
```
**Note:** `PrintEnvVarsStdout()` and `PrintEnvVars()` will return an error if called before `Load()`. Make sure to call `Load()` first.
### Writing to Custom Writer
```go
// Write to a file
file, _ := os.Create("env-vars.txt")
defer file.Close()
if err := loader.PrintEnvVars(file, true); err != nil {
log.Fatal(err)
}
// Write to a buffer
buf := &bytes.Buffer{}
if err := loader.PrintEnvVars(buf, false); err != nil {
log.Fatal(err)
}
```
### Generating .env Files
```go
// Generate new .env file with default values
err := loader.GenerateEnvFile(".env", false)
// Generate with current environment values
err := loader.GenerateEnvFile(".env.production", true)
// Generated file format:
// # Environment Variables Configuration
// # Generated by ezconf
//
// # Log level for the logger (default: info)
// LOG_LEVEL=info
//
// # Database connection string (required)
// DATABASE_URL=postgres://localhost/db
```
### Updating Existing .env Files
```go
// Update existing file, preserving existing values
err := loader.UpdateEnvFile(".env", false)
// Update or create if doesn't exist
err := loader.UpdateEnvFile(".env", true)
```
When updating:
- Existing variables keep their values
- New variables are added with default values
- Comments are updated to match current documentation
- Variables not in the config are preserved
### Getting Environment Variable Metadata
```go
envVars := loader.GetEnvVars()
for _, ev := range envVars {
fmt.Printf("Name: %s\n", ev.Name)
fmt.Printf("Description: %s\n", ev.Description)
fmt.Printf("Required: %t\n", ev.Required)
fmt.Printf("Default: %s\n", ev.Default)
fmt.Printf("Current: %s\n", ev.CurrentValue)
}
```
## ENV Comment Format
### Basic Format
```go
type Config struct {
// ENV LOG_LEVEL: Log level for the application
LogLevel string
}
```
### With Default Value
```go
type Config struct {
// ENV LOG_LEVEL: Log level for the application (default: info)
LogLevel string
}
```
### Required Variable
```go
type Config struct {
// ENV DATABASE_URL: Database connection string (required)
DatabaseURL string
}
```
### Conditional Requirement
```go
type Config struct {
// ENV LOG_DIR: Directory for log files (required when LOG_OUTPUT is file) (default: /var/log)
LogDir string
}
```
### Inline Comments
```go
type Config struct {
LogLevel string // ENV LOG_LEVEL: Log level (default: info)
Port int // ENV PORT: Server port (default: 8080)
}
```
### Multiple Modifiers
```go
type Config struct {
// ENV LOG_FILE_NAME: Name of the log file (required when LOG_OUTPUT is file or both) (default: app.log)
LogFileName string
}
```
## Integration
### With HLog
```go
loader := ezconf.New()
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")
loader.AddConfigFunc("hlog", func() (interface{}, error) {
return hlog.ConfigFromEnv()
})
loader.Load()
// Get hlog config
hlogCfg, _ := loader.GetConfig("hlog")
cfg := hlogCfg.(*hlog.Config)
// Create logger
logger, _ := hlog.NewLogger(cfg, os.Stdout)
```
### With HWS and HWSAuth
```go
loader := ezconf.New()
// Add all packages
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hws")
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hwsauth")
loader.AddPackagePath("vendor/git.haelnorr.com/h/golib/hlog")
// Add config functions
loader.AddConfigFunc("hws", func() (interface{}, error) {
return hws.ConfigFromEnv()
})
loader.AddConfigFunc("hwsauth", func() (interface{}, error) {
return hwsauth.ConfigFromEnv()
})
loader.AddConfigFunc("hlog", func() (interface{}, error) {
return hlog.ConfigFromEnv()
})
loader.Load()
// Get all configs
hwsCfg, _ := loader.GetConfig("hws")
authCfg, _ := loader.GetConfig("hwsauth")
hlogCfg, _ := loader.GetConfig("hlog")
```
### With Custom Config Structs
```go
package myapp
type Config struct {
// ENV MYAPP_NAME: Application name (default: myapp)
Name string
// ENV MYAPP_DEBUG: Enable debug mode (default: false)
Debug bool
// ENV MYAPP_API_KEY: API key for external service (required)
APIKey string
}
func ConfigFromEnv() (*Config, error) {
cfg := &Config{
Name: env.String("MYAPP_NAME", "myapp"),
Debug: env.Bool("MYAPP_DEBUG", false),
APIKey: env.String("MYAPP_API_KEY", ""),
}
if cfg.APIKey == "" {
return nil, errors.New("MYAPP_API_KEY is required")
}
return cfg, nil
}
// In your main application
loader := ezconf.New()
loader.AddPackagePath("./myapp")
loader.AddConfigFunc("myapp", func() (interface{}, error) {
return myapp.ConfigFromEnv()
})
```
## Best Practices
### 1. Use Consistent Comment Format
Always document your config struct fields with ENV comments:
```go
type Config struct {
// ENV VARNAME: Description (modifiers)
FieldName string
}
```
### 2. Group Related Configurations
Create a single Config struct per package rather than multiple structs:
```go
// Good
type Config struct {
Host string
Port int
TLS bool
}
// Avoid
type HostConfig struct {
Host string
}
type PortConfig struct {
Port int
}
```
### 3. Use Descriptive Names
Environment variable names should be clear and prefixed with the package/app name:
```go
// Good
// ENV MYAPP_DATABASE_URL: Database connection string
// Avoid
// ENV DB: Database
```
### 4. Document Requirements Clearly
Always indicate if a variable is required and under what conditions:
```go
// ENV LOG_DIR: Directory for log files (required when LOG_OUTPUT is file or both)
```
### 5. Provide Sensible Defaults
Include default values for optional configuration:
```go
// ENV LOG_LEVEL: Log level (default: info)
```
### 6. Generate .env Templates
Use ezconf to generate .env template files for your application:
```go
// In a separate tool or init command
loader := ezconf.New()
// Add all packages...
loader.Load()
loader.GenerateEnvFile(".env.example", false)
```
## Troubleshooting
### ENV Comments Not Being Parsed
**Problem:** Environment variables are not showing up after parsing.
**Solutions:**
- Ensure comments start with `ENV ` (with space after ENV)
- Check that the comment includes a colon after the variable name
- Verify the package path is correct and points to the actual source files
- Make sure files are not excluded (like test files)
### Config Function Fails to Load
**Problem:** `Load()` returns an error from a config function.
**Solutions:**
- Check that required environment variables are set
- Verify the ConfigFromEnv function handles errors properly
- Ensure the config function signature matches `func() (interface{}, error)`
### .env File Update Doesn't Work
**Problem:** UpdateEnvFile doesn't preserve existing values.
**Solutions:**
- Ensure the file exists or use `createIfNotExist: true`
- Check file permissions (needs write access)
- Verify the .env file format is correct (KEY=value lines)
### Missing Environment Variables in Output
**Problem:** Some expected variables don't appear in the output.
**Solutions:**
- Check if the struct field has an ENV comment
- Verify the package was added with `AddPackagePath`
- Ensure `Load()` was called before accessing env vars
- Check that the file isn't a test file (*_test.go)
## See Also
- [HLog](https://git.haelnorr.com/h/golib/wiki/HLog.md) - Structured logging with ConfigFromEnv
- [HWS](https://git.haelnorr.com/h/golib/wiki/HWS.md) - HTTP web server with ConfigFromEnv
- [HWSAuth](https://git.haelnorr.com/h/golib/wiki/HWSAuth.md) - Authentication with ConfigFromEnv
- [Env](https://git.haelnorr.com/h/golib/wiki/Env.md) - Environment variable helpers
## Links
- [GoDoc API Documentation](https://pkg.go.dev/git.haelnorr.com/h/golib/ezconf)
- [Source Code](https://git.haelnorr.com/h/golib/tree/master/ezconf)
- [Issue Tracker](https://git.haelnorr.com/h/golib/issues)

51
HLog.md

@@ -1,4 +1,4 @@
# HLog - v0.10.3
# HLog - v0.10.4
HLog is a structured logging package built on top of [zerolog](https://github.com/rs/zerolog), providing simple configuration, flexible output options, and automatic log file management for Go applications.
@@ -320,6 +320,55 @@ if logger.Debug().Enabled() {
HLog is designed to work seamlessly with other golib packages:
### With ezconf
HLog includes built-in integration with [ezconf](https://git.haelnorr.com/h/golib/ezconf) for unified configuration management. This is the recommended approach when using multiple golib packages:
```go
import (
"log"
"os"
"git.haelnorr.com/h/golib/ezconf"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
)
func main() {
// Create ezconf loader
loader := ezconf.New()
// Register hlog and other packages
loader.RegisterIntegrations(
hlog.NewEZConfIntegration(),
hws.NewEZConfIntegration(),
)
// Load all configurations
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get hlog configuration
hlogCfg, _ := loader.GetConfig("hlog")
cfg := hlogCfg.(*hlog.Config)
// Create logger
logger, _ := hlog.NewLogger(cfg, os.Stdout)
defer logger.CloseLogFile()
logger.Info().Msg("Application started")
}
```
Benefits of using ezconf:
- Unified configuration across multiple packages
- Automatic environment variable documentation
- Generate and manage .env files
- Print all configuration options
See the [ezconf documentation](EZConf.md) for more details.
### With env Package
HLog uses the [env](https://git.haelnorr.com/h/golib/env) package for environment variable parsing in `ConfigFromEnv()`. The env package provides type-safe environment variable access with defaults.

49
HWS.md

@@ -1,4 +1,4 @@
# HWS - v0.2.2
# HWS - v0.2.3
A lightweight, opinionated HTTP web server framework for Go built on top of the standard library's `net/http`.
@@ -1037,7 +1037,52 @@ func (e *ErrorPage) Render(ctx context.Context, w io.Writer) error {
}
```
## Integration with hwsauth
## Integration
### With ezconf
HWS includes built-in integration with [ezconf](https://git.haelnorr.com/h/golib/ezconf) for unified configuration management:
```go
import (
"git.haelnorr.com/h/golib/ezconf"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
)
func main() {
// Create ezconf loader
loader := ezconf.New()
// Register packages
loader.RegisterIntegrations(
hws.NewEZConfIntegration(),
hlog.NewEZConfIntegration(),
)
// Load all configurations
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get configurations
hwsCfg, _ := loader.GetConfig("hws")
cfg := hwsCfg.(*hws.Config)
// Create server with configuration
server := hws.NewServer(cfg)
server.Start()
}
```
Benefits of using ezconf:
- Unified configuration across multiple packages
- Automatic environment variable documentation
- Generate and manage .env files
See the [ezconf documentation](EZConf.md) for more details.
### With hwsauth
`hws` integrates seamlessly with `hwsauth` for JWT authentication:

@@ -1,4 +1,4 @@
# HWSAuth - v0.3.2
# HWSAuth - v0.3.4
JWT-based authentication middleware for the hws web framework.
@@ -751,6 +751,59 @@ func changePasswordHandler(
- Set `HWSAUTH_SECRET_KEY` environment variable
- Or provide in `Config` struct
## Integration
### With ezconf
HWSAuth includes built-in integration with [ezconf](https://git.haelnorr.com/h/golib/ezconf) for unified configuration management:
```go
import (
"git.haelnorr.com/h/golib/ezconf"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
)
func main() {
// Create ezconf loader
loader := ezconf.New()
// Register all packages
loader.RegisterIntegrations(
hws.NewEZConfIntegration(),
hlog.NewEZConfIntegration(),
hwsauth.NewEZConfIntegration(),
)
// Load all configurations
if err := loader.Load(); err != nil {
log.Fatal(err)
}
// Get configurations
authCfg, _ := loader.GetConfig("hwsauth")
cfg := authCfg.(*hwsauth.Config)
// Use configuration
auth, _ := hwsauth.NewAuthenticator[*User, bun.Tx](
cfg,
loadUserFunc,
server,
beginTxFunc,
logger,
errorPageFunc,
)
}
```
Benefits of using ezconf:
- Unified configuration across hws, hwsauth, hlog, and other packages
- Automatic environment variable documentation
- Generate and manage .env files for your entire application
See the [ezconf documentation](EZConf.md) for more details.
## See Also
- [HWS](./HWS.md) - H Web Server framework

@@ -16,6 +16,12 @@ JWT-based authentication middleware for the HWS web framework. Provides complete
### [JWT](JWT.md)
JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs.
### [EZConf](EZConf.md)
Unified configuration management system for loading and managing environment-based configurations across multiple packages. Provides source code parsing, .env file generation/updating, and environment variable documentation.
### [TMDB](TMDB.md)
Go client library for The Movie Database (TMDB) API. Features automatic rate limiting, retry logic with exponential backoff, movie search, detailed movie information, cast/crew data, and convenient image URL helpers.
## Installation
```bash

510
TMDB.md Normal file

@@ -0,0 +1,510 @@
# 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
```bash
go get git.haelnorr.com/h/golib/tmdb
```
## Configuration
### Environment Variables
```go
// 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
```go
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
```go
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
```go
// NewAPIConnection creates a new TMDB API connection
api, err := tmdb.NewAPIConnection()
if err != nil {
log.Fatal(err)
}
```
### Searching for Movies
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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:
```go
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
```go
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
```go
type SearchResponse struct {
Page int `json:"page"`
Results []Movie `json:"results"`
TotalPages int `json:"total_pages"`
TotalResults int `json:"total_results"`
}
```
### Credits
```go
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
```go
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:
```bash
# 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 ./...
```
## Related Packages
- [ezconf](EZConf.md) - Unified configuration management
- [hlog](HLog.md) - Structured logging
## External Resources
- [TMDB API Documentation](https://developer.themoviedb.org/docs)
- [Get API Token](https://www.themoviedb.org/settings/api)
- [TMDB Website](https://www.themoviedb.org/)
## License
MIT License - see LICENSE file for details