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 gemgeminstallpug-client# 2. Set your credentials and namespaceexportPUG_CLIENT_ID=your_client_idexportPUG_CLIENT_SECRET=your_client_secretexportPUG_NAMESPACE=my-videos# 3. Use the clientrequire'pug_client'# Namespace is REQUIRED during initializationclient=PugClient::Client.new(namespace: ENV['PUG_NAMESPACE'])client.authenticate!# Access your configured namespacenamespace=client.namespaceputs"Namespace: #{namespace.id}"# Iterate videos lazily (uses default namespace)client.videos.eachdo |video|
puts"Video #{video.id}: #{video.metadata[:title]}"end# Get a specific video and update itvideo=client.video('video-123')video.metadata[:labels][:status]='ready'video.metadata[:labels][:featured]=truevideo.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 namespacenamespace=client.namespace# Uses default namespaceputs"Namespace: #{namespace.id}"puts"Created at: #{namespace.created_at}"# Or access a different namespaceother_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 callsvideo=client.video('video-456',namespace: 'other-videos')# 5. Update video naturally with dirty trackingvideo.metadata[:labels][:status]='processed'video.metadata[:labels][:featured]=truevideo.save# Automatically generates JSON Patch operations# 6. Create a clip from videoclip=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')endvideo.wait_until_ready(timeout: 600)puts"Video ready: #{video.playback_urls}"# 8. Iterate videos lazily (fetches pages on-demand, uses default namespace)client.videos.eachdo |video|
puts"Video: #{video.id}"end# Or get first N videosrecent_videos=client.videos.first(10)# Override namespace for listingsother_videos=client.videos(namespace: 'other-videos').first(10)# 9. Delete videovideo.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-videosclient=PugClient::Client.new(namespace: ENV['PUG_NAMESPACE']# Required)client.authenticate!2. Pass Directly (Good for Testing)
client=PugClient::Client.new(namespace: 'my-videos',# Requiredclient_id: 'your_client_id',client_secret: 'your_client_secret')client.authenticate!3. Global Configuration (For Singleton Pattern)
PugClient.configuredo |c|
c.namespace='my-videos'# Requiredc.client_id='your_client_id'c.client_secret='your_client_secret'end# Now use module-level methodsPugClient.authenticate!namespace=PugClient.namespace# Uses configured namespace# Check if authenticatedclient.authenticated?# => true/false# Check if token is expiredclient.token_expired?# => true/false# Auto-refresh token if needed (recommended before API calls)client.ensure_authenticated!# Manually refresh tokenclient.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 IDclient_id: ENV['PUG_CLIENT_ID'],client_secret: ENV['PUG_CLIENT_SECRET'])# Get your configured namespace (no ID needed)namespace=client.namespaceputsnamespace.id# => "my-videos"putsnamespace.metadata# Get a different namespace by IDother=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.reloadputsnamespace.metadata# List all namespaces you have access to (returns lazy enumerator)client.namespaces.eachdo |ns|
puts"Namespace: #{ns.id}"end# List user's namespacesclient.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 namespaceclient=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')putsvideo.id# Override namespace for specific callsvideo=client.video('video-456',namespace: 'other-videos')# Way 2: Use namespace objectnamespace=client.namespacevideo=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]=truevideo.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 completevideo.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 clipmetadata: {labels: {type: 'highlight'}})puts"Created clip: #{clip.id}"# You can work with the clip like any other videoclip.metadata[:labels][:featured]=trueclip.save# Get the parent namespace of a videoparent=video.namespaceputs"Video belongs to: #{parent.id}"# Iterate videos (both approaches work)client.videos.eachdo |video| # Uses default namespaceputs"Video #{video.id}: #{video.started_at}"endnamespace.videos.eachdo |video| # Uses namespace's IDputs"Video #{video.id}: #{video.started_at}"end# Override namespace for listingsclient.videos(namespace: 'other-videos').each{ |v| putsv.id}# Get first N videos efficientlyrecent_videos=client.videos.first(10)# Force eager loading if neededall_videos=client.videos.to_a# Filter videos (still lazy)ready_videos=client.videos.select{ |v| v.status == 'ready'}# Delete a videovideo.delete# Reload video from API (discards local changes)video.reloadSome attributes cannot be modified after creation:
Namespace:id, created_at, updated_atVideo:id, created_at, updated_at, duration, renditions, playback_urls, thumbnail_url
video.id='new-id'# Raises ValidationErrorvideo.duration=90000# Raises ValidationError# But you can read themputsvideo.durationputsvideo.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.eachdo |video|
putsvideo.idbreakifvideo.id == 'target-id'# Stops fetchingend# Get first N (only fetches enough pages)recent=namespace.videos.first(10)# Full Enumerable interfaceids=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 neededall_videos=namespace.videos.to_aResources automatically track changes and generate JSON Patch operations:
video=client.video('my-namespace','video-123')# Make multiple changesvideo.metadata[:labels][:status]='ready'video.metadata[:labels][:reviewed]=truevideo.metadata[:annotations]={reviewer: 'john@example.com'}# Check if changedvideo.changed?# => true# Save sends JSON Patch automaticallyvideo.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 dirtyvideo.changed?# => false# Reload discards unsaved changesvideo.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'],# Requiredclient_id: ENV['PUG_CLIENT_ID'],client_secret: ENV['PUG_CLIENT_SECRET']# Uses production endpoints automatically)Staging
# Option 1: Pass environment parameterclient=PugClient::Client.new(environment: :staging,namespace: ENV['PUG_NAMESPACE'],# Requiredclient_id: ENV['PUG_STAGING_CLIENT_ID'],client_secret: ENV['PUG_STAGING_CLIENT_SECRET'])# Option 2: Use global configurationPugClient.use_staging!PugClient.configuredo |c|
c.namespace=ENV['PUG_NAMESPACE']# Requiredc.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',# Requiredapi_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 IDclient_secret: 'your_client_secret',# OAuth client secretaccess_token: 'manual_token',# Optional: skip auth flow with existing token# Endpoints (auto-set by environment, or specify manually)api_endpoint: 'https://...',# API base URLauth_endpoint: 'https://...',# Auth0 token endpointauth_audience: 'https://...',# Auth0 audienceauth_grant_type: 'client_credentials',# OAuth grant type# Paginationper_page: 10,# Default page size (default: 10)# HTTP Configurationconnection_options: {# Faraday optionsrequest: {open_timeout: 5,# Connection timeouttimeout: 10# Read timeout}})Use the module-level API for a singleton-style interface:
# Configure once (namespace is required)PugClient.configuredo |c|
c.namespace=ENV['PUG_NAMESPACE']# Requiredc.client_id=ENV['PUG_CLIENT_ID']c.client_secret=ENV['PUG_CLIENT_SECRET']end# Use anywherePugClient.authenticate!# Access configured namespacenamespace=PugClient.namespacevideos=namespace.videos.to_a# Or use client methods directly (uses configured namespace)videos=PugClient.videos.to_avideo=PugClient.video('video-123')The gem provides specific error classes for better error handling:
beginvideo=client.video('non-existent')rescuePugClient::ResourceNotFound=>eputs"Video not found: #{e.resource_type}#{e.id}"rescuePugClient::NetworkError=>eputs"Network error: #{e.message}"rescuePugClient::AuthenticationError=>eputs"Auth error: #{e.message}"end# Namespace requirement validationbeginclient=PugClient::Client.new(client_id: ENV['PUG_CLIENT_ID'],client_secret: ENV['PUG_CLIENT_SECRET']# Missing required namespace parameter)rescueArgumentError=>eputse.message# => "namespace is required"end# Upload validationbeginvideo.upload(file,filename: 'video.avi',content_type: 'video/avi')rescuePugClient::ValidationError=>eputs"Invalid upload: #{e.message}"# => "Unsupported content type: video/avi. Currently only video/mp4 is supported."end# Wait timeoutbeginvideo.wait_until_ready(timeout: 60)rescuePugClient::TimeoutError=>eputs"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 requestresponse=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 requestclient.delete("livestreams/123")# Access response detailsputsclient.last_response.status# HTTP status codeputsclient.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 authenticationbeginclient.authenticate!puts"Authenticated successfully!"rescuePugClient::AuthenticationError=>eputs"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 expiredclient.ensure_authenticated!namespace=client.namespace('my-videos')# Or check manuallyifclient.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 defaulttimeout: 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