Add integration testing infrastructure with Podman Compose

- Add docker/podman-compose.yml for Immich server (no ML)
- Add test-data/sample-photos/ with 3 public domain images
- Add scripts/start-immich.sh for container startup + admin/API key creation
- Add scripts/seed-data.sh for uploading test photos and creating albums
- Add scripts/stop-immich.sh for cleanup with volume destruction
- Add scripts/run-example.sh wrapper (fixed env var export)
- Update examples to use IMMICH_URL and IMMICH_API_KEY env vars
- Add .github/workflows/integration-test.yml for CI
- Update assets.list() to use /search/metadata endpoint
- Update AGENTS.md with example running instructions
- Add 5 new examples: search_metadata, album_management, timeline_browsing,
  delete_assets, server_info
This commit is contained in:
Joakim Hulthe
2026-04-14 19:56:41 +00:00
parent c55d2b9080
commit 2e7db3b35a
22 changed files with 1327 additions and 22 deletions

View File

@@ -0,0 +1,133 @@
//! Example: Album management (CRUD operations)
//!
//! This example demonstrates creating, reading, updating, and deleting albums
//!
//! Environment variables:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use immich_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
println!("=== Album Management Example ===\n");
// List all albums
println!("1. Listing all albums...");
let albums = client.albums().list().execute().await?;
println!(" Found {} albums", albums.len());
for album in &albums {
println!(
" - {} (ID: {}, {} assets)",
album.album_name, album.id, album.asset_count
);
}
println!();
// Get details of the first album if one exists
let existing_album = albums.first().cloned();
if let Some(ref album) = existing_album {
println!("2. Getting album details...");
let details = client.albums().get(album.id).execute().await?;
println!(" Album: {}", details.album_name);
println!(" Description: {}", details.description);
println!(" Asset count: {}", details.asset_count);
println!();
}
// Create a new album
println!("3. Creating a new album...");
let new_album = client
.albums()
.create()
.name("SDK Test Album")
.execute()
.await?;
println!(
" Created album: {} (ID: {})",
new_album.album_name, new_album.id
);
println!();
// If we have assets, add some to the album
let asset_ids: Vec<_> = existing_album
.as_ref()
.map(|a| a.assets.iter().map(|asset| asset.id).collect())
.unwrap_or_default();
if !asset_ids.is_empty() {
println!("4. Adding assets to the new album...");
let asset_ids_to_add: Vec<_> = asset_ids.iter().take(2).copied().collect();
let num_added = asset_ids_to_add.len();
client
.albums()
.add_assets(new_album.id)
.asset_ids(asset_ids_to_add.clone())
.execute()
.await?;
println!(" Added {} assets to the album", num_added);
// Get updated album details
let updated_details = client.albums().get(new_album.id).execute().await?;
println!(" Album now has {} assets", updated_details.asset_count);
println!();
// Remove assets from the album
println!("5. Removing assets from the album...");
client
.albums()
.remove_assets(new_album.id)
.asset_ids(asset_ids_to_add)
.execute()
.await?;
println!(" Removed {} assets from the album", num_added);
let after_removal = client.albums().get(new_album.id).execute().await?;
println!(" Album now has {} assets", after_removal.asset_count);
println!();
} else {
println!("4. Skipping asset operations (no existing assets found)");
println!();
}
// Update the album
println!("6. Updating album name and description...");
let updated = client
.albums()
.update(new_album.id)
.name("Updated SDK Album")
.description("This description was updated by the SDK example")
.execute()
.await?;
println!(" Updated album name: {}", updated.album_name);
println!(" Updated description: {}", updated.description);
println!();
// Delete the album
println!("7. Deleting the album...");
client.albums().delete(new_album.id).execute().await?;
println!(" Album deleted successfully");
println!();
// Verify deletion by listing albums again
println!("8. Verifying deletion...");
let albums_after = client.albums().list().execute().await?;
let still_exists = albums_after.iter().any(|a| a.id == new_album.id);
if still_exists {
println!(" Warning: Album still exists after deletion attempt");
} else {
println!(" Confirmed: Album no longer exists");
}
println!("\n=== Example completed successfully ===");
Ok(())
}

View File

@@ -1,11 +1,20 @@
//! Basic usage example for immich-sdk
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use immich_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Create a client
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
let client = Client::from_url(&url)?.with_api_key(api_key);
// Get server version
let version = client.server().version().await?;

67
examples/delete_assets.rs Normal file
View File

