A Ruby client library for the Pug Video API - a modern video management platform for creating, managing, and delivering video content at scale.
This gem provides an intuitive, object-oriented interface for working with video resources, livestreams, namespaces, campaigns, and more. It handles authentication, pagination, and API communication so you can focus on building great video applications.
- Manage video resources (upload, update, delete, retrieve)
- Create and manage livestreams and campaigns
- Organize content with namespaces
- Generate signed upload URLs
- Execute video commands (clipping, processing, etc.)
- Track video metadata and labels
Get up and running in 30 seconds:
# 1. Install the gem
gem install pug-client
# 2. Set your credentials and namespace
export PUG_CLIENT_ID=your_client_id
export PUG_CLIENT_SECRET=your_client_secret
export PUG_NAMESPACE=my-videos
# 3. Use the client
require 'pug_client'
# Namespace is REQUIRED during initialization
client = PugClient::Client.new(
namespace: ENV['PUG_NAMESPACE']
)
client.authenticate!
# Access your configured namespace
namespace = client.namespace
puts "Namespace: #{namespace.id}"
# Iterate videos lazily (uses default namespace)
client.videos.each do |video|
puts "Video #{video.id}: #{video.metadata[:title]}"
end
# Get a specific video and update it
video = client.video('video-123')
video.metadata[:labels][:status] = 'ready'
video.metadata[:labels][:featured] = true
video.save # Auto-generates JSON Patch- Installation
- API Compatibility
- Basic Usage
- Authentication
- Working with Resources
- Resource API Features
- Configuration
- Advanced Usage
- Troubleshooting
Add this line to your application's Gemfile:
gem 'pug-client'Or install it yourself:
gem install pug-clientThis client library is designed for and tested against Pug Video API version 0.4.0.
PugClient::API_VERSION # => "0.4.0"The library includes comprehensive integration tests recorded against the staging API using VCR cassettes. These tests validate that the client works correctly with the specified API version and serve as both verification and documentation of the expected API behavior.
Testing Against Specific API Versions:
Integration tests are organized by API version in spec/integration/api_v0.4.0/. When the API is updated, we maintain separate test suites and VCR recordings for each version to ensure compatibility and catch breaking changes.
For more information about API versions and compatibility, see the Pug Video API documentation.
Here's a complete example showing the resource-based API:
require 'pug_client'
# 1. Create a client with namespace (REQUIRED)
client = PugClient::Client.new(
namespace: 'my-videos',
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET']
)
# 2. Authenticate (gets an OAuth access token)
client.authenticate!
# 3. Work with your configured namespace
namespace = client.namespace # Uses default namespace
puts "Namespace: #{namespace.id}"
puts "Created at: #{namespace.created_at}"
# Or access a different namespace
other_namespace = client.namespace('other-videos')
# 4. Work with videos (uses default namespace)
video = client.video('video-123')
puts "Video: #{video.id}"
# Or override namespace for specific calls
video = client.video('video-456', namespace: 'other-videos')
# 5. Update video naturally with dirty tracking
video.metadata[:labels][:status] = 'processed'
video.metadata[:labels][:featured] = true
video.save # Automatically generates JSON Patch operations
# 6. Create a clip from video
clip = video.clip(start_time: 5000, duration: 30000,
metadata: { labels: { type: 'highlight' } }
)
puts "Created clip: #{clip.id}"
# 7. Upload a file (MP4 only currently)
File.open('video.mp4', 'rb') do |file|
video.upload(file, filename: 'video.mp4')
end
video.wait_until_ready(timeout: 600)
puts "Video ready: #{video.playback_urls}"
# 8. Iterate videos lazily (fetches pages on-demand, uses default namespace)
client.videos.each do |video|
puts "Video: #{video.id}"
end
# Or get first N videos
recent_videos = client.videos.first(10)
# Override namespace for listings
other_videos = client.videos(namespace: 'other-videos').first(10)
# 9. Delete video
video.deleteThe client uses Auth0's client credentials flow for machine-to-machine authentication. You'll need API credentials (client ID and secret) from your Pug account.
1. Environment Variables (Recommended for Production)
# Set in your environment:
# export PUG_CLIENT_ID=your_client_id
# export PUG_CLIENT_SECRET=your_client_secret
# export PUG_NAMESPACE=my-videos
client = PugClient::Client.new(
namespace: ENV['PUG_NAMESPACE'] # Required
)
client.authenticate!2. Pass Directly (Good for Testing)
client = PugClient::Client.new(
namespace: 'my-videos', # Required
client_id: 'your_client_id',
client_secret: 'your_client_secret'
)
client.authenticate!3. Global Configuration (For Singleton Pattern)
PugClient.configure do |c|
c.namespace = 'my-videos' # Required
c.client_id = 'your_client_id'
c.client_secret = 'your_client_secret'
end
# Now use module-level methods
PugClient.authenticate!
namespace = PugClient.namespace # Uses configured namespace# Check if authenticated
client.authenticated? # => true/false
# Check if token is expired
client.token_expired? # => true/false
# Auto-refresh token if needed (recommended before API calls)
client.ensure_authenticated!
# Manually refresh token
client.authenticate!Namespaces help organize your video content. Think of them as folders or projects. The resource-based API makes working with namespaces intuitive and object-oriented.
Important: A namespace ID is required when creating a client and serves as the default for all operations. You'll use an existing namespace that's been set up for your account.
# Create client with default namespace (REQUIRED - use existing namespace ID)
client = PugClient::Client.new(
namespace: 'my-videos', # Your existing namespace ID
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET']
)
# Get your configured namespace (no ID needed)
namespace = client.namespace
puts namespace.id # => "my-videos"
puts namespace.metadata
# Get a different namespace by ID
other = client.namespace('other-videos')
# Update namespace metadata (with automatic dirty tracking)
namespace.metadata[:labels][:status] = 'active'
namespace.metadata[:annotations] = { project: 'v2' }
namespace.save # Generates and sends JSON Patch automatically
# Reload from API (discards unsaved changes)
namespace.reload
puts namespace.metadata
# List all namespaces you have access to (returns lazy enumerator)
client.namespaces.each do |ns|
puts "Namespace: #{ns.id}"
end
# List user's namespaces
client.user_namespaces.first(10)Videos are the core resource. Each video belongs to a namespace and uses the powerful resource-based API for natural, Ruby-idiomatic operations.
Two ways to work with videos:
- Through the client (uses configured default namespace)
- Through a namespace object (uses that namespace's ID)
# Setup: Client with default namespace
client = PugClient::Client.new(
namespace: 'my-videos',
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET']
)
# Way 1: Use client directly (uses default namespace 'my-videos')
video = client.video('video-123')
puts video.id
# Override namespace for specific calls
video = client.video('video-456', namespace: 'other-videos')
# Way 2: Use namespace object
namespace = client.namespace
video = namespace.video('video-123') # Same as client.video('video-123')
# Update video metadata (automatic dirty tracking + JSON Patch)
video.metadata[:labels][:status] = 'processed'
video.metadata[:labels][:featured] = true
video.save # Auto-generates: [
# {op: 'add', path: '/attributes/metadata/labels/status', value: 'processed'},
# {op: 'add', path: '/attributes/metadata/labels/featured', value: true}
# ]
# Upload a video file (MP4 only currently)
File.open('video.mp4', 'rb') do |file|
video.upload(file, filename: 'video.mp4')
end
# Wait for video processing to complete
video.wait_until_ready(timeout: 600, interval: 5)
puts "Video ready! Playback URLs: #{video.playback_urls}"
# Create a clip from a video (returns NEW video resource)
clip = video.clip(
start_time: 10000, # Start at 10 seconds (milliseconds)
duration: 30000, # 30 second clip
metadata: { labels: { type: 'highlight' } }
)
puts "Created clip: #{clip.id}"
# You can work with the clip like any other video
clip.metadata[:labels][:featured] = true
clip.save
# Get the parent namespace of a video
parent = video.namespace
puts "Video belongs to: #{parent.id}"
# Iterate videos (both approaches work)
client.videos.each do |video| # Uses default namespace
puts "Video #{video.id}: #{video.started_at}"
end
namespace.videos.each do |video| # Uses namespace's ID
puts "Video #{video.id}: #{video.started_at}"
end
# Override namespace for listings
client.videos(namespace: 'other-videos').each { |v| puts v.id }
# Get first N videos efficiently
recent_videos = client.videos.first(10)
# Force eager loading if needed
all_videos = client.videos.to_a
# Filter videos (still lazy)
ready_videos = client.videos.select { |v| v.status == 'ready' }
# Delete a video
video.delete
# Reload video from API (discards local changes)
video.reloadSome attributes cannot be modified after creation:
Namespace: id, created_at, updated_at
Video: id, created_at, updated_at, duration, renditions, playback_urls, thumbnail_url
video.id = 'new-id' # Raises ValidationError
video.duration = 90000 # Raises ValidationError
# But you can read them
puts video.duration
puts video.playback_urlsResource collections use Ruby's Enumerator pattern for efficient, on-demand pagination:
# Fetches pages as you iterate (memory efficient for large datasets)
namespace.videos.each do |video|
puts video.id
break if video.id == 'target-id' # Stops fetching
end
# Get first N (only fetches enough pages)
recent = namespace.videos.first(10)
# Full Enumerable interface
ids = namespace.videos.map(&:id)
featured = namespace.videos.select { |v| v.metadata[:labels][:featured] }
count = namespace.videos.count # Note: fetches all pages
# Force eager loading when needed
all_videos = namespace.videos.to_aResources automatically track changes and generate JSON Patch operations:
video = client.video('my-namespace', 'video-123')
# Make multiple changes
video.metadata[:labels][:status] = 'ready'
video.metadata[:labels][:reviewed] = true
video.metadata[:annotations] = { reviewer: 'john@example.com' }
# Check if changed
video.changed? # => true
# Save sends JSON Patch automatically
video.save # Generates:
# [
# {op: 'add', path: '/attributes/metadata/labels/status', value: 'ready'},
# {op: 'add', path: '/attributes/metadata/labels/reviewed', value: true},
# {op: 'add', path: '/attributes/metadata/annotations', value: {...}}
# ]
# After save, no longer dirty
video.changed? # => false
# Reload discards unsaved changes
video.metadata[:labels][:new] = 'value'
video.reload # Local changes discarded, fresh from APIThe API uses camelCase, but Ruby code uses snake_case:
# API returns: { data: { attributes: { startedAt: '...' } } }
# Ruby sees:
video.started_at # Automatic translation
# When you update:
video.metadata[:created_by] = 'user@example.com'
# Sent to API as: { createdBy: 'user@example.com' }Pug provides preset configurations for production and staging environments.
Production (Default)
client = PugClient::Client.new(
namespace: ENV['PUG_NAMESPACE'], # Required
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET']
# Uses production endpoints automatically
)Staging
# Option 1: Pass environment parameter
client = PugClient::Client.new(
environment: :staging,
namespace: ENV['PUG_NAMESPACE'], # Required
client_id: ENV['PUG_STAGING_CLIENT_ID'],
client_secret: ENV['PUG_STAGING_CLIENT_SECRET']
)
# Option 2: Use global configuration
PugClient.use_staging!
PugClient.configure do |c|
c.namespace = ENV['PUG_NAMESPACE'] # Required
c.client_id = ENV['PUG_STAGING_CLIENT_ID']
c.client_secret = ENV['PUG_STAGING_CLIENT_SECRET']
endCustom/Local Development
For custom environments (local development, custom deployments), pass all endpoints explicitly:
client = PugClient::Client.new(
namespace: 'test-namespace', # Required
api_endpoint: 'http://localhost:3000',
auth_endpoint: 'http://localhost:3001/oauth/token',
auth_audience: 'http://localhost:3000/',
auth_grant_type: 'client_credentials',
client_id: 'local_client_id',
client_secret: 'local_client_secret'
)| Setting | Production | Staging |
|---|---|---|
| API Endpoint | https://api.video.scorevision.com |
https://staging-api.video.scorevision.com |
| Auth Endpoint | https://fantagio.auth0.com/oauth/token |
https://fantagio-staging.auth0.com/oauth/token |
| Auth Audience | https://api.fantag.io/ |
https://staging-api.fantag.io/ |
All available configuration options:
PugClient::Client.new(
# Namespace (REQUIRED)
namespace: 'my-videos', # Default namespace for all operations (required)
# Environment (preset configurations)
environment: :production, # :production (default) or :staging
# Authentication (required)
client_id: 'your_client_id', # OAuth client ID
client_secret: 'your_client_secret', # OAuth client secret
access_token: 'manual_token', # Optional: skip auth flow with existing token
# Endpoints (auto-set by environment, or specify manually)
api_endpoint: 'https://...', # API base URL
auth_endpoint: 'https://...', # Auth0 token endpoint
auth_audience: 'https://...', # Auth0 audience
auth_grant_type: 'client_credentials', # OAuth grant type
# Pagination
per_page: 10, # Default page size (default: 10)
# HTTP Configuration
connection_options: { # Faraday options
request: {
open_timeout: 5, # Connection timeout
timeout: 10 # Read timeout
}
}
)Use the module-level API for a singleton-style interface:
# Configure once (namespace is required)
PugClient.configure do |c|
c.namespace = ENV['PUG_NAMESPACE'] # Required
c.client_id = ENV['PUG_CLIENT_ID']
c.client_secret = ENV['PUG_CLIENT_SECRET']
end
# Use anywhere
PugClient.authenticate!
# Access configured namespace
namespace = PugClient.namespace
videos = namespace.videos.to_a
# Or use client methods directly (uses configured namespace)
videos = PugClient.videos.to_a
video = PugClient.video('video-123')The gem provides specific error classes for better error handling:
begin
video = client.video('non-existent')
rescue PugClient::ResourceNotFound => e
puts "Video not found: #{e.resource_type} #{e.id}"
rescue PugClient::NetworkError => e
puts "Network error: #{e.message}"
rescue PugClient::AuthenticationError => e
puts "Auth error: #{e.message}"
end
# Namespace requirement validation
begin
client = PugClient::Client.new(
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET']
# Missing required namespace parameter
)
rescue ArgumentError => e
puts e.message # => "namespace is required"
end
# Upload validation
begin
video.upload(file, filename: 'video.avi', content_type: 'video/avi')
rescue PugClient::ValidationError => e
puts "Invalid upload: #{e.message}"
# => "Unsupported content type: video/avi. Currently only video/mp4 is supported."
end
# Wait timeout
begin
video.wait_until_ready(timeout: 60)
rescue PugClient::TimeoutError => e
puts "Video processing took too long: #{e.message}"
endFor resources not yet supported by dedicated resource classes (livestreams, campaigns, etc.), use the low-level HTTP interface:
# GET request
response = client.get('livestreams')
# POST request (with JSON:API formatted body)
response = client.post('livestreams', {
data: {
type: 'livestreams',
attributes: {
title: 'My Livestream'
}
}
})
# PATCH request (JSON Patch format)
response = client.patch("livestreams/123", [
{ op: 'replace', path: '/attributes/title', value: 'Updated Title' }
])
# DELETE request
client.delete("livestreams/123")
# Access response details
puts client.last_response.status # HTTP status code
puts client.last_response.headers # Response headersProblem: PugClient::AuthenticationError: Authentication failed
Solutions:
- Verify your
client_idandclient_secretare correct - Check that you're using the right environment (production vs staging credentials)
- Ensure your credentials haven't expired or been revoked
- Try authenticating manually:
client.authenticate!
# Debug authentication
begin
client.authenticate!
puts "Authenticated successfully!"
rescue PugClient::AuthenticationError => e
puts "Auth failed: #{e.message}"
puts "Using client_id: #{client.client_id}"
puts "Auth endpoint: #{client.auth_endpoint}"
endProblem: Getting 401 errors after initial authentication
Solution: Tokens expire after a certain time. Use ensure_authenticated! before API calls:
# Automatically refresh if expired
client.ensure_authenticated!
namespace = client.namespace('my-videos')
# Or check manually
if client.token_expired?
client.authenticate!
endProblem: PugClient::ResourceNotFound error
Solutions:
- Verify the resource exists:
client.videos.each { |v| puts v.id } - Check for typos in the resource ID
- Ensure you have permission to access the resource
- Verify you're using the correct environment (production vs staging)
- Confirm you're using the correct namespace:
client.namespace.id
Problem: Upload fails or content type rejected
Solutions:
- Only MP4 format is currently supported:
content_type: 'video/mp4' - Ensure file is readable:
File.open('video.mp4', 'rb') - Check file size and timeout settings
- Verify video processing with
wait_until_ready
Problem: Requests timing out
Solution: Increase timeout values:
client = PugClient::Client.new(
namespace: ENV['PUG_NAMESPACE'],
client_id: ENV['PUG_CLIENT_ID'],
client_secret: ENV['PUG_CLIENT_SECRET'],
connection_options: {
request: {
open_timeout: 10, # Increase from default
timeout: 30 # Increase from default
}
}
)This gem uses a resource-based API design for core resources (Namespace, Video), providing:
- Object-oriented interface - Resources are first-class Ruby objects
- Lazy enumeration - Efficient iteration over large collections
- Automatic dirty tracking - Changes tracked and converted to JSON Patch
- Idiomatic Ruby - snake_case attributes, natural mutations, Enumerable support
Other resources (livestreams, campaigns, webhooks, etc.) currently use the client-centric API. These will be migrated to the resource-based pattern in future versions.
- Ruby >= 3.4
- Faraday >= 2.14
- Sawyer ~> 0.9
Contributions are welcome! If you want to contribute to this gem, please see DEVELOPING.md for development setup, architecture details, and guidelines for adding new features.
This project is licensed under the MIT License - see the gemspec for details.
- Homepage: http://git.scorevision.com/fantag/pug-client-ruby
- Issues: http://git.scorevision.com/fantag/pug-client-ruby/issues
- Development Guide: DEVELOPING.md