Compare commits
8 Commits
17c540a7d4
...
feat/auth
| Author | SHA1 | Date | |
|---|---|---|---|
| 075bc855fd | |||
|
|
17009cae28 | ||
|
|
c39e0a5058 | ||
|
|
2e7db3b35a | ||
|
|
c55d2b9080 | ||
|
|
3284a18dcb | ||
|
|
6820dd765d | ||
|
|
0d8042287c |
51
.github/workflows/integration-test.yml
vendored
Normal file
51
.github/workflows/integration-test.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Integration Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-action@stable
|
||||||
|
|
||||||
|
- name: Install Podman
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y podman
|
||||||
|
sudo systemctl enable --now podman.socket
|
||||||
|
|
||||||
|
- name: Install podman-compose
|
||||||
|
run: |
|
||||||
|
sudo pip3 install podman-compose
|
||||||
|
|
||||||
|
- name: Start Immich
|
||||||
|
run: |
|
||||||
|
./scripts/start-immich.sh
|
||||||
|
|
||||||
|
- name: Seed test data
|
||||||
|
run: |
|
||||||
|
./scripts/seed-data.sh
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
source .env.test
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
- name: Run examples
|
||||||
|
run: |
|
||||||
|
source .env.test
|
||||||
|
cargo run --example basic_usage
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
./scripts/stop-immich.sh
|
||||||
140
AGENTS.md
Normal file
140
AGENTS.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Agent Instructions for immich-sdk
|
||||||
|
|
||||||
|
## Build & Test Commands
|
||||||
|
|
||||||
|
**Quick Checks:**
|
||||||
|
```bash
|
||||||
|
cargo check # Fast compilation check
|
||||||
|
cargo clippy # Run linter
|
||||||
|
cargo fmt # Format code
|
||||||
|
cargo test # Run all tests
|
||||||
|
cargo test <test_name> # Run single test (e.g., cargo test test_client_creation)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full verification before commit:**
|
||||||
|
```bash
|
||||||
|
cargo check && cargo clippy && cargo test && cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Examples
|
||||||
|
|
||||||
|
All examples should be run using the provided wrapper script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/run-example.sh <example_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper handles everything automatically:
|
||||||
|
- Starts Immich containers (if not running)
|
||||||
|
- Creates admin user and API key
|
||||||
|
- Seeds test data (photos, albums)
|
||||||
|
- Loads credentials
|
||||||
|
- Runs the example
|
||||||
|
|
||||||
|
**When adding new examples:**
|
||||||
|
1. Read `IMMICH_URL` and `IMMICH_API_KEY` from environment variables
|
||||||
|
2. Test with: `./scripts/run-example.sh <name>` (Important!)
|
||||||
|
3. Verify it works with the seeded test data (3 photos, 1 album)
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- Group by: std, external crates, internal modules (crate::)
|
||||||
|
- Use `use crate::` for internal modules, not relative paths
|
||||||
|
- Example:
|
||||||
|
```rust
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Client,
|
||||||
|
error::{ImmichError, Result},
|
||||||
|
models::AssetId,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types & Naming
|
||||||
|
- Prefer using newtypes or type aliases instead of Strings or integers
|
||||||
|
For example: `AssetId` (Uuid alias), not `String` for asset IDs
|
||||||
|
- Model types: PascalCase with `Response` or `Request` suffix
|
||||||
|
- Enum variants: Use serde `rename_all` attributes for API compatibility
|
||||||
|
- Builders: `FooBuilder` for constructing `Foo` operations
|
||||||
|
- Type aliases for IDs: `pub type AssetId = uuid::Uuid;`
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use `crate::error::Result<T>` alias
|
||||||
|
- Use `thiserror` for error enums
|
||||||
|
- Prefer `ImmichError::Validation(String)` for user input errors
|
||||||
|
- Convert external errors: `#[from] reqwest::Error`
|
||||||
|
|
||||||
|
### API Design Patterns
|
||||||
|
- **Builder pattern**: All API operations use builders
|
||||||
|
```rust
|
||||||
|
client.assets().upload().file("path").execute().await?;
|
||||||
|
```
|
||||||
|
- Cheap cloning: `Client` wraps data in `Arc<ClientInner>`
|
||||||
|
- API modules take `Client` by value
|
||||||
|
- Store `Client` in builders, not references
|
||||||
|
|
||||||
|
### OpenAPI
|
||||||
|
Refer to `openapi-spec.yaml` for documentation regarding immich API.
|
||||||
|
Note that some type definitions in this file are incorrect with regards to nullability.
|
||||||
|
The file is big, so prefer to use nushell to query the things you need:
|
||||||
|
```nu
|
||||||
|
# List all api paths
|
||||||
|
open openapi-spec.yaml | get paths | transpose key val | get key
|
||||||
|
|
||||||
|
# Read the details about a particular path
|
||||||
|
open openapi-spec.yaml | get paths.'/workflows' | to yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- All public items must have doc comments
|
||||||
|
- Module-level docs explaining purpose
|
||||||
|
- Example code in docs should compile (use `no_run` or `ignore` if needed)
|
||||||
|
- Include `# Errors` section for fallible methods
|
||||||
|
|
||||||
|
### Rust Edition Features
|
||||||
|
- Edition 2024 (use latest idioms)
|
||||||
|
- Minimum Rust version: 1.85
|
||||||
|
- Use `const fn` where possible
|
||||||
|
- Prefer `impl Into<String>` for string parameters iff the function requires ownership of the String
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
lib.rs # Re-exports, module declarations
|
||||||
|
client.rs # Client struct with Arc<ClientInner>
|
||||||
|
error.rs # Error types with thiserror
|
||||||
|
models/ # Data models, types
|
||||||
|
apis/ # API modules (albums, assets, etc.)
|
||||||
|
examples/ # Usage examples
|
||||||
|
tests/ # Integration tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint Configuration
|
||||||
|
|
||||||
|
From Cargo.toml - DO NOT SUPPRESS THESE:
|
||||||
|
- `unused_async = "deny"` - No async fn without await
|
||||||
|
- `wildcard_dependencies = "deny"` - No wildcard deps
|
||||||
|
- `non_ascii_idents = "forbid"` - ASCII only
|
||||||
|
- `rust_2018_idioms = "deny"` - Modern Rust idioms
|
||||||
|
|
||||||
|
## Agile agentic workflow
|
||||||
|
|
||||||
|
- You are responsible for high-level architecture.
|
||||||
|
- You may (and should) read code to understand the architecture.
|
||||||
|
- You should AVOID doing implementation work and testing. Use the `task` tool to spawn subagents for this.
|
||||||
|
- You should also use the `task` tool to spawn subagents to for code review.
|
||||||
|
- Prefer splitting subagent-tasks into small incremental bits of work.
|
||||||
|
- Subagents must NEVER exit with compilation errors, but warnings due to stubbed implementations (`todo!()`) are fine.
|
||||||
|
|
||||||
|
## Never Do
|
||||||
|
|
||||||
|
- Never use `unsafe` without asking first
|
||||||
|
- Never ignore clippy warnings (fix them)
|
||||||
|
- Never suppress warnings with `#[allow(...)]`
|
||||||
|
- Never use `panic!` or `unwrap()` in library code
|
||||||
|
- Never break the builder pattern chain
|
||||||
|
- Never write pointless tests to simply get more code coverage
|
||||||
63
docker/podman-compose.yml
Normal file
63
docker/podman-compose.yml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: immich
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
container_name: immich_server
|
||||||
|
image: ghcr.io/immich-app/immich-server:v2
|
||||||
|
command: ['start.sh', 'immich']
|
||||||
|
ports:
|
||||||
|
- 2283:2283
|
||||||
|
environment:
|
||||||
|
UPLOAD_LOCATION: /usr/src/app/upload
|
||||||
|
DB_HOSTNAME: postgres
|
||||||
|
DB_USERNAME: postgres
|
||||||
|
DB_PASSWORD: postgres
|
||||||
|
DB_DATABASE_NAME: immich
|
||||||
|
REDIS_HOSTNAME: redis
|
||||||
|
DB_PORT: 5432
|
||||||
|
volumes:
|
||||||
|
- uploads:/usr/src/app/upload
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_started
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:2283/api/server/ping']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
container_name: immich_postgres
|
||||||
|
image: tensorchord/pgvecto-rs:pg16-v0.3.0
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: immich
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis
|
||||||
|
image: redis:7-alpine
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uploads:
|
||||||
|
pgdata:
|
||||||
133
examples/album_management.rs
Normal file
133
examples/album_management.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
//! Basic usage example for immich-sdk
|
//! 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;
|
use immich_sdk::Client;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
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
|
// 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
|
// Get server version
|
||||||
let version = client.server().version().await?;
|
let version = client.server().version().await?;
|
||||||
|
|||||||
67
examples/delete_assets.rs
Normal file
67
examples/delete_assets.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
//! Example: Download an asset from Immich
|
//! 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 immich_sdk::Client;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Create a client
|
// Configure the client from environment variables
|
||||||
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
|
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)
|
// Create a client
|
||||||
let asset_id = "your-asset-id-here".parse()?;
|
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
|
// Download the asset
|
||||||
println!("Downloading asset...");
|
println!("Downloading asset...");
|
||||||
|
|||||||
52
examples/oauth_login.rs
Normal file
52
examples/oauth_login.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use immich_sdk::{Client, Config};
|
||||||
|
use std::time::Duration;
|
||||||
|
use immich_sdk::models::{OAuthConfigDto, OAuthCallbackDto};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = Config::new("http://localhost:2283")
|
||||||
|
.with_api_key("your-api-key")
|
||||||
|
.with_timeout(Duration::from_secs(30));
|
||||||
|
let client = Client::new(config)?;
|
||||||
|
|
||||||
|
println!("Starting OAuth authorization process...");
|
||||||
|
|
||||||
|
// 1. Start OAuth authorization
|
||||||
|
// In a real scenario, this URL would be opened in a browser.
|
||||||
|
let auth_config = OAuthConfigDto {
|
||||||
|
redirect_uri: "http://localhost:8080/callback".to_string(),
|
||||||
|
code_challenge: None,
|
||||||
|
state: Some("random_state_string".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth_response = client
|
||||||
|
.oauth()
|
||||||
|
.authorize(auth_config)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let auth_url = auth_response.url;
|
||||||
|
println!("Please visit this URL to authorize: {}", auth_url);
|
||||||
|
|
||||||
|
// 2. Simulate the callback from the OAuth provider
|
||||||
|
// In a real scenario, your web server would receive this POST request.
|
||||||
|
let callback_data = OAuthCallbackDto {
|
||||||
|
url: "http://localhost:8080/callback".to_string(),
|
||||||
|
state: Some("random_state_string".to_string()),
|
||||||
|
code_verifier: Some("some_verifier".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Simulating OAuth callback with: {:?}", callback_data);
|
||||||
|
|
||||||
|
// 3. Finish OAuth process by exchanging the code for a session token
|
||||||
|
let login_response = client
|
||||||
|
.oauth()
|
||||||
|
.finish_oauth(callback_data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Successfully logged in!");
|
||||||
|
println!("Access Token: {}", login_response.access_token);
|
||||||
|
println!("User ID: {}", login_response.user_id);
|
||||||
|
println!("User Email: {}", login_response.user_email);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
224
examples/search_metadata.rs
Normal file
224
examples/search_metadata.rs
Normal 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(())
|
||||||
|
}
|
||||||
50
examples/server_info.rs
Normal file
50
examples/server_info.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//! 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);
|
||||||
|
println!(" Licensed: {}", about.licensed);
|
||||||
|
if let Some(ref build) = about.build {
|
||||||
|
println!(" Build: {}", build);
|
||||||
|
}
|
||||||
|
if let Some(ref repository) = about.repository {
|
||||||
|
println!(" Repository: {}", repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
//! Example: Download asset thumbnails
|
//! 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 image::GenericImageView;
|
||||||
use immich_sdk::Client;
|
use immich_sdk::Client;
|
||||||
@@ -7,11 +11,21 @@ use std::fs;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Create a client
|
// Configure the client from environment variables
|
||||||
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
|
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
|
// Create a client
|
||||||
let asset_id = "your-asset-id-here".parse()?;
|
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)
|
// Download thumbnail (small size, default)
|
||||||
println!("Downloading thumbnail...");
|
println!("Downloading thumbnail...");
|
||||||
|
|||||||
127
examples/timeline_browsing.rs
Normal file
127
examples/timeline_browsing.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
//! Example: Upload photos to Immich
|
//! 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 immich_sdk::Client;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Create a client
|
// Configure the client from environment variables
|
||||||
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
|
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
|
// Create a client
|
||||||
let photo_path = Path::new("/path/to/your/photo.jpg");
|
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
|
// Upload the photo
|
||||||
println!("Uploading: {}", photo_path.display());
|
println!("Uploading: {}", photo_path.display());
|
||||||
@@ -37,6 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create an album and add the uploaded asset
|
// Create an album and add the uploaded asset
|
||||||
|
// Note: For duplicates, the ID is returned even though status is duplicate
|
||||||
if let Some(asset_id) = result.id {
|
if let Some(asset_id) = result.id {
|
||||||
let album = client
|
let album = client
|
||||||
.albums()
|
.albums()
|
||||||
@@ -45,7 +69,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.execute()
|
.execute()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
client
|
let _add_result = client
|
||||||
.albums()
|
.albums()
|
||||||
.add_assets(album.id)
|
.add_assets(album.id)
|
||||||
.asset_ids([asset_id])
|
.asset_ids([asset_id])
|
||||||
|
|||||||
16981
openapi-spec.yaml
Normal file
16981
openapi-spec.yaml
Normal file
File diff suppressed because it is too large
Load Diff
42
scripts/run-example.sh
Executable file
42
scripts/run-example.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
EXAMPLE=$1
|
||||||
|
|
||||||
|
if [ -z "$EXAMPLE" ]; then
|
||||||
|
echo "Usage: $0 <example_name>"
|
||||||
|
echo "Available examples:"
|
||||||
|
ls examples/*.rs | sed 's/examples\///' | sed 's/\.rs$//' | sed 's/^/ - /'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if example exists
|
||||||
|
if [ ! -f "examples/${EXAMPLE}.rs" ]; then
|
||||||
|
echo "Error: Example 'examples/${EXAMPLE}.rs' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Start immich if not running
|
||||||
|
if ! curl -s http://localhost:2283/api/server/ping > /dev/null 2>&1; then
|
||||||
|
echo "Immich is not running. Starting..."
|
||||||
|
./scripts/start-immich.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Seed data if not already done
|
||||||
|
if [ ! -f .seeded ]; then
|
||||||
|
echo "Seeding test data..."
|
||||||
|
./scripts/seed-data.sh
|
||||||
|
touch .seeded
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
# set -a exports all variables defined from here on
|
||||||
|
set -a
|
||||||
|
source .env.test
|
||||||
|
set +a
|
||||||
|
|
||||||
|
# Run the example
|
||||||
|
echo "Running example: $EXAMPLE"
|
||||||
|
cargo run --example "$EXAMPLE"
|
||||||
175
scripts/seed-data.sh
Executable file
175
scripts/seed-data.sh
Executable file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Seed data script for Immich SDK testing
|
||||||
|
# Uploads sample photos to an Immich test instance
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Change to script directory
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Load environment variables from .env.test if it exists
|
||||||
|
if [ -f ".env.test" ]; then
|
||||||
|
echo "Loading environment from .env.test"
|
||||||
|
source .env.test
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required environment variables
|
||||||
|
if [ -z "$IMMICH_URL" ]; then
|
||||||
|
echo "Error: IMMICH_URL environment variable is not set"
|
||||||
|
echo "Please set it or create a .env.test file with:"
|
||||||
|
echo " IMMICH_URL=http://localhost:2283"
|
||||||
|
echo " IMMICH_API_KEY=your-api-key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$IMMICH_API_KEY" ]; then
|
||||||
|
echo "Error: IMMICH_API_KEY environment variable is not set"
|
||||||
|
echo "Please set it or create a .env.test file with:"
|
||||||
|
echo " IMMICH_URL=http://localhost:2283"
|
||||||
|
echo " IMMICH_API_KEY=your-api-key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PHOTO_DIR="test-data/sample-photos"
|
||||||
|
|
||||||
|
# Check if photo directory exists
|
||||||
|
if [ ! -d "$PHOTO_DIR" ]; then
|
||||||
|
echo "Warning: Photo directory $PHOTO_DIR does not exist"
|
||||||
|
echo "Creating directory..."
|
||||||
|
mkdir -p "$PHOTO_DIR"
|
||||||
|
echo "Please add sample .jpg files to $PHOTO_DIR and run again"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count jpg files
|
||||||
|
jpg_count=$(find "$PHOTO_DIR" -name "*.jpg" -type f 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
if [ "$jpg_count" -eq 0 ]; then
|
||||||
|
echo "Warning: No .jpg files found in $PHOTO_DIR"
|
||||||
|
echo "Please add sample .jpg files and run again"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "Immich Seed Data Script"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Target: $IMMICH_URL"
|
||||||
|
echo "Photos to upload: $jpg_count"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Array to store uploaded asset IDs
|
||||||
|
declare -a asset_ids
|
||||||
|
|
||||||
|
# Upload photos
|
||||||
|
for photo in "$PHOTO_DIR"/*.jpg; do
|
||||||
|
[ -f "$photo" ] || continue
|
||||||
|
|
||||||
|
filename=$(basename "$photo")
|
||||||
|
device_asset_id=$(basename "$photo" .jpg)
|
||||||
|
|
||||||
|
echo -n "Uploading $filename... "
|
||||||
|
|
||||||
|
# Upload with asset data and capture response
|
||||||
|
response=$(curl -s -X POST "$IMMICH_URL/api/assets" \
|
||||||
|
-H "x-api-key: $IMMICH_API_KEY" \
|
||||||
|
-F "assetData=@$photo" \
|
||||||
|
-F "deviceAssetId=$device_asset_id" \
|
||||||
|
-F "deviceId=test-device" \
|
||||||
|
-F "fileCreatedAt=2024-01-15T10:00:00.000Z" \
|
||||||
|
-F "fileModifiedAt=2024-01-15T10:00:00.000Z" \
|
||||||
|
-H "Accept: application/json" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Extract asset ID from response if possible
|
||||||
|
asset_id=$(echo "$response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
||||||
|
|
||||||
|
if [ -n "$asset_id" ]; then
|
||||||
|
asset_ids+=("$asset_id")
|
||||||
|
echo "OK (ID: ${asset_id:0:8}...)"
|
||||||
|
else
|
||||||
|
echo "OK"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo "Upload Summary"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Photos uploaded: $jpg_count"
|
||||||
|
|
||||||
|
# Create album and add assets if we have asset IDs
|
||||||
|
if [ ${#asset_ids[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Creating album..."
|
||||||
|
|
||||||
|
# Create album
|
||||||
|
album_response=$(curl -s -X POST "$IMMICH_URL/api/albums" \
|
||||||
|
-H "x-api-key: $IMMICH_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"albumName":"SDK Test Album"}' 2>/dev/null || true)
|
||||||
|
|
||||||
|
album_id=$(echo "$album_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
||||||
|
|
||||||
|
if [ -n "$album_id" ]; then
|
||||||
|
echo "Album created: SDK Test Album (ID: ${album_id:0:8}...)"
|
||||||
|
|
||||||
|
# Add assets to album
|
||||||
|
echo "Adding photos to album..."
|
||||||
|
|
||||||
|
# Build JSON array of asset IDs
|
||||||
|
asset_json="["
|
||||||
|
first=true
|
||||||
|
for id in "${asset_ids[@]}"; do
|
||||||
|
if [ "$first" = true ]; then
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
asset_json+=","
|
||||||
|
fi
|
||||||
|
asset_json+="\"$id\""
|
||||||
|
done
|
||||||
|
asset_json+="]"
|
||||||
|
|
||||||
|
add_response=$(curl -s -X PUT "$IMMICH_URL/api/albums/$album_id/assets" \
|
||||||
|
-H "x-api-key: $IMMICH_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ids\":$asset_json}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
echo "Added ${#asset_ids[@]} photos to album"
|
||||||
|
|
||||||
|
# Mark first photo as favorite
|
||||||
|
if [ ${#asset_ids[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Marking first photo as favorite..."
|
||||||
|
|
||||||
|
curl -s -X PUT "$IMMICH_URL/api/assets" \
|
||||||
|
-H "x-api-key: $IMMICH_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ids\":[\"${asset_ids[0]}\"],\"isFavorite\":true}" \
|
||||||
|
2>/dev/null > /dev/null || true
|
||||||
|
|
||||||
|
echo "Marked as favorite"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: Could not create album (album ID not found in response)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: No asset IDs captured - skipping album creation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo "Seed complete!"
|
||||||
|
echo "======================================"
|
||||||
|
echo "Server: $IMMICH_URL"
|
||||||
|
echo "Photos uploaded: $jpg_count"
|
||||||
|
if [ ${#asset_ids[@]} -gt 0 ]; then
|
||||||
|
echo "Album: SDK Test Album"
|
||||||
|
echo "Favorites: 1"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "You can now run examples against this server:"
|
||||||
|
echo " cargo run --example list_assets"
|
||||||
|
echo " cargo run --example list_albums"
|
||||||
|
echo "======================================"
|
||||||
121
scripts/start-immich.sh
Executable file
121
scripts/start-immich.sh
Executable file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Script to start Immich containers, create admin user, and generate API key
|
||||||
|
# This script is used for integration testing
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
check_dependency() {
|
||||||
|
if ! command -v "$1" &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: $1 is not installed${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Checking dependencies..."
|
||||||
|
check_dependency "podman"
|
||||||
|
check_dependency "curl"
|
||||||
|
check_dependency "jq"
|
||||||
|
|
||||||
|
# Check if podman-compose is available
|
||||||
|
if ! podman compose version &> /dev/null && ! command -v podman-compose &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: podman-compose is not available${NC}"
|
||||||
|
echo "Please install podman-compose or ensure 'podman compose' is available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
echo "Starting Immich containers..."
|
||||||
|
podman compose -f docker/podman-compose.yml up -d
|
||||||
|
|
||||||
|
# Wait for health check (max 120s)
|
||||||
|
echo "Waiting for Immich to be ready..."
|
||||||
|
READY=false
|
||||||
|
for i in {1..60}; do
|
||||||
|
if curl -s http://localhost:2283/api/server/ping > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN}Immich is ready!${NC}"
|
||||||
|
READY=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/60 - waiting for Immich..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$READY" = false ]; then
|
||||||
|
echo -e "${RED}Error: Immich failed to become ready within 120 seconds${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Admin credentials
|
||||||
|
ADMIN_EMAIL="admin@example.com"
|
||||||
|
ADMIN_PASSWORD="admin123"
|
||||||
|
ADMIN_NAME="Admin User"
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
echo "Creating admin user..."
|
||||||
|
curl -s -X POST http://localhost:2283/api/auth/admin-sign-up \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\",\"name\":\"$ADMIN_NAME\"}" \
|
||||||
|
|| echo -e "${YELLOW}Admin may already exist, continuing...${NC}"
|
||||||
|
|
||||||
|
# Login to get access token
|
||||||
|
echo "Logging in to get access token..."
|
||||||
|
LOGIN_RESPONSE=$(curl -s -X POST http://localhost:2283/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}")
|
||||||
|
|
||||||
|
ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.accessToken')
|
||||||
|
|
||||||
|
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
||||||
|
echo -e "${RED}Error: Failed to get access token${NC}"
|
||||||
|
echo "Response: $LOGIN_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create API key
|
||||||
|
echo "Creating API key..."
|
||||||
|
API_KEY_RESPONSE=$(curl -s -X POST http://localhost:2283/api/api-keys \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||||
|
-d '{"name":"sdk-test-key","permissions":["all"]}')
|
||||||
|
|
||||||
|
API_KEY=$(echo "$API_KEY_RESPONSE" | jq -r '.secret')
|
||||||
|
|
||||||
|
if [ -z "$API_KEY" ] || [ "$API_KEY" = "null" ]; then
|
||||||
|
echo -e "${RED}Error: Failed to create API key${NC}"
|
||||||
|
echo "Response: $API_KEY_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save to .env.test
|
||||||
|
echo "Saving credentials to .env.test..."
|
||||||
|
cat > .env.test << EOF
|
||||||
|
IMMICH_URL=http://localhost:2283
|
||||||
|
IMMICH_API_KEY=$API_KEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Success message
|
||||||
|
echo -e "${GREEN}=======================================${NC}"
|
||||||
|
echo -e "${GREEN}Immich setup complete!${NC}"
|
||||||
|
echo -e "${GREEN}=======================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Credentials saved to .env.test:"
|
||||||
|
echo " IMMICH_URL: http://localhost:2283"
|
||||||
|
echo " IMMICH_API_KEY: ${API_KEY:0:10}... (truncated)"
|
||||||
|
echo ""
|
||||||
|
echo "Admin user:"
|
||||||
|
echo " Email: $ADMIN_EMAIL"
|
||||||
|
echo " Password: $ADMIN_PASSWORD"
|
||||||
|
echo ""
|
||||||
|
echo "You can now access Immich at: http://localhost:2283"
|
||||||
|
echo ""
|
||||||
|
echo "To stop the containers, run:"
|
||||||
|
echo " podman compose -f docker/podman-compose.yml down"
|
||||||
12
scripts/stop-immich.sh
Executable file
12
scripts/stop-immich.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "Stopping Immich containers and destroying volumes..."
|
||||||
|
podman compose -f docker/podman-compose.yml down -v
|
||||||
|
|
||||||
|
# Clean up state files
|
||||||
|
rm -f .env.test .seeded
|
||||||
|
|
||||||
|
echo "Immich stopped and cleaned up."
|
||||||
@@ -241,13 +241,13 @@ impl AddAssetsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the request
|
/// Execute the request
|
||||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
pub async fn execute(self) -> Result<Vec<serde_json::Value>> {
|
||||||
let path = format!("/albums/{}/assets", self.album_id);
|
let path = format!("/albums/{}/assets", self.album_id);
|
||||||
let body = serde_json::json!({ "ids": self.asset_ids });
|
let body = serde_json::json!({ "ids": self.asset_ids });
|
||||||
let req = self.client.put(&path).json(&body);
|
let req = self.client.put(&path).json(&body);
|
||||||
let response = self.client.execute(req.build()?).await?;
|
let response = self.client.execute(req.build()?).await?;
|
||||||
let album: AlbumResponse = response.json().await?;
|
let result: Vec<serde_json::Value> = response.json().await?;
|
||||||
Ok(album)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use std::path::Path;
|
|||||||
use crate::{
|
use crate::{
|
||||||
Client,
|
Client,
|
||||||
error::{ImmichError, Result},
|
error::{ImmichError, Result},
|
||||||
models::{AssetId, AssetMediaSize, AssetResponse, AssetUploadResponse, DeleteAssetsRequest},
|
models::{
|
||||||
|
AssetId, AssetMediaSize, AssetResponse, AssetUploadResponse, DeleteAssetsRequest,
|
||||||
|
MetadataSearchRequest, SearchResponse,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Response from downloading a thumbnail containing image data and metadata
|
/// Response from downloading a thumbnail containing image data and metadata
|
||||||
@@ -66,10 +69,9 @@ impl ThumbnailResponse {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn decode(&self) -> Result<image::DynamicImage> {
|
pub fn decode(&self) -> Result<image::DynamicImage> {
|
||||||
// Get image format from content type
|
// Get image format from content type
|
||||||
let format = image::ImageFormat::from_mime_type(&self.content_type)
|
let format = image::ImageFormat::from_mime_type(&self.content_type).ok_or_else(|| {
|
||||||
.ok_or_else(|| ImmichError::Image(
|
ImmichError::Image(format!("Unsupported content type: {}", self.content_type))
|
||||||
format!("Unsupported content type: {}", self.content_type)
|
})?;
|
||||||
))?;
|
|
||||||
|
|
||||||
// Create reader with the format and decode
|
// Create reader with the format and decode
|
||||||
image::ImageReader::with_format(Cursor::new(&self.data), format)
|
image::ImageReader::with_format(Cursor::new(&self.data), format)
|
||||||
@@ -161,24 +163,26 @@ impl ListAssetsBuilder {
|
|||||||
|
|
||||||
/// Execute the request
|
/// Execute the request
|
||||||
pub async fn execute(self) -> Result<Vec<AssetResponse>> {
|
pub async fn execute(self) -> Result<Vec<AssetResponse>> {
|
||||||
let mut req = self.client.get("/assets");
|
let req = self.client.post("/search/metadata");
|
||||||
|
|
||||||
|
let mut body = MetadataSearchRequest::default();
|
||||||
|
|
||||||
if let Some(album_id) = self.album_id {
|
if let Some(album_id) = self.album_id {
|
||||||
req = req.query(&[("albumId", album_id.to_string())]);
|
body.album_ids = Some(vec![album_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(is_favorite) = self.is_favorite {
|
if let Some(is_favorite) = self.is_favorite {
|
||||||
req = req.query(&[("isFavorite", is_favorite.to_string())]);
|
body.is_favorite = Some(is_favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(is_trashed) = self.is_trashed {
|
if let Some(is_trashed) = self.is_trashed {
|
||||||
req = req.query(&[("isTrashed", is_trashed.to_string())]);
|
body.with_deleted = Some(is_trashed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.client.execute(req.build()?).await?;
|
let response = self.client.execute(req.json(&body).build()?).await?;
|
||||||
let assets: Vec<AssetResponse> = response.json().await?;
|
let search_result: SearchResponse = response.json().await?;
|
||||||
|
|
||||||
Ok(assets)
|
Ok(search_result.assets.items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +218,8 @@ pub struct UploadAssetBuilder {
|
|||||||
device_asset_id: Option<String>,
|
device_asset_id: Option<String>,
|
||||||
device_id: Option<String>,
|
device_id: Option<String>,
|
||||||
is_favorite: bool,
|
is_favorite: bool,
|
||||||
|
file_created_at: Option<String>,
|
||||||
|
file_modified_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UploadAssetBuilder {
|
impl UploadAssetBuilder {
|
||||||
@@ -225,6 +231,8 @@ impl UploadAssetBuilder {
|
|||||||
device_asset_id: None,
|
device_asset_id: None,
|
||||||
device_id: None,
|
device_id: None,
|
||||||
is_favorite: false,
|
is_favorite: false,
|
||||||
|
file_created_at: None,
|
||||||
|
file_modified_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +260,18 @@ impl UploadAssetBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set file creation timestamp (ISO 8601 format)
|
||||||
|
pub fn file_created_at(mut self, timestamp: impl Into<String>) -> Self {
|
||||||
|
self.file_created_at = Some(timestamp.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set file modification timestamp (ISO 8601 format)
|
||||||
|
pub fn file_modified_at(mut self, timestamp: impl Into<String>) -> Self {
|
||||||
|
self.file_modified_at = Some(timestamp.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute the upload
|
/// Execute the upload
|
||||||
pub async fn execute(self) -> Result<AssetUploadResponse> {
|
pub async fn execute(self) -> Result<AssetUploadResponse> {
|
||||||
let file_path = self
|
let file_path = self
|
||||||
@@ -281,6 +301,11 @@ impl UploadAssetBuilder {
|
|||||||
|
|
||||||
form = form.text("isFavorite", self.is_favorite.to_string());
|
form = form.text("isFavorite", self.is_favorite.to_string());
|
||||||
|
|
||||||
|
// Add file timestamps (required by Immich API v2)
|
||||||
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
form = form.text("fileCreatedAt", self.file_created_at.unwrap_or_else(|| now.clone()));
|
||||||
|
form = form.text("fileModifiedAt", self.file_modified_at.unwrap_or(now));
|
||||||
|
|
||||||
let req = self.client.post("/assets").multipart(form);
|
let req = self.client.post("/assets").multipart(form);
|
||||||
let response = self.client.execute(req.build()?).await?;
|
let response = self.client.execute(req.build()?).await?;
|
||||||
let result: AssetUploadResponse = response.json().await?;
|
let result: AssetUploadResponse = response.json().await?;
|
||||||
|
|||||||
55
src/apis/auth.rs
Normal file
55
src/apis/auth.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//! Authentication API
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Client,
|
||||||
|
error::Result,
|
||||||
|
models::{
|
||||||
|
AuthStatusResponseDto, LoginCredentialDto, LoginResponseDto, LogoutResponseDto,
|
||||||
|
ValidateAccessTokenResponseDto,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// API for authentication
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuthApi {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthApi {
|
||||||
|
/// Create a new auth API instance
|
||||||
|
pub const fn new(client: Client) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login with username and password
|
||||||
|
pub async fn login(&self, credentials: LoginCredentialDto) -> Result<LoginResponseDto> {
|
||||||
|
let req = self.client.post("/auth/login").json(&credentials);
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let login_response: LoginResponseDto = response.json().await?;
|
||||||
|
Ok(login_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logout the current user and invalidate the session token
|
||||||
|
pub async fn logout(&self) -> Result<LogoutResponseDto> {
|
||||||
|
let req = self.client.post("/auth/logout");
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let logout_response: LogoutResponseDto = response.json().await?;
|
||||||
|
Ok(logout_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get information about the current session
|
||||||
|
pub async fn get_auth_status(&self) -> Result<AuthStatusResponseDto> {
|
||||||
|
let req = self.client.get("/auth/status");
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let auth_status: AuthStatusResponseDto = response.json().await?;
|
||||||
|
Ok(auth_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the current authorization method is still valid
|
||||||
|
pub async fn validate_access_token(&self) -> Result<ValidateAccessTokenResponseDto> {
|
||||||
|
let req = self.client.post("/auth/validateToken");
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let validate_response: ValidateAccessTokenResponseDto = response.json().await?;
|
||||||
|
Ok(validate_response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
pub mod albums;
|
pub mod albums;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
pub mod search;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod oauth;
|
||||||
|
|
||||||
// Re-export main API modules
|
// Re-export main API modules
|
||||||
pub use albums::AlbumsApi;
|
pub use albums::AlbumsApi;
|
||||||
pub use assets::AssetsApi;
|
pub use assets::AssetsApi;
|
||||||
|
pub use search::SearchApi;
|
||||||
pub use server::ServerApi;
|
pub use server::ServerApi;
|
||||||
pub use timeline::TimelineApi;
|
pub use timeline::TimelineApi;
|
||||||
|
pub use auth::AuthApi;
|
||||||
|
pub use oauth::OAuthApi;
|
||||||
|
|||||||
61
src/apis/oauth.rs
Normal file
61
src/apis/oauth.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//! OAuth API
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Client,
|
||||||
|
error::Result,
|
||||||
|
models::{
|
||||||
|
OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, UserAdminResponseDto,
|
||||||
|
LoginResponseDto,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// API for OAuth
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OAuthApi {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuthApi {
|
||||||
|
/// Create a new oauth API instance
|
||||||
|
pub const fn new(client: Client) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start OAuth authorization
|
||||||
|
pub async fn authorize(&self, config: OAuthConfigDto) -> Result<OAuthAuthorizeResponseDto> {
|
||||||
|
let req = self.client.post("/oauth/authorize").json(&config);
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let auth_response: OAuthAuthorizeResponseDto = response.json().await?;
|
||||||
|
Ok(auth_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish OAuth authorization
|
||||||
|
pub async fn finish_oauth(&self, callback_data: OAuthCallbackDto) -> Result<LoginResponseDto> {
|
||||||
|
let req = self.client.post("/oauth/callback").json(&callback_data);
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let login_response: LoginResponseDto = response.json().await?;
|
||||||
|
Ok(login_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Link an OAuth account
|
||||||
|
pub async fn link_oauth_account(&self, callback_data: OAuthCallbackDto) -> Result<UserAdminResponseDto> {
|
||||||
|
let req = self.client.post("/oauth/link").json(&callback_data);
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let user_admin_response: UserAdminResponseDto = response.json().await?;
|
||||||
|
Ok(user_admin_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirect OAuth to mobile
|
||||||
|
pub async fn redirect_oauth_to_mobile(&self) -> Result<()> {
|
||||||
|
let req = self.client.get("/oauth/mobile-redirect");
|
||||||
|
self.client.execute(req.build()?).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unlink an OAuth account
|
||||||
|
pub async fn unlink_oauth_account(&self) -> Result<()> {
|
||||||
|
let req = self.client.post("/oauth/unlink");
|
||||||
|
self.client.execute(req.build()?).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
395
src/apis/search.rs
Normal file
395
src/apis/search.rs
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
//! Search API - Search for assets and albums by metadata
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Client,
|
||||||
|
error::Result,
|
||||||
|
models::{
|
||||||
|
AssetId, AssetOrder, AssetType, AssetVisibility, MetadataSearchRequest, SearchResponse,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// API for searching assets and albums
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SearchApi {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchApi {
|
||||||
|
/// Create a new search API instance
|
||||||
|
pub const fn new(client: Client) -> Self {
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a metadata search with the builder pattern
|
||||||
|
pub fn metadata(&self) -> SearchMetadataBuilder {
|
||||||
|
SearchMetadataBuilder::new(self.client.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for the metadata search endpoint
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SearchMetadataBuilder {
|
||||||
|
client: Client,
|
||||||
|
album_ids: Vec<AssetId>,
|
||||||
|
checksum: Option<String>,
|
||||||
|
city: Option<String>,
|
||||||
|
country: Option<String>,
|
||||||
|
created_after: Option<DateTime<Utc>>,
|
||||||
|
created_before: Option<DateTime<Utc>>,
|
||||||
|
description: Option<String>,
|
||||||
|
device_asset_id: Option<String>,
|
||||||
|
device_id: Option<String>,
|
||||||
|
is_favorite: Option<bool>,
|
||||||
|
is_motion: Option<bool>,
|
||||||
|
is_not_in_album: Option<bool>,
|
||||||
|
is_offline: Option<bool>,
|
||||||
|
lens_model: Option<String>,
|
||||||
|
library_id: Option<AssetId>,
|
||||||
|
make: Option<String>,
|
||||||
|
model: Option<String>,
|
||||||
|
ocr: Option<String>,
|
||||||
|
order: Option<AssetOrder>,
|
||||||
|
original_file_name: Option<String>,
|
||||||
|
page: Option<u32>,
|
||||||
|
person_ids: Vec<AssetId>,
|
||||||
|
rating: Option<i32>,
|
||||||
|
size: Option<u32>,
|
||||||
|
state: Option<String>,
|
||||||
|
tag_ids: Vec<AssetId>,
|
||||||
|
taken_after: Option<DateTime<Utc>>,
|
||||||
|
taken_before: Option<DateTime<Utc>>,
|
||||||
|
asset_type: Option<AssetType>,
|
||||||
|
updated_after: Option<DateTime<Utc>>,
|
||||||
|
updated_before: Option<DateTime<Utc>>,
|
||||||
|
visibility: Option<AssetVisibility>,
|
||||||
|
with_deleted: Option<bool>,
|
||||||
|
with_exif: Option<bool>,
|
||||||
|
with_people: Option<bool>,
|
||||||
|
with_stacked: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchMetadataBuilder {
|
||||||
|
/// Create a new metadata search builder
|
||||||
|
const fn new(client: Client) -> Self {
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
album_ids: Vec::new(),
|
||||||
|
checksum: None,
|
||||||
|
city: None,
|
||||||
|
country: None,
|
||||||
|
created_after: None,
|
||||||
|
created_before: None,
|
||||||
|
description: None,
|
||||||
|
device_asset_id: None,
|
||||||
|
device_id: None,
|
||||||
|
is_favorite: None,
|
||||||
|
is_motion: None,
|
||||||
|
is_not_in_album: None,
|
||||||
|
is_offline: None,
|
||||||
|
lens_model: None,
|
||||||
|
library_id: None,
|
||||||
|
make: None,
|
||||||
|
model: None,
|
||||||
|
ocr: None,
|
||||||
|
order: None,
|
||||||
|
original_file_name: None,
|
||||||
|
page: None,
|
||||||
|
person_ids: Vec::new(),
|
||||||
|
rating: None,
|
||||||
|
size: None,
|
||||||
|
state: None,
|
||||||
|
tag_ids: Vec::new(),
|
||||||
|
taken_after: None,
|
||||||
|
taken_before: None,
|
||||||
|
asset_type: None,
|
||||||
|
updated_after: None,
|
||||||
|
updated_before: None,
|
||||||
|
visibility: None,
|
||||||
|
with_deleted: None,
|
||||||
|
with_exif: None,
|
||||||
|
with_people: None,
|
||||||
|
with_stacked: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by album ID (can be called multiple times to filter by multiple albums)
|
||||||
|
pub fn album_id(mut self, id: AssetId) -> Self {
|
||||||
|
self.album_ids.push(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by file checksum
|
||||||
|
pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
|
||||||
|
self.checksum = Some(checksum.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by city name
|
||||||
|
pub fn city(mut self, city: impl Into<String>) -> Self {
|
||||||
|
self.city = Some(city.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by country name
|
||||||
|
pub fn country(mut self, country: impl Into<String>) -> Self {
|
||||||
|
self.country = Some(country.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by creation date after
|
||||||
|
pub fn created_after(mut self, after: DateTime<Utc>) -> Self {
|
||||||
|
self.created_after = Some(after);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by creation date before
|
||||||
|
pub fn created_before(mut self, before: DateTime<Utc>) -> Self {
|
||||||
|
self.created_before = Some(before);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by description text
|
||||||
|
pub fn description(mut self, desc: impl Into<String>) -> Self {
|
||||||
|
self.description = Some(desc.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by device asset ID
|
||||||
|
pub fn device_asset_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.device_asset_id = Some(id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by device ID
|
||||||
|
pub fn device_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.device_id = Some(id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by favorite status
|
||||||
|
pub fn favorite(mut self, is_favorite: bool) -> Self {
|
||||||
|
self.is_favorite = Some(is_favorite);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by motion photo status
|
||||||
|
pub fn motion(mut self, is_motion: bool) -> Self {
|
||||||
|
self.is_motion = Some(is_motion);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter assets not in any album
|
||||||
|
pub fn not_in_album(mut self) -> Self {
|
||||||
|
self.is_not_in_album = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by offline status
|
||||||
|
pub fn offline(mut self, is_offline: bool) -> Self {
|
||||||
|
self.is_offline = Some(is_offline);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by lens model
|
||||||
|
pub fn lens_model(mut self, model: impl Into<String>) -> Self {
|
||||||
|
self.lens_model = Some(model.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by library ID
|
||||||
|
pub fn library_id(mut self, id: AssetId) -> Self {
|
||||||
|
self.library_id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by camera make
|
||||||
|
pub fn make(mut self, make: impl Into<String>) -> Self {
|
||||||
|
self.make = Some(make.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by camera model
|
||||||
|
pub fn model(mut self, model: impl Into<String>) -> Self {
|
||||||
|
self.model = Some(model.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by OCR text content
|
||||||
|
pub fn ocr(mut self, text: impl Into<String>) -> Self {
|
||||||
|
self.ocr = Some(text.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set sort order
|
||||||
|
pub fn order(mut self, order: AssetOrder) -> Self {
|
||||||
|
self.order = Some(order);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by original file name
|
||||||
|
pub fn file_name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.original_file_name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set page number
|
||||||
|
pub fn page(mut self, page: u32) -> Self {
|
||||||
|
self.page = Some(page);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by person ID (can be called multiple times to filter by multiple people)
|
||||||
|
pub fn person_id(mut self, id: AssetId) -> Self {
|
||||||
|
self.person_ids.push(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by rating (1-5, or -1 deprecated)
|
||||||
|
pub fn rating(mut self, rating: i32) -> Self {
|
||||||
|
self.rating = Some(rating);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set number of results (1-1000, default 100)
|
||||||
|
pub fn size(mut self, size: u32) -> Self {
|
||||||
|
self.size = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by state/province name
|
||||||
|
pub fn state(mut self, state: impl Into<String>) -> Self {
|
||||||
|
self.state = Some(state.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by tag ID (can be called multiple times to filter by multiple tags)
|
||||||
|
pub fn tag_id(mut self, id: AssetId) -> Self {
|
||||||
|
self.tag_ids.push(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by taken date after
|
||||||
|
pub fn taken_after(mut self, after: DateTime<Utc>) -> Self {
|
||||||
|
self.taken_after = Some(after);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by taken date before
|
||||||
|
pub fn taken_before(mut self, before: DateTime<Utc>) -> Self {
|
||||||
|
self.taken_before = Some(before);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by asset type
|
||||||
|
pub fn asset_type(mut self, asset_type: AssetType) -> Self {
|
||||||
|
self.asset_type = Some(asset_type);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by update date after
|
||||||
|
pub fn updated_after(mut self, after: DateTime<Utc>) -> Self {
|
||||||
|
self.updated_after = Some(after);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by update date before
|
||||||
|
pub fn updated_before(mut self, before: DateTime<Utc>) -> Self {
|
||||||
|
self.updated_before = Some(before);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter by visibility
|
||||||
|
pub fn visibility(mut self, visibility: AssetVisibility) -> Self {
|
||||||
|
self.visibility = Some(visibility);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include deleted assets in results
|
||||||
|
pub fn with_deleted(mut self) -> Self {
|
||||||
|
self.with_deleted = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include EXIF data in response
|
||||||
|
pub fn with_exif(mut self) -> Self {
|
||||||
|
self.with_exif = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include people data in response
|
||||||
|
pub fn with_people(mut self) -> Self {
|
||||||
|
self.with_people = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Include stacked assets
|
||||||
|
pub fn with_stacked(mut self) -> Self {
|
||||||
|
self.with_stacked = Some(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the request and execute it
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the HTTP request fails, if the response cannot be deserialized,
|
||||||
|
/// or if authentication is missing/invalid
|
||||||
|
pub async fn execute(self) -> Result<SearchResponse> {
|
||||||
|
let request = MetadataSearchRequest {
|
||||||
|
album_ids: if self.album_ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.album_ids)
|
||||||
|
},
|
||||||
|
checksum: self.checksum,
|
||||||
|
city: self.city,
|
||||||
|
country: self.country,
|
||||||
|
created_after: self.created_after,
|
||||||
|
created_before: self.created_before,
|
||||||
|
description: self.description,
|
||||||
|
device_asset_id: self.device_asset_id,
|
||||||
|
device_id: self.device_id,
|
||||||
|
is_favorite: self.is_favorite,
|
||||||
|
is_motion: self.is_motion,
|
||||||
|
is_not_in_album: self.is_not_in_album,
|
||||||
|
is_offline: self.is_offline,
|
||||||
|
lens_model: self.lens_model,
|
||||||
|
library_id: self.library_id,
|
||||||
|
make: self.make,
|
||||||
|
model: self.model,
|
||||||
|
ocr: self.ocr,
|
||||||
|
order: self.order,
|
||||||
|
original_file_name: self.original_file_name,
|
||||||
|
page: self.page,
|
||||||
|
person_ids: if self.person_ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.person_ids)
|
||||||
|
},
|
||||||
|
rating: self.rating,
|
||||||
|
size: self.size,
|
||||||
|
state: self.state,
|
||||||
|
tag_ids: if self.tag_ids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.tag_ids)
|
||||||
|
},
|
||||||
|
taken_after: self.taken_after,
|
||||||
|
taken_before: self.taken_before,
|
||||||
|
asset_type: self.asset_type,
|
||||||
|
updated_after: self.updated_after,
|
||||||
|
updated_before: self.updated_before,
|
||||||
|
visibility: self.visibility,
|
||||||
|
with_deleted: self.with_deleted,
|
||||||
|
with_exif: self.with_exif,
|
||||||
|
with_people: self.with_people,
|
||||||
|
with_stacked: self.with_stacked,
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = self.client.post("/search/metadata").json(&request);
|
||||||
|
let response = self.client.execute(req.build()?).await?;
|
||||||
|
let result: SearchResponse = response.json().await?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Client for interacting with the Immich API
|
//! Client for interacting with the Immich API
|
||||||
|
|
||||||
use crate::apis::{AlbumsApi, AssetsApi, ServerApi, TimelineApi};
|
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi, AuthApi, OAuthApi};
|
||||||
use crate::error::{ImmichError, Result};
|
use crate::error::{ImmichError, Result};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Configuration for the Immich client
|
/// Configuration for the Immich client
|
||||||
@@ -56,13 +57,22 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Client for making requests to the Immich API
|
/// Internal client data wrapped in Arc for cheap cloning
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
struct ClientInner {
|
||||||
config: Config,
|
config: Config,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Client for making requests to the Immich API
|
||||||
|
///
|
||||||
|
/// This struct is cheap to clone - it uses an internal Arc to share the underlying
|
||||||
|
/// HTTP client and configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
inner: Arc<ClientInner>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client with the given configuration
|
/// Create a new client with the given configuration
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
pub fn new(config: Config) -> Result<Self> {
|
||||||
@@ -86,8 +96,10 @@ impl Client {
|
|||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
inner: Arc::new(ClientInner {
|
||||||
config: Config { base_url, ..config },
|
config: Config { base_url, ..config },
|
||||||
http,
|
http,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,38 +109,46 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the API key
|
/// Set the API key
|
||||||
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
|
pub fn with_api_key(self, api_key: impl Into<String>) -> Self {
|
||||||
self.config.api_key = Some(api_key.into());
|
// We need to create a new ClientInner since we can't modify Arc contents
|
||||||
self
|
let mut config = self.inner.config.clone();
|
||||||
|
config.api_key = Some(api_key.into());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(ClientInner {
|
||||||
|
config,
|
||||||
|
http: self.inner.http.clone(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the configuration
|
/// Get the configuration
|
||||||
pub fn config(&self) -> &Config {
|
pub fn config(&self) -> &Config {
|
||||||
&self.config
|
&self.inner.config
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the base URL
|
/// Get the base URL
|
||||||
pub fn base_url(&self) -> &str {
|
pub fn base_url(&self) -> &str {
|
||||||
&self.config.base_url
|
&self.inner.config.base_url
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the client has an API key configured
|
/// Check if the client has an API key configured
|
||||||
pub fn has_api_key(&self) -> bool {
|
pub fn has_api_key(&self) -> bool {
|
||||||
self.config.api_key.is_some()
|
self.inner.config.api_key.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the underlying HTTP client
|
/// Get the underlying HTTP client
|
||||||
pub fn http(&self) -> &reqwest::Client {
|
pub fn http(&self) -> &reqwest::Client {
|
||||||
&self.http
|
&self.inner.http
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an authenticated request builder
|
/// Create an authenticated request builder
|
||||||
pub fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
|
pub fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
|
||||||
let url = format!("{}/api{}", self.config.base_url, path);
|
let url = format!("{}/api{}", self.inner.config.base_url, path);
|
||||||
let mut builder = self.http.request(method, &url);
|
let mut builder = self.inner.http.request(method, &url);
|
||||||
|
|
||||||
// Add API key authentication
|
// Add API key authentication
|
||||||
if let Some(ref api_key) = self.config.api_key {
|
if let Some(ref api_key) = self.inner.config.api_key {
|
||||||
builder = builder.header("x-api-key", api_key);
|
builder = builder.header("x-api-key", api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +157,7 @@ impl Client {
|
|||||||
|
|
||||||
/// Execute a request and handle common error cases
|
/// Execute a request and handle common error cases
|
||||||
pub async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response> {
|
pub async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response> {
|
||||||
let response = self.http.execute(request).await?;
|
let response = self.inner.http.execute(request).await?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
@@ -181,6 +201,11 @@ impl Client {
|
|||||||
AssetsApi::new(self.clone())
|
AssetsApi::new(self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the search API
|
||||||
|
pub fn search(&self) -> SearchApi {
|
||||||
|
SearchApi::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Access the server API
|
/// Access the server API
|
||||||
pub fn server(&self) -> ServerApi {
|
pub fn server(&self) -> ServerApi {
|
||||||
ServerApi::new(self.clone())
|
ServerApi::new(self.clone())
|
||||||
@@ -190,6 +215,16 @@ impl Client {
|
|||||||
pub fn timeline(&self) -> TimelineApi {
|
pub fn timeline(&self) -> TimelineApi {
|
||||||
TimelineApi::new(self.clone())
|
TimelineApi::new(self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the auth API
|
||||||
|
pub fn auth(&self) -> AuthApi {
|
||||||
|
AuthApi::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the oauth API
|
||||||
|
pub fn oauth(&self) -> OAuthApi {
|
||||||
|
OAuthApi::new(self.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -186,10 +186,66 @@ pub struct ServerFeatures {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ServerAbout {
|
pub struct ServerAbout {
|
||||||
/// Version information
|
/// Server version (e.g., "v2.7.5")
|
||||||
pub version: ServerVersion,
|
pub version: String,
|
||||||
/// Version string
|
/// URL to version information
|
||||||
pub version_url: String,
|
pub version_url: String,
|
||||||
|
/// Build identifier
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub build: Option<String>,
|
||||||
|
/// Build URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub build_url: Option<String>,
|
||||||
|
/// Build image name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub build_image: Option<String>,
|
||||||
|
/// Build image URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub build_image_url: Option<String>,
|
||||||
|
/// ExifTool version
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub exiftool: Option<String>,
|
||||||
|
/// FFmpeg version
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ffmpeg: Option<String>,
|
||||||
|
/// ImageMagick version
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub imagemagick: Option<String>,
|
||||||
|
/// libvips version
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub libvips: Option<String>,
|
||||||
|
/// Node.js version
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub nodejs: Option<String>,
|
||||||
|
/// Repository name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repository: Option<String>,
|
||||||
|
/// Repository URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repository_url: Option<String>,
|
||||||
|
/// Source commit hash
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub source_commit: Option<String>,
|
||||||
|
/// Source reference (branch/tag)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub source_ref: Option<String>,
|
||||||
|
/// Source URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub source_url: Option<String>,
|
||||||
|
/// Third-party bug/feature URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub third_party_bug_feature_url: Option<String>,
|
||||||
|
/// Third-party documentation URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub third_party_documentation_url: Option<String>,
|
||||||
|
/// Third-party source URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub third_party_source_url: Option<String>,
|
||||||
|
/// Third-party support URL
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub third_party_support_url: Option<String>,
|
||||||
|
/// Whether the server is licensed
|
||||||
|
pub licensed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create album request
|
/// Create album request
|
||||||
@@ -248,7 +304,7 @@ pub struct AssetUploadResponse {
|
|||||||
|
|
||||||
/// Asset upload status
|
/// Asset upload status
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum AssetUploadStatus {
|
pub enum AssetUploadStatus {
|
||||||
/// Upload created new asset
|
/// Upload created new asset
|
||||||
Created,
|
Created,
|
||||||
@@ -284,7 +340,7 @@ pub struct ApiKeyResponse {
|
|||||||
|
|
||||||
/// Asset order enumeration
|
/// Asset order enumeration
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AssetOrder {
|
pub enum AssetOrder {
|
||||||
/// Oldest first
|
/// Oldest first
|
||||||
Asc,
|
Asc,
|
||||||
@@ -344,24 +400,305 @@ pub struct TimeBucketAssetResponse {
|
|||||||
pub visibility: Vec<AssetVisibility>,
|
pub visibility: Vec<AssetVisibility>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
/// Metadata search request for filtering assets
|
||||||
mod tests {
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
use super::*;
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MetadataSearchRequest {
|
||||||
#[test]
|
/// Filter by album IDs
|
||||||
fn test_asset_type_serialization() {
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
let asset_type = AssetType::Image;
|
pub album_ids: Option<Vec<AssetId>>,
|
||||||
let json = serde_json::to_string(&asset_type).unwrap();
|
/// Filter by file checksum
|
||||||
assert_eq!(json, r#""IMAGE""#);
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub checksum: Option<String>,
|
||||||
|
/// Filter by city name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub city: Option<String>,
|
||||||
|
/// Filter by country name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub country: Option<String>,
|
||||||
|
/// Filter by creation date (after)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_after: Option<DateTime<Utc>>,
|
||||||
|
/// Filter by creation date (before)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_before: Option<DateTime<Utc>>,
|
||||||
|
/// Filter by description text
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Filter by device asset ID
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub device_asset_id: Option<String>,
|
||||||
|
/// Device ID to filter by
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub device_id: Option<String>,
|
||||||
|
/// Filter by favorite status
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_favorite: Option<bool>,
|
||||||
|
/// Filter by motion photo status
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_motion: Option<bool>,
|
||||||
|
/// Filter assets not in any album
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_not_in_album: Option<bool>,
|
||||||
|
/// Filter by offline status
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub is_offline: Option<bool>,
|
||||||
|
/// Filter by lens model
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lens_model: Option<String>,
|
||||||
|
/// Library ID to filter by
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub library_id: Option<AssetId>,
|
||||||
|
/// Filter by camera make
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub make: Option<String>,
|
||||||
|
/// Filter by camera model
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub model: Option<String>,
|
||||||
|
/// Filter by OCR text content
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ocr: Option<String>,
|
||||||
|
/// Sort order
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub order: Option<AssetOrder>,
|
||||||
|
/// Filter by original file name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub original_file_name: Option<String>,
|
||||||
|
/// Page number (default: 1)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub page: Option<u32>,
|
||||||
|
/// Filter by person IDs
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub person_ids: Option<Vec<AssetId>>,
|
||||||
|
/// Filter by rating [1-5], or null for unrated (-1 deprecated)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rating: Option<i32>,
|
||||||
|
/// Number of results to return (default: 100)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub size: Option<u32>,
|
||||||
|
/// Filter by state/province name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
/// Filter by tag IDs
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tag_ids: Option<Vec<AssetId>>,
|
||||||
|
/// Filter by taken date (after)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub taken_after: Option<DateTime<Utc>>,
|
||||||
|
/// Filter by taken date (before)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub taken_before: Option<DateTime<Utc>>,
|
||||||
|
/// Asset type filter
|
||||||
|
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub asset_type: Option<AssetType>,
|
||||||
|
/// Filter by update date (after)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated_after: Option<DateTime<Utc>>,
|
||||||
|
/// Filter by update date (before)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated_before: Option<DateTime<Utc>>,
|
||||||
|
/// Filter by visibility
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub visibility: Option<AssetVisibility>,
|
||||||
|
/// Include deleted assets
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_deleted: Option<bool>,
|
||||||
|
/// Include EXIF data in response
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_exif: Option<bool>,
|
||||||
|
/// Include people data in response
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_people: Option<bool>,
|
||||||
|
/// Include stacked assets
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub with_stacked: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
/// Represents a facet value count
|
||||||
fn test_server_version_display() {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
let version = ServerVersion {
|
#[serde(rename_all = "camelCase")]
|
||||||
major: 1,
|
pub struct SearchFacetCount {
|
||||||
minor: 137,
|
/// Number of assets with this facet value
|
||||||
patch: 0,
|
pub count: i64,
|
||||||
};
|
/// Facet value
|
||||||
assert_eq!(version.to_string(), "1.137.0");
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a search facet
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchFacet {
|
||||||
|
/// Facet counts
|
||||||
|
pub counts: Vec<SearchFacetCount>,
|
||||||
|
/// Facet field name
|
||||||
|
pub field_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paginated asset search results
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchAssetResult {
|
||||||
|
/// Number of assets in this page
|
||||||
|
pub count: i64,
|
||||||
|
/// Facet information
|
||||||
|
pub facets: Vec<SearchFacet>,
|
||||||
|
/// The actual assets
|
||||||
|
pub items: Vec<AssetResponse>,
|
||||||
|
/// Next page token
|
||||||
|
pub next_page: Option<String>,
|
||||||
|
/// Total number of matching assets
|
||||||
|
pub total: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paginated album search results
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchAlbumResult {
|
||||||
|
/// Number of albums in this page
|
||||||
|
pub count: i64,
|
||||||
|
/// Facet information
|
||||||
|
pub facets: Vec<SearchFacet>,
|
||||||
|
/// The actual albums
|
||||||
|
pub items: Vec<AlbumResponse>,
|
||||||
|
/// Total number of matching albums
|
||||||
|
pub total: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combined search response
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SearchResponse {
|
||||||
|
/// Album results
|
||||||
|
pub albums: SearchAlbumResult,
|
||||||
|
/// Asset results
|
||||||
|
pub assets: SearchAssetResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LoginCredentialDto {
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LoginResponseDto {
|
||||||
|
pub access_token: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
pub is_onboarded: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub profile_image_path: String,
|
||||||
|
pub should_change_password: bool,
|
||||||
|
pub user_email: String,
|
||||||
|
pub user_id: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LogoutResponseDto {
|
||||||
|
pub redirect_uri: String,
|
||||||
|
pub successful: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthStatusResponseDto {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub expires_at: Option<DateTime<Utc>>,
|
||||||
|
pub is_elevated: bool,
|
||||||
|
pub password: bool,
|
||||||
|
pub pin_code: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pin_expires_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ValidateAccessTokenResponseDto {
|
||||||
|
pub auth_status: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OAuthConfigDto {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub code_challenge: Option<String>,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OAuthAuthorizeResponseDto {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OAuthCallbackDto {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub code_verifier: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserAdminResponseDto {
|
||||||
|
pub avatar_color: UserAvatarColor,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted_at: Option<DateTime<Utc>>,
|
||||||
|
pub email: String,
|
||||||
|
pub id: UserId,
|
||||||
|
pub is_admin: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub license: Option<UserLicense>,
|
||||||
|
pub name: String,
|
||||||
|
pub oauth_id: String,
|
||||||
|
pub profile_changed_at: DateTime<Utc>,
|
||||||
|
pub profile_image_path: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub quota_size_in_bytes: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub quota_usage_in_bytes: Option<i64>,
|
||||||
|
pub should_change_password: bool,
|
||||||
|
pub status: UserStatus,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub storage_label: Option<String>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UserAvatarColor {
|
||||||
|
Primary,
|
||||||
|
Pink,
|
||||||
|
Red,
|
||||||
|
Yellow,
|
||||||
|
Blue,
|
||||||
|
Green,
|
||||||
|
Purple,
|
||||||
|
Orange,
|
||||||
|
Gray,
|
||||||
|
Amber,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserLicense {
|
||||||
|
pub activated_at: DateTime<Utc>,
|
||||||
|
pub activation_key: String,
|
||||||
|
pub license_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UserStatus {
|
||||||
|
Active,
|
||||||
|
Removing,
|
||||||
|
Deleted,
|
||||||
}
|
}
|
||||||
|
|||||||
0
test-data/.gitkeep
Normal file
0
test-data/.gitkeep
Normal file
37
test-data/sample-photos/README.md
Normal file
37
test-data/sample-photos/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Sample Photos
|
||||||
|
|
||||||
|
This directory contains sample photos for testing the Immich SDK.
|
||||||
|
|
||||||
|
## Photos
|
||||||
|
|
||||||
|
### photo1.jpg
|
||||||
|
- **Source**: Unsplash
|
||||||
|
- **Photographer**: [Yannick Pulver](https://unsplash.com/@yanu)
|
||||||
|
- **URL**: https://unsplash.com/photos/photo-1506905925346-21bda4d32df4
|
||||||
|
- **License**: Unsplash License (Free to use)
|
||||||
|
- **Description**: Swiss Alps mountain landscape
|
||||||
|
|
||||||
|
### photo2.jpg
|
||||||
|
- **Source**: Unsplash
|
||||||
|
- **Photographer**: [Manja Vitolic](https://unsplash.com/@madhatterzone)
|
||||||
|
- **URL**: https://unsplash.com/photos/photo-1514888286974-6c03e2ca1dba
|
||||||
|
- **License**: Unsplash License (Free to use)
|
||||||
|
- **Description**: Cat portrait
|
||||||
|
|
||||||
|
### photo3.jpg
|
||||||
|
- **Source**: Unsplash
|
||||||
|
- **Photographer**: [Pedro Lastra](https://unsplash.com/@peterlaster)
|
||||||
|
- **URL**: https://unsplash.com/photos/photo-1477959858617-67f85cf4f1df
|
||||||
|
- **License**: Unsplash License (Free to use)
|
||||||
|
- **Description**: Chicago cityscape aerial view
|
||||||
|
|
||||||
|
## License Information
|
||||||
|
|
||||||
|
All photos sourced from Unsplash are available under the [Unsplash License](https://unsplash.com/license):
|
||||||
|
- Free to use for commercial and non-commercial purposes
|
||||||
|
- No permission needed (though attribution is appreciated)
|
||||||
|
- Cannot be sold without significant modification
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
These photos are intended for development and testing purposes only.
|
||||||
BIN
test-data/sample-photos/photo1.jpg
Normal file
BIN
test-data/sample-photos/photo1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
test-data/sample-photos/photo2.jpg
Normal file
BIN
test-data/sample-photos/photo2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
test-data/sample-photos/photo3.jpg
Normal file
BIN
test-data/sample-photos/photo3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
Reference in New Issue
Block a user