@@ -0,0 +1,67 @@
//! Example: Delete assets
//!
//! This example demonstrates how to delete assets from Immich.
//!
//! WARNING: This will actually delete photos. Use with caution!
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
//!
//! IMPORTANT: Run this against a test instance only. This operation permanently
//! removes assets from the server.
use immich_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// List existing assets
let assets = client.assets().list().execute().await?;
println!("Found {} assets", assets.len());
if assets.is_empty() {
println!("No assets to delete. Run upload_photos first.");
return Ok(());
}
// Get the first asset details
let first_id = assets[0].id;
println!("Getting details for asset: {}", first_id);
let asset = client.assets().get(first_id).execute().await?;
println!("Asset: {} ({})", asset.original_file_name, asset.id);
// Delete a single asset
println!("\nDeleting single asset...");
client.assets().delete().id(first_id).execute().await?;
println!("Asset deleted successfully");
// Batch delete (if there are more assets)
if assets.len() > 1 {
println!("\nBatch deleting remaining assets...");
let ids_to_delete: Vec<_> = assets.iter().skip(1).take(2).map(|a| a.id).collect();
client
.assets()
.delete()
.ids(ids_to_delete)
.execute()
.await?;
println!("Batch delete completed");
}
// Verify deletion
let remaining = client.assets().list().execute().await?;
println!("\nRemaining assets: {}", remaining.len());
Ok(())
}

View File

@@ -1,15 +1,29 @@
//! Example: Download an asset from Immich
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use immich_sdk::Client;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Asset ID to download (replace with a real asset ID)
let asset_id = "your-asset-id-here".parse()?;
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// Get first asset from the server
let assets = client.assets().list().execute().await?;
if assets.is_empty() {
println!("No assets found. Run upload_photos example first.");
return Ok(());
}
let asset_id = assets[0].id;
// Download the asset
println!("Downloading asset...");

224
examples/search_metadata.rs Normal file
View File

@@ -0,0 +1,224 @@
//! Example: Search for assets using metadata filters
//!
//! This example demonstrates the powerful search capabilities of Immich
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use chrono::Duration;
use chrono::Utc;
use immich_sdk::Client;
use immich_sdk::models::AssetOrder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// Get total asset count
let all_results = client.search().metadata().size(1).execute().await?;
println!("Total assets in library: {}", all_results.assets.total);
println!();
// =================================================================
// Example 1: Search for favorite assets
// =================================================================
println!("=== Example 1: Search for favorites ===");
let favorites = client.search().metadata().favorite(true).execute().await?;
println!("Found {} favorite assets", favorites.assets.total);
if favorites.assets.items.is_empty() {
println!("No favorite assets found. Try marking some photos as favorites in Immich.");
} else {
for asset in favorites.assets.items.iter().take(3) {
println!(" - {}: {}", asset.id, asset.original_file_name);
}
}
println!();
// =================================================================
// Example 2: Search by asset type (images only)
// =================================================================
println!("=== Example 2: Search for images only ===");
let images = client
.search()
.metadata()
.asset_type(immich_sdk::models::AssetType::Image)
.size(5)
.execute()
.await?;
println!("Found {} image assets", images.assets.total);
for asset in &images.assets.items {
println!(" - {}", asset.original_file_name);
}
println!();
// =================================================================
// Example 3: Search by date range
// =================================================================
// Search for assets created in the last 30 days
println!("=== Example 3: Search by date range (last 30 days) ===");
let thirty_days_ago = Utc::now() - Duration::days(30);
let recent = client
.search()
.metadata()
.created_after(thirty_days_ago)
.execute()
.await?;
println!(
"Found {} assets created in the last 30 days",
recent.assets.total
);
if !recent.assets.items.is_empty() {
println!("Recent assets:");
for asset in recent.assets.items.iter().take(3) {
println!(
" - {} (created: {})",
asset.original_file_name,
asset.created_at.format("%Y-%m-%d")
);
}
}
println!();
// =================================================================
// Example 4: Search by city (if photos have location data)
// =================================================================
println!("=== Example 4: Search by city (location-based) ===");
let city_results = client
.search()
.metadata()
.city("New York")
.size(10)
.execute()
.await?;
if city_results.assets.total > 0 {
println!("Found {} assets from New York", city_results.assets.total);
for asset in &city_results.assets.items {
println!(" - {}", asset.original_file_name);
}
} else {
println!("No assets found from New York. Try searching for photos with GPS location data.");
}
println!();
// =================================================================
// Example 5: Combined filters (favorites + date range)
// =================================================================
println!("=== Example 5: Combined filters (favorites + recent) ===");
let recent_favorites = client
.search()
.metadata()
.favorite(true)
.created_after(thirty_days_ago)
.execute()
.await?;
println!(
"Found {} favorite assets from the last 30 days",
recent_favorites.assets.total
);
if !recent_favorites.assets.items.is_empty() {
for asset in &recent_favorites.assets.items {
println!(" - {}", asset.original_file_name);
}
}
println!();
// =================================================================
// Example 6: Search with pagination
// =================================================================
println!("=== Example 6: Pagination ===");
println!("Retrieving results with pagination (page 1, size 2)...");
let page1 = client
.search()
.metadata()
.page(1)
.size(2)
.order(AssetOrder::Desc)
.execute()
.await?;
println!(
"Page 1: {} of {} assets",
page1.assets.count, page1.assets.total
);
for asset in &page1.assets.items {
println!(" - {}", asset.original_file_name);
}
// Check if there are more pages
if page1.assets.next_page.is_some() {
println!("More results available on next page!");
// Fetch page 2
let page2 = client
.search()
.metadata()
.page(2)
.size(2)
.order(AssetOrder::Desc)
.execute()
.await?;
println!(
"\nPage 2: {} of {} assets",
page2.assets.count, page2.assets.total
);
for asset in &page2.assets.items {
println!(" - {}", asset.original_file_name);
}
} else if page1.assets.total > 0 {
println!("No more pages available.");
}
println!();
// =================================================================
// Example 7: Search with ordering
// =================================================================
println!("=== Example 7: Search with ordering (oldest first) ===");
let oldest = client
.search()
.metadata()
.order(AssetOrder::Asc)
.size(5)
.execute()
.await?;
println!("Oldest {} assets:", oldest.assets.items.len());
for asset in &oldest.assets.items {
println!(
" - {} (created: {})",
asset.original_file_name,
asset.created_at.format("%Y-%m-%d")
);
}
println!();
// =================================================================
// Summary
// =================================================================
println!("=== Search Summary ===");
println!("Demonstrated search filters:");
println!(" - Favorite status (is_favorite)");
println!(" - Asset type (IMAGE/VIDEO)");
println!(" - Date range (created_after, created_before)");
println!(" - Location (city, country)");
println!(" - Combined filters");
println!(" - Pagination (page, size)");
println!(" - Ordering (asc/desc)");
println!();
println!("Total assets in library: {}", all_results.assets.total);
Ok(())
}

43
examples/server_info.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Example: Server information
//!
//! This example demonstrates how to check server health and capabilities
use immich_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
let client = Client::from_url(&url)?.with_api_key(api_key);
// Health check (ping)
println!("Checking server health...");
match client.server().ping().await {
Ok(response) => println!("Server is healthy! Response: {}", response),
Err(e) => println!("Health check failed: {}", e),
}
// Get version
println!("\nServer Version:");
let version = client.server().version().await?;
println!(" Version: {}", version);
// Get features
println!("\nServer Features:");
let features = client.server().features().await?;
println!(" OAuth enabled: {}", features.oauth);
println!(" OAuth auto launch: {}", features.oauth_auto_launch);
println!(" Password login: {}", features.password_login);
println!(" Config file: {}", features.config_file);
// Get about info
println!("\nServer About:");
let about = client.server().about().await?;
println!(" Version: {}", about.version);
println!(" Version URL: {}", about.version_url);
Ok(())
}

View File

@@ -1,4 +1,8 @@
//! Example: Download asset thumbnails
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use image::GenericImageView;
use immich_sdk::Client;
@@ -7,11 +11,21 @@ use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Asset ID to download thumbnail for
let asset_id = "your-asset-id-here".parse()?;
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// Get first asset from the server
let assets = client.assets().list().execute().await?;
if assets.is_empty() {
println!("No assets found. Run upload_photos example first.");
return Ok(());
}
let asset_id = assets[0].id;
// Download thumbnail (small size, default)
println!("Downloading thumbnail...");

View File

@@ -0,0 +1,127 @@
//! Example: Timeline browsing
//!
//! This example demonstrates Immich's unique time-based asset organization.
//! Immich groups photos by date into "time buckets", allowing efficient
//! browsing of large photo collections chronologically.
//!
//! Environment variables:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
use immich_sdk::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// Get all time buckets
// Time buckets are date-based groupings (e.g., "2024-01-15")
let buckets = client.timeline().buckets().execute().await?;
println!("Found {} time buckets", buckets.len());
if buckets.is_empty() {
println!("No time buckets found. Upload some photos first!");
return Ok(());
}
// Show first few buckets
println!("\n--- First 5 Time Buckets ---");
for (i, bucket) in buckets.iter().take(5).enumerate() {
println!(
"Bucket {}: {} - {} assets",
i + 1,
bucket.time_bucket,
bucket.count
);
}
// Get assets from the first bucket
if let Some(first_bucket) = buckets.first() {
println!(
"\n--- Fetching assets from bucket: {} ---",
first_bucket.time_bucket
);
let assets = client
.timeline()
.bucket(&first_bucket.time_bucket)
.execute()
.await?;
println!("Found {} assets in this bucket", assets.id.len());
// The TimeBucketAssetResponse contains parallel arrays
// This is a memory-efficient design where each field is a column:
// - assets.id[i], assets.file_created_at[i], etc. all refer to the same asset
// - All arrays have the same length
println!("\n--- Sample assets from this bucket (showing up to 5) ---");
for i in 0..assets.id.len().min(5) {
println!(" Asset {}:", i + 1);
println!(" ID: {}", assets.id[i]);
println!(" Created: {}", assets.file_created_at[i]);
println!(" Is Image: {}", assets.is_image[i]);
println!(" Is Favorite: {}", assets.is_favorite[i]);
println!(" Owner ID: {}", assets.owner_id[i]);
// Optional fields (may be None)
if let Some(ref durations) = assets.duration {
if let Some(duration) = &durations[i] {
println!(" Duration: {}", duration);
}
}
if let Some(ref cities) = assets.city {
if let Some(city) = &cities[i] {
println!(" City: {}", city);
}
}
if let Some(ref countries) = assets.country {
if let Some(country) = &countries[i] {
println!(" Country: {}", country);
}
}
}
// Demonstrate iterating through parallel arrays
println!("\n--- Understanding the parallel array structure ---");
println!("The response contains these parallel arrays (all same length):");
println!(" - id.len() = {}", assets.id.len());
println!(
" - file_created_at.len() = {}",
assets.file_created_at.len()
);
println!(" - is_image.len() = {}", assets.is_image.len());
println!(" - is_favorite.len() = {}", assets.is_favorite.len());
println!(" - owner_id.len() = {}", assets.owner_id.len());
println!("\nIndex 'i' corresponds to the same asset across all arrays.");
println!("This columnar structure is memory-efficient for large collections.");
}
// Demonstrate filtering with the timeline API
println!("\n--- Timeline API supports filtering ---");
println!("You can filter time buckets with:");
println!(" - album_id: Filter by specific album");
println!(" - is_favorite: Show only favorited assets");
println!(" - is_trashed: Include/exclude trashed assets");
println!(" - visibility: Timeline, Archive, Hidden, or Locked");
println!(" - with_partners: Include assets shared by partners");
// Example: Get only favorite buckets
let favorite_buckets = client
.timeline()
.buckets()
.is_favorite(true)
.execute()
.await?;
println!(
"\nFound {} time buckets with favorite assets",
favorite_buckets.len()
);
Ok(())
}

View File

@@ -1,15 +1,38 @@
//! Example: Upload photos to Immich
//!
//! This example uses environment variables for configuration:
//! - IMMICH_URL: The Immich server URL (defaults to http://localhost:2283)
//! - IMMICH_API_KEY: Your API key (required)
//!
//! It also reads photos from test-data/sample-photos/ directory.
use immich_sdk::Client;
use std::path::Path;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
// Configure the client from environment variables
let url = std::env::var("IMMICH_URL").unwrap_or_else(|_| "http://localhost:2283".to_string());
let api_key =
std::env::var("IMMICH_API_KEY").expect("IMMICH_API_KEY environment variable not set");
// Path to the photo
let photo_path = Path::new("/path/to/your/photo.jpg");
// Create a client
let client = Client::from_url(&url)?.with_api_key(api_key);
// Use test data photos
let photo_dir = std::path::Path::new("test-data/sample-photos");
let photos: Vec<_> = std::fs::read_dir(photo_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map(|e| e == "jpg").unwrap_or(false))
.collect();
if photos.is_empty() {
println!("No photos found in test-data/sample-photos/");
return Ok(());
}
// Upload first photo
let photo_path = &photos[0];
// Upload the photo
println!("Uploading: {}", photo_path.display());