Initial commit: immich-sdk v1.137.0
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
2343
Cargo.lock
generated
Normal file
2343
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
Normal file
55
Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "immich-sdk"
|
||||
version = "1.137.0"
|
||||
edition = "2024"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
description = "A modern Rust SDK for the Immich photo and video management server"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/yourusername/immich-sdk"
|
||||
documentation = "https://docs.rs/immich-sdk"
|
||||
keywords = ["immich", "photos", "api", "sdk", "client"]
|
||||
categories = ["api-bindings", "multimedia"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.13", default-features = false, features = ["json", "multipart", "rustls", "query"] }
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
serde_repr = "0.1"
|
||||
thiserror = "2.0"
|
||||
uuid = { version = "1.22", features = ["serde", "v4"] }
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
url = "2.5"
|
||||
bytes = "1.10"
|
||||
async-trait = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
wiremock = "0.6"
|
||||
|
||||
[lib]
|
||||
name = "immich_sdk"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints.clippy]
|
||||
allow_attributes = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
as_underscore = "warn"
|
||||
borrow_as_ptr = "warn"
|
||||
implicit_clone = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unicode_not_nfc = "warn"
|
||||
unused_async = "deny"
|
||||
wildcard_dependencies = "deny"
|
||||
|
||||
[lints.rust]
|
||||
absolute_paths_not_starting_with_crate = "deny"
|
||||
explicit_outlives_requirements = "warn"
|
||||
macro_use_extern_crate = "deny"
|
||||
missing_abi = "deny"
|
||||
non_ascii_idents = "forbid"
|
||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
single_use_lifetimes = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
180
README.md
Normal file
180
README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# immich-sdk
|
||||
|
||||
A modern Rust SDK for the [Immich](https://immich.app/) photo and video management server.
|
||||
|
||||
## Features
|
||||
|
||||
- **Async-first**: Built on `tokio` and `reqwest` for modern async Rust
|
||||
- **Builder pattern**: Ergonomic API with fluent builders
|
||||
- **Type-safe**: Strongly typed models
|
||||
- **Error handling**: Comprehensive error types with `thiserror`
|
||||
- **Rustls TLS**: Uses rustls instead of OpenSSL
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
immich-sdk = "1.137"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```rust
|
||||
use immich_sdk::{Client, error::Result};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Create a client
|
||||
let client = Client::from_url("https://immich.example.com")?
|
||||
.with_api_key("your-api-key");
|
||||
|
||||
// List albums
|
||||
let albums = client.albums().list().execute().await?;
|
||||
println!("Found {} albums", albums.len());
|
||||
|
||||
// Upload an asset
|
||||
let asset = client
|
||||
.assets()
|
||||
.upload()
|
||||
.file("/path/to/photo.jpg")
|
||||
.device_asset_id("phone-123")
|
||||
.device_id("my-device")
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
println!("Uploaded asset: {:?}", asset.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The SDK supports API key authentication:
|
||||
|
||||
```rust
|
||||
use immich_sdk::Client;
|
||||
|
||||
let client = Client::from_url("https://immich.example.com")?
|
||||
.with_api_key("your-api-key-here");
|
||||
```
|
||||
|
||||
## API Modules
|
||||
|
||||
### Assets
|
||||
|
||||
Upload, download, and manage photos and videos:
|
||||
|
||||
```rust
|
||||
// List all assets
|
||||
let assets = client.assets().list().execute().await?;
|
||||
|
||||
// Get a specific asset
|
||||
let asset = client.assets().get(asset_id).execute().await?;
|
||||
|
||||
// Upload a new asset
|
||||
let uploaded = client
|
||||
.assets()
|
||||
.upload()
|
||||
.file("/path/to/photo.jpg")
|
||||
.device_asset_id("unique-device-id")
|
||||
.device_id("device-name")
|
||||
.favorite()
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Delete assets
|
||||
client
|
||||
.assets()
|
||||
.delete()
|
||||
.id(asset_id)
|
||||
.execute()
|
||||
.await?;
|
||||
```
|
||||
|
||||
### Albums
|
||||
|
||||
Create and manage albums:
|
||||
|
||||
```rust
|
||||
// Create an album
|
||||
let album = client
|
||||
.albums()
|
||||
.create()
|
||||
.name("My Vacation")
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Add assets to album
|
||||
client
|
||||
.albums()
|
||||
.add_assets(album.id)
|
||||
.asset_ids(vec![asset_id1, asset_id2])
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// List all albums
|
||||
let albums = client.albums().list().execute().await?;
|
||||
```
|
||||
|
||||
### Server Info
|
||||
|
||||
Get server information:
|
||||
|
||||
```rust
|
||||
// Get server version
|
||||
let version = client.server().version().await?;
|
||||
println!("Server version: {}", version);
|
||||
|
||||
// Get server features
|
||||
let features = client.server().features().await?;
|
||||
println!("OAuth enabled: {}", features.oauth);
|
||||
|
||||
// Ping the server
|
||||
let pong = client.server().ping().await?;
|
||||
println!("Ping: {}", pong);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The SDK uses the `ImmichError` type for all errors:
|
||||
|
||||
```rust
|
||||
use immich_sdk::error::ImmichError;
|
||||
|
||||
match client.assets().get(asset_id).execute().await {
|
||||
Ok(asset) => println!("Found: {:?}", asset),
|
||||
Err(ImmichError::NotFound(_)) => println!("Asset not found"),
|
||||
Err(ImmichError::Authentication(_)) => println!("Auth failed"),
|
||||
Err(e) => println!("Error: {}", e),
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a client with custom configuration:
|
||||
|
||||
```rust
|
||||
use immich_sdk::{Client, Config};
|
||||
use std::time::Duration;
|
||||
|
||||
let config = Config::new("https://immich.example.com")
|
||||
.with_api_key("your-api-key")
|
||||
.with_timeout(Duration::from_secs(60));
|
||||
|
||||
let client = Client::new(config)?;
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT OR Apache-2.0 license.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [Immich](https://immich.app/) - The amazing photo and video management solution
|
||||
32
examples/basic_usage.rs
Normal file
32
examples/basic_usage.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Basic usage example for immich-sdk
|
||||
|
||||
use immich_sdk::Client;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client
|
||||
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
|
||||
|
||||
// Get server version
|
||||
let version = client.server().version().await?;
|
||||
println!("Server version: {}", version);
|
||||
|
||||
// List albums
|
||||
let albums = client.albums().list().execute().await?;
|
||||
println!("Found {} albums", albums.len());
|
||||
|
||||
// Create a new album
|
||||
let album = client
|
||||
.albums()
|
||||
.create()
|
||||
.name("My New Album")
|
||||
.execute()
|
||||
.await?;
|
||||
println!("Created album: {} (ID: {})", album.album_name, album.id);
|
||||
|
||||
// List assets
|
||||
let assets = client.assets().list().execute().await?;
|
||||
println!("Found {} assets", assets.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
59
examples/upload_photos.rs
Normal file
59
examples/upload_photos.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Example: Upload photos to Immich
|
||||
|
||||
use immich_sdk::Client;
|
||||
use std::path::Path;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a client
|
||||
let client = Client::from_url("https://immich.example.com")?.with_api_key("your-api-key");
|
||||
|
||||
// Path to the photo
|
||||
let photo_path = Path::new("/path/to/your/photo.jpg");
|
||||
|
||||
// Upload the photo
|
||||
println!("Uploading: {}", photo_path.display());
|
||||
|
||||
let result = client
|
||||
.assets()
|
||||
.upload()
|
||||
.file(photo_path)
|
||||
.device_asset_id("my-photo-001")
|
||||
.device_id("my-computer")
|
||||
.favorite()
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
match result.status {
|
||||
immich_sdk::models::AssetUploadStatus::Created => {
|
||||
println!("Successfully uploaded! Asset ID: {:?}", result.id);
|
||||
}
|
||||
immich_sdk::models::AssetUploadStatus::Duplicate => {
|
||||
println!("Photo already exists (duplicate)");
|
||||
}
|
||||
_ => {
|
||||
println!("Upload status: {:?}", result.status);
|
||||
}
|
||||
}
|
||||
|
||||
// Create an album and add the uploaded asset
|
||||
if let Some(asset_id) = result.id {
|
||||
let album = client
|
||||
.albums()
|
||||
.create()
|
||||
.name("Uploaded Photos")
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
client
|
||||
.albums()
|
||||
.add_assets(album.id)
|
||||
.asset_ids([asset_id])
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
println!("Added to album: {}", album.album_name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
287
src/apis/albums.rs
Normal file
287
src/apis/albums.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
//! Albums API - Manage photo albums
|
||||
|
||||
use crate::{
|
||||
Client,
|
||||
error::Result,
|
||||
models::{AlbumId, AlbumResponse, AssetId, CreateAlbumRequest, UpdateAlbumRequest},
|
||||
};
|
||||
|
||||
/// API for managing albums
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AlbumsApi {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl AlbumsApi {
|
||||
/// Create a new albums API instance
|
||||
pub const fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// List all albums
|
||||
pub fn list(&self) -> ListAlbumsBuilder {
|
||||
ListAlbumsBuilder::new(self.client.clone())
|
||||
}
|
||||
|
||||
/// Get a single album by ID
|
||||
pub fn get(&self, id: AlbumId) -> GetAlbumBuilder {
|
||||
GetAlbumBuilder::new(self.client.clone(), id)
|
||||
}
|
||||
|
||||
/// Create a new album
|
||||
pub fn create(&self) -> CreateAlbumBuilder {
|
||||
CreateAlbumBuilder::new(self.client.clone())
|
||||
}
|
||||
|
||||
/// Update an album
|
||||
pub fn update(&self, id: AlbumId) -> UpdateAlbumBuilder {
|
||||
UpdateAlbumBuilder::new(self.client.clone(), id)
|
||||
}
|
||||
|
||||
/// Delete an album
|
||||
pub fn delete(&self, id: AlbumId) -> DeleteAlbumBuilder {
|
||||
DeleteAlbumBuilder::new(self.client.clone(), id)
|
||||
}
|
||||
|
||||
/// Add assets to an album
|
||||
pub fn add_assets(&self, album_id: AlbumId) -> AddAssetsBuilder {
|
||||
AddAssetsBuilder::new(self.client.clone(), album_id)
|
||||
}
|
||||
|
||||
/// Remove assets from an album
|
||||
pub fn remove_assets(&self, album_id: AlbumId) -> RemoveAssetsBuilder {
|
||||
RemoveAssetsBuilder::new(self.client.clone(), album_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for listing albums
|
||||
#[derive(Debug)]
|
||||
pub struct ListAlbumsBuilder {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ListAlbumsBuilder {
|
||||
/// Create a new list builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<Vec<AlbumResponse>> {
|
||||
let req = self.client.get("/albums");
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let albums: Vec<AlbumResponse> = response.json().await?;
|
||||
Ok(albums)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for getting a single album
|
||||
#[derive(Debug)]
|
||||
pub struct GetAlbumBuilder {
|
||||
client: Client,
|
||||
id: AlbumId,
|
||||
}
|
||||
|
||||
impl GetAlbumBuilder {
|
||||
/// Create a new get builder
|
||||
const fn new(client: Client, id: AlbumId) -> Self {
|
||||
Self { client, id }
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
||||
let path = format!("/albums/{}", self.id);
|
||||
let req = self.client.get(&path);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let album: AlbumResponse = response.json().await?;
|
||||
Ok(album)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating an album
|
||||
#[derive(Debug)]
|
||||
pub struct CreateAlbumBuilder {
|
||||
client: Client,
|
||||
name: Option<String>,
|
||||
asset_ids: Vec<AssetId>,
|
||||
}
|
||||
|
||||
impl CreateAlbumBuilder {
|
||||
/// Create a new create builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
name: None,
|
||||
asset_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the album name
|
||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add asset IDs to include in the album
|
||||
pub fn asset_ids(mut self, ids: impl IntoIterator<Item = AssetId>) -> Self {
|
||||
self.asset_ids.extend(ids);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
||||
let name = self.name.ok_or_else(|| {
|
||||
crate::error::ImmichError::Validation("Album name is required".to_string())
|
||||
})?;
|
||||
|
||||
let body = CreateAlbumRequest {
|
||||
album_name: name,
|
||||
asset_ids: self.asset_ids,
|
||||
album_users: Vec::new(),
|
||||
};
|
||||
|
||||
let req = self.client.post("/albums").json(&body);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let album: AlbumResponse = response.json().await?;
|
||||
Ok(album)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for updating an album
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateAlbumBuilder {
|
||||
client: Client,
|
||||
id: AlbumId,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateAlbumBuilder {
|
||||
/// Create a new update builder
|
||||
const fn new(client: Client, id: AlbumId) -> Self {
|
||||
Self {
|
||||
client,
|
||||
id,
|
||||
name: None,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the album name
|
||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the album description
|
||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
||||
let body = UpdateAlbumRequest {
|
||||
album_name: self.name,
|
||||
description: self.description,
|
||||
};
|
||||
|
||||
let path = format!("/albums/{}", self.id);
|
||||
let req = self.client.patch(&path).json(&body);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let album: AlbumResponse = response.json().await?;
|
||||
Ok(album)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for deleting an album
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteAlbumBuilder {
|
||||
client: Client,
|
||||
id: AlbumId,
|
||||
}
|
||||
|
||||
impl DeleteAlbumBuilder {
|
||||
/// Create a new delete builder
|
||||
const fn new(client: Client, id: AlbumId) -> Self {
|
||||
Self { client, id }
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<()> {
|
||||
let path = format!("/albums/{}", self.id);
|
||||
let req = self.client.delete(&path);
|
||||
let _response = self.client.execute(req.build()?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for adding assets to an album
|
||||
#[derive(Debug)]
|
||||
pub struct AddAssetsBuilder {
|
||||
client: Client,
|
||||
album_id: AlbumId,
|
||||
asset_ids: Vec<AssetId>,
|
||||
}
|
||||
|
||||
impl AddAssetsBuilder {
|
||||
/// Create a new add assets builder
|
||||
const fn new(client: Client, album_id: AlbumId) -> Self {
|
||||
Self {
|
||||
client,
|
||||
album_id,
|
||||
asset_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add asset IDs to include
|
||||
pub fn asset_ids(mut self, ids: impl IntoIterator<Item = AssetId>) -> Self {
|
||||
self.asset_ids.extend(ids);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
||||
let path = format!("/albums/{}/assets", self.album_id);
|
||||
let body = serde_json::json!({ "ids": self.asset_ids });
|
||||
let req = self.client.put(&path).json(&body);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let album: AlbumResponse = response.json().await?;
|
||||
Ok(album)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for removing assets from an album
|
||||
#[derive(Debug)]
|
||||
pub struct RemoveAssetsBuilder {
|
||||
client: Client,
|
||||
album_id: AlbumId,
|
||||
asset_ids: Vec<AssetId>,
|
||||
}
|
||||
|
||||
impl RemoveAssetsBuilder {
|
||||
/// Create a new remove assets builder
|
||||
const fn new(client: Client, album_id: AlbumId) -> Self {
|
||||
Self {
|
||||
client,
|
||||
album_id,
|
||||
asset_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add asset IDs to remove
|
||||
pub fn asset_ids(mut self, ids: impl IntoIterator<Item = AssetId>) -> Self {
|
||||
self.asset_ids.extend(ids);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AlbumResponse> {
|
||||
let path = format!("/albums/{}/assets", self.album_id);
|
||||
let body = serde_json::json!({ "ids": self.asset_ids });
|
||||
let req = self.client.delete(&path).json(&body);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let album: AlbumResponse = response.json().await?;
|
||||
Ok(album)
|
||||
}
|
||||
}
|
||||
257
src/apis/assets.rs
Normal file
257
src/apis/assets.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Assets API - Manage photos and videos
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
Client,
|
||||
error::{ImmichError, Result},
|
||||
models::{AssetId, AssetResponse, AssetUploadResponse, DeleteAssetsRequest},
|
||||
};
|
||||
|
||||
/// API for managing assets (photos and videos)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AssetsApi {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl AssetsApi {
|
||||
/// Create a new assets API instance
|
||||
pub const fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// List assets with optional filters
|
||||
pub fn list(&self) -> ListAssetsBuilder {
|
||||
ListAssetsBuilder::new(self.client.clone())
|
||||
}
|
||||
|
||||
/// Get a single asset by ID
|
||||
pub fn get(&self, id: AssetId) -> GetAssetBuilder {
|
||||
GetAssetBuilder::new(self.client.clone(), id)
|
||||
}
|
||||
|
||||
/// Upload a new asset
|
||||
pub fn upload(&self) -> UploadAssetBuilder {
|
||||
UploadAssetBuilder::new(self.client.clone())
|
||||
}
|
||||
|
||||
/// Delete assets
|
||||
pub fn delete(&self) -> DeleteAssetsBuilder {
|
||||
DeleteAssetsBuilder::new(self.client.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for listing assets
|
||||
#[derive(Debug)]
|
||||
pub struct ListAssetsBuilder {
|
||||
client: Client,
|
||||
album_id: Option<AssetId>,
|
||||
is_favorite: Option<bool>,
|
||||
is_trashed: Option<bool>,
|
||||
}
|
||||
|
||||
impl ListAssetsBuilder {
|
||||
/// Create a new list builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
album_id: None,
|
||||
is_favorite: None,
|
||||
is_trashed: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter by album ID
|
||||
pub fn with_album_id(mut self, album_id: AssetId) -> Self {
|
||||
self.album_id = Some(album_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by favorite status
|
||||
pub fn is_favorite(mut self, favorite: bool) -> Self {
|
||||
self.is_favorite = Some(favorite);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by trash status
|
||||
pub fn is_trashed(mut self, trashed: bool) -> Self {
|
||||
self.is_trashed = Some(trashed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<Vec<AssetResponse>> {
|
||||
let mut req = self.client.get("/assets");
|
||||
|
||||
if let Some(album_id) = self.album_id {
|
||||
req = req.query(&[("albumId", album_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(is_favorite) = self.is_favorite {
|
||||
req = req.query(&[("isFavorite", is_favorite.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(is_trashed) = self.is_trashed {
|
||||
req = req.query(&[("isTrashed", is_trashed.to_string())]);
|
||||
}
|
||||
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let assets: Vec<AssetResponse> = response.json().await?;
|
||||
|
||||
Ok(assets)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for getting a single asset
|
||||
#[derive(Debug)]
|
||||
pub struct GetAssetBuilder {
|
||||
client: Client,
|
||||
id: AssetId,
|
||||
}
|
||||
|
||||
impl GetAssetBuilder {
|
||||
/// Create a new get builder
|
||||
const fn new(client: Client, id: AssetId) -> Self {
|
||||
Self { client, id }
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<AssetResponse> {
|
||||
let path = format!("/assets/{}", self.id);
|
||||
let req = self.client.get(&path);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let asset: AssetResponse = response.json().await?;
|
||||
|
||||
Ok(asset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for uploading an asset
|
||||
#[derive(Debug)]
|
||||
pub struct UploadAssetBuilder {
|
||||
client: Client,
|
||||
file_path: Option<String>,
|
||||
device_asset_id: Option<String>,
|
||||
device_id: Option<String>,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
impl UploadAssetBuilder {
|
||||
/// Create a new upload builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
file_path: None,
|
||||
device_asset_id: None,
|
||||
device_id: None,
|
||||
is_favorite: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the file path to upload
|
||||
pub fn file(mut self, path: impl AsRef<Path>) -> Self {
|
||||
self.file_path = Some(path.as_ref().to_string_lossy().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the device asset ID (required)
|
||||
pub fn device_asset_id(mut self, id: impl Into<String>) -> Self {
|
||||
self.device_asset_id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the device ID (required)
|
||||
pub fn device_id(mut self, id: impl Into<String>) -> Self {
|
||||
self.device_id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as favorite
|
||||
pub fn favorite(mut self) -> Self {
|
||||
self.is_favorite = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the upload
|
||||
pub async fn execute(self) -> Result<AssetUploadResponse> {
|
||||
let file_path = self
|
||||
.file_path
|
||||
.ok_or_else(|| ImmichError::Validation("File path is required".to_string()))?;
|
||||
|
||||
// Read file
|
||||
let file_content = tokio::fs::read(&file_path).await?;
|
||||
let file_name = std::path::Path::new(&file_path)
|
||||
.file_name()
|
||||
.ok_or_else(|| ImmichError::Validation("Invalid file path".to_string()))?
|
||||
.to_string_lossy();
|
||||
|
||||
// Build multipart form
|
||||
let mut form = reqwest::multipart::Form::new().part(
|
||||
"assetData",
|
||||
reqwest::multipart::Part::bytes(file_content).file_name(file_name.to_string()),
|
||||
);
|
||||
|
||||
if let Some(device_asset_id) = self.device_asset_id {
|
||||
form = form.text("deviceAssetId", device_asset_id);
|
||||
}
|
||||
|
||||
if let Some(device_id) = self.device_id {
|
||||
form = form.text("deviceId", device_id);
|
||||
}
|
||||
|
||||
form = form.text("isFavorite", self.is_favorite.to_string());
|
||||
|
||||
let req = self.client.post("/assets").multipart(form);
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let result: AssetUploadResponse = response.json().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for deleting assets
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteAssetsBuilder {
|
||||
client: Client,
|
||||
ids: Vec<AssetId>,
|
||||
}
|
||||
|
||||
impl DeleteAssetsBuilder {
|
||||
/// Create a new delete builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an asset ID to delete
|
||||
pub fn id(mut self, id: AssetId) -> Self {
|
||||
self.ids.push(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple asset IDs to delete
|
||||
pub fn ids(mut self, ids: impl IntoIterator<Item = AssetId>) -> Self {
|
||||
self.ids.extend(ids);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the deletion
|
||||
pub async fn execute(self) -> Result<()> {
|
||||
if self.ids.is_empty() {
|
||||
return Err(ImmichError::Validation(
|
||||
"At least one asset ID is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let body = DeleteAssetsRequest {
|
||||
ids: self.ids,
|
||||
force: false,
|
||||
};
|
||||
let req = self.client.delete("/assets").json(&body);
|
||||
let _response = self.client.execute(req.build()?).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
12
src/apis/mod.rs
Normal file
12
src/apis/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! API modules for interacting with Immich endpoints
|
||||
|
||||
pub mod albums;
|
||||
pub mod assets;
|
||||
pub mod server;
|
||||
pub mod timeline;
|
||||
|
||||
// Re-export main API modules
|
||||
pub use albums::AlbumsApi;
|
||||
pub use assets::AssetsApi;
|
||||
pub use server::ServerApi;
|
||||
pub use timeline::TimelineApi;
|
||||
57
src/apis/server.rs
Normal file
57
src/apis/server.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Server API - Get server information
|
||||
|
||||
use crate::{
|
||||
Client,
|
||||
error::Result,
|
||||
models::{ServerAbout, ServerFeatures, ServerVersion},
|
||||
};
|
||||
|
||||
/// API for server information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerApi {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ServerApi {
|
||||
/// Create a new server API instance
|
||||
pub const fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Get server version
|
||||
pub async fn version(&self) -> Result<ServerVersion> {
|
||||
let req = self.client.get("/server/version");
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let version: ServerVersion = response.json().await?;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Get server features
|
||||
pub async fn features(&self) -> Result<ServerFeatures> {
|
||||
let req = self.client.get("/server/features");
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let features: ServerFeatures = response.json().await?;
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
/// Get server about info
|
||||
pub async fn about(&self) -> Result<ServerAbout> {
|
||||
let req = self.client.get("/server/about");
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let about: ServerAbout = response.json().await?;
|
||||
Ok(about)
|
||||
}
|
||||
|
||||
/// Ping the server
|
||||
pub async fn ping(&self) -> Result<String> {
|
||||
let req = self.client.get("/server/ping");
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let ping_response: serde_json::Value = response.json().await?;
|
||||
// The ping endpoint typically returns {"res": "pong"}
|
||||
let res = ping_response
|
||||
.get("res")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
Ok(res.to_string())
|
||||
}
|
||||
}
|
||||
395
src/apis/timeline.rs
Normal file
395
src/apis/timeline.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
//! Timeline API - Get time-bucketed views of assets
|
||||
|
||||
use crate::{
|
||||
Client,
|
||||
error::Result,
|
||||
models::{AssetId, AssetOrder, AssetVisibility, TimeBucketAssetResponse, TimeBucketResponse},
|
||||
};
|
||||
|
||||
/// API for timeline operations
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimelineApi {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl TimelineApi {
|
||||
/// Create a new timeline API instance
|
||||
pub const fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// List all time buckets
|
||||
///
|
||||
/// Retrieves a list of all minimal time buckets for organizing assets by date.
|
||||
pub fn buckets(&self) -> ListTimeBucketsBuilder {
|
||||
ListTimeBucketsBuilder::new(self.client.clone())
|
||||
}
|
||||
|
||||
/// Get assets in a specific time bucket
|
||||
///
|
||||
/// Retrieves all asset IDs and metadata for a given time bucket.
|
||||
pub fn bucket(&self, time_bucket: impl Into<String>) -> GetTimeBucketBuilder {
|
||||
GetTimeBucketBuilder::new(self.client.clone(), time_bucket.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for listing time buckets
|
||||
#[derive(Debug)]
|
||||
pub struct ListTimeBucketsBuilder {
|
||||
client: Client,
|
||||
album_id: Option<AssetId>,
|
||||
bbox: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
is_trashed: Option<bool>,
|
||||
order: Option<AssetOrder>,
|
||||
person_id: Option<AssetId>,
|
||||
tag_id: Option<AssetId>,
|
||||
user_id: Option<AssetId>,
|
||||
visibility: Option<AssetVisibility>,
|
||||
with_coordinates: Option<bool>,
|
||||
with_partners: Option<bool>,
|
||||
with_stacked: Option<bool>,
|
||||
}
|
||||
|
||||
impl ListTimeBucketsBuilder {
|
||||
/// Create a new list time buckets builder
|
||||
const fn new(client: Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
album_id: None,
|
||||
bbox: None,
|
||||
is_favorite: None,
|
||||
is_trashed: None,
|
||||
order: None,
|
||||
person_id: None,
|
||||
tag_id: None,
|
||||
user_id: None,
|
||||
visibility: None,
|
||||
with_coordinates: None,
|
||||
with_partners: None,
|
||||
with_stacked: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter by album ID
|
||||
pub fn album_id(mut self, album_id: AssetId) -> Self {
|
||||
self.album_id = Some(album_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by bounding box coordinates (west,south,east,north in WGS84)
|
||||
pub fn bbox(mut self, bbox: impl Into<String>) -> Self {
|
||||
self.bbox = Some(bbox.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by favorite status
|
||||
pub fn is_favorite(mut self, favorite: bool) -> Self {
|
||||
self.is_favorite = Some(favorite);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by trash status
|
||||
pub fn is_trashed(mut self, trashed: bool) -> Self {
|
||||
self.is_trashed = Some(trashed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sort order (ASC for oldest first, DESC for newest first)
|
||||
pub fn order(mut self, order: AssetOrder) -> Self {
|
||||
self.order = Some(order);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by person ID (face recognition)
|
||||
pub fn person_id(mut self, person_id: AssetId) -> Self {
|
||||
self.person_id = Some(person_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by tag ID
|
||||
pub fn tag_id(mut self, tag_id: AssetId) -> Self {
|
||||
self.tag_id = Some(tag_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by user ID
|
||||
pub fn user_id(mut self, user_id: AssetId) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by visibility status
|
||||
pub fn visibility(mut self, visibility: AssetVisibility) -> Self {
|
||||
self.visibility = Some(visibility);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include location data in the response
|
||||
pub fn with_coordinates(mut self) -> Self {
|
||||
self.with_coordinates = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include assets shared by partners
|
||||
pub fn with_partners(mut self) -> Self {
|
||||
self.with_partners = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include stacked assets (only primary assets from stacks when true)
|
||||
pub fn with_stacked(mut self) -> Self {
|
||||
self.with_stacked = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<Vec<TimeBucketResponse>> {
|
||||
let mut req = self.client.get("/timeline/buckets");
|
||||
|
||||
if let Some(album_id) = self.album_id {
|
||||
req = req.query(&[("albumId", album_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref bbox) = self.bbox {
|
||||
req = req.query(&[("bbox", bbox.as_str())]);
|
||||
}
|
||||
|
||||
if let Some(is_favorite) = self.is_favorite {
|
||||
req = req.query(&[("isFavorite", is_favorite.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(is_trashed) = self.is_trashed {
|
||||
req = req.query(&[("isTrashed", is_trashed.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref order) = self.order {
|
||||
let order_str = match order {
|
||||
AssetOrder::Asc => "ASC",
|
||||
AssetOrder::Desc => "DESC",
|
||||
};
|
||||
req = req.query(&[("order", order_str)]);
|
||||
}
|
||||
|
||||
if let Some(person_id) = self.person_id {
|
||||
req = req.query(&[("personId", person_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(tag_id) = self.tag_id {
|
||||
req = req.query(&[("tagId", tag_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(user_id) = self.user_id {
|
||||
req = req.query(&[("userId", user_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref visibility) = self.visibility {
|
||||
let visibility_str = match visibility {
|
||||
AssetVisibility::Timeline => "timeline",
|
||||
AssetVisibility::Archived => "archive",
|
||||
AssetVisibility::Hidden => "hidden",
|
||||
AssetVisibility::Locked => "locked",
|
||||
};
|
||||
req = req.query(&[("visibility", visibility_str)]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_coordinates {
|
||||
req = req.query(&[("withCoordinates", "true")]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_partners {
|
||||
req = req.query(&[("withPartners", "true")]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_stacked {
|
||||
req = req.query(&[("withStacked", "true")]);
|
||||
}
|
||||
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let buckets: Vec<TimeBucketResponse> = response.json().await?;
|
||||
Ok(buckets)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for getting a specific time bucket
|
||||
#[derive(Debug)]
|
||||
pub struct GetTimeBucketBuilder {
|
||||
client: Client,
|
||||
time_bucket: String,
|
||||
album_id: Option<AssetId>,
|
||||
bbox: Option<String>,
|
||||
is_favorite: Option<bool>,
|
||||
is_trashed: Option<bool>,
|
||||
order: Option<AssetOrder>,
|
||||
person_id: Option<AssetId>,
|
||||
tag_id: Option<AssetId>,
|
||||
user_id: Option<AssetId>,
|
||||
visibility: Option<AssetVisibility>,
|
||||
with_coordinates: Option<bool>,
|
||||
with_partners: Option<bool>,
|
||||
with_stacked: Option<bool>,
|
||||
}
|
||||
|
||||
impl GetTimeBucketBuilder {
|
||||
/// Create a new get time bucket builder
|
||||
fn new(client: Client, time_bucket: String) -> Self {
|
||||
Self {
|
||||
client,
|
||||
time_bucket,
|
||||
album_id: None,
|
||||
bbox: None,
|
||||
is_favorite: None,
|
||||
is_trashed: None,
|
||||
order: None,
|
||||
person_id: None,
|
||||
tag_id: None,
|
||||
user_id: None,
|
||||
visibility: None,
|
||||
with_coordinates: None,
|
||||
with_partners: None,
|
||||
with_stacked: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter by album ID
|
||||
pub fn album_id(mut self, album_id: AssetId) -> Self {
|
||||
self.album_id = Some(album_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by bounding box coordinates (west,south,east,north in WGS84)
|
||||
pub fn bbox(mut self, bbox: impl Into<String>) -> Self {
|
||||
self.bbox = Some(bbox.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by favorite status
|
||||
pub fn is_favorite(mut self, favorite: bool) -> Self {
|
||||
self.is_favorite = Some(favorite);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by trash status
|
||||
pub fn is_trashed(mut self, trashed: bool) -> Self {
|
||||
self.is_trashed = Some(trashed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sort order (ASC for oldest first, DESC for newest first)
|
||||
pub fn order(mut self, order: AssetOrder) -> Self {
|
||||
self.order = Some(order);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by person ID (face recognition)
|
||||
pub fn person_id(mut self, person_id: AssetId) -> Self {
|
||||
self.person_id = Some(person_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by tag ID
|
||||
pub fn tag_id(mut self, tag_id: AssetId) -> Self {
|
||||
self.tag_id = Some(tag_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by user ID
|
||||
pub fn user_id(mut self, user_id: AssetId) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter by visibility status
|
||||
pub fn visibility(mut self, visibility: AssetVisibility) -> Self {
|
||||
self.visibility = Some(visibility);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include location data in the response
|
||||
pub fn with_coordinates(mut self) -> Self {
|
||||
self.with_coordinates = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include assets shared by partners
|
||||
pub fn with_partners(mut self) -> Self {
|
||||
self.with_partners = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Include stacked assets (only primary assets from stacks when true)
|
||||
pub fn with_stacked(mut self) -> Self {
|
||||
self.with_stacked = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the request
|
||||
pub async fn execute(self) -> Result<TimeBucketAssetResponse> {
|
||||
let mut req = self
|
||||
.client
|
||||
.get("/timeline/bucket")
|
||||
.query(&[("timeBucket", &self.time_bucket)]);
|
||||
|
||||
if let Some(album_id) = self.album_id {
|
||||
req = req.query(&[("albumId", album_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref bbox) = self.bbox {
|
||||
req = req.query(&[("bbox", bbox.as_str())]);
|
||||
}
|
||||
|
||||
if let Some(is_favorite) = self.is_favorite {
|
||||
req = req.query(&[("isFavorite", is_favorite.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(is_trashed) = self.is_trashed {
|
||||
req = req.query(&[("isTrashed", is_trashed.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref order) = self.order {
|
||||
let order_str = match order {
|
||||
AssetOrder::Asc => "ASC",
|
||||
AssetOrder::Desc => "DESC",
|
||||
};
|
||||
req = req.query(&[("order", order_str)]);
|
||||
}
|
||||
|
||||
if let Some(person_id) = self.person_id {
|
||||
req = req.query(&[("personId", person_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(tag_id) = self.tag_id {
|
||||
req = req.query(&[("tagId", tag_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(user_id) = self.user_id {
|
||||
req = req.query(&[("userId", user_id.to_string())]);
|
||||
}
|
||||
|
||||
if let Some(ref visibility) = self.visibility {
|
||||
let visibility_str = match visibility {
|
||||
AssetVisibility::Timeline => "timeline",
|
||||
AssetVisibility::Archived => "archive",
|
||||
AssetVisibility::Hidden => "hidden",
|
||||
AssetVisibility::Locked => "locked",
|
||||
};
|
||||
req = req.query(&[("visibility", visibility_str)]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_coordinates {
|
||||
req = req.query(&[("withCoordinates", "true")]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_partners {
|
||||
req = req.query(&[("withPartners", "true")]);
|
||||
}
|
||||
|
||||
if let Some(true) = self.with_stacked {
|
||||
req = req.query(&[("withStacked", "true")]);
|
||||
}
|
||||
|
||||
let response = self.client.execute(req.build()?).await?;
|
||||
let bucket: TimeBucketAssetResponse = response.json().await?;
|
||||
Ok(bucket)
|
||||
}
|
||||
}
|
||||
232
src/client.rs
Normal file
232
src/client.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
//! Client for interacting with the Immich API
|
||||
|
||||
use crate::apis::{AlbumsApi, AssetsApi, ServerApi, TimelineApi};
|
||||
use crate::error::{ImmichError, Result};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for the Immich client
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Base URL of the Immich server
|
||||
pub base_url: String,
|
||||
/// API key for authentication
|
||||
pub api_key: Option<String>,
|
||||
/// Request timeout
|
||||
pub timeout: Duration,
|
||||
/// User agent string
|
||||
pub user_agent: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: String::new(),
|
||||
api_key: None,
|
||||
timeout: Duration::from_secs(30),
|
||||
user_agent: format!("immich-sdk/{} (Rust)", env!("CARGO_PKG_VERSION")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new config with the given base URL
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the API key
|
||||
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
|
||||
self.api_key = Some(api_key.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a custom user agent
|
||||
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
|
||||
self.user_agent = user_agent.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Client for making requests to the Immich API
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
config: Config,
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client with the given configuration
|
||||
pub fn new(config: Config) -> Result<Self> {
|
||||
// Validate base URL
|
||||
let base_url = if config.base_url.ends_with('/') {
|
||||
config.base_url.trim_end_matches('/').to_string()
|
||||
} else {
|
||||
config.base_url.clone()
|
||||
};
|
||||
|
||||
if base_url.is_empty() {
|
||||
return Err(ImmichError::Config("Base URL cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
// Parse to validate URL
|
||||
let _ = url::Url::parse(&base_url)?;
|
||||
|
||||
let http = reqwest::Client::builder()
|
||||
.timeout(config.timeout)
|
||||
.user_agent(&config.user_agent)
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
config: Config { base_url, ..config },
|
||||
http,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a client from a base URL string
|
||||
pub fn from_url(base_url: impl Into<String>) -> Result<Self> {
|
||||
Self::new(Config::new(base_url))
|
||||
}
|
||||
|
||||
/// Set the API key
|
||||
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
|
||||
self.config.api_key = Some(api_key.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Get the base URL
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.config.base_url
|
||||
}
|
||||
|
||||
/// Check if the client has an API key configured
|
||||
pub fn has_api_key(&self) -> bool {
|
||||
self.config.api_key.is_some()
|
||||
}
|
||||
|
||||
/// Get the underlying HTTP client
|
||||
pub fn http(&self) -> &reqwest::Client {
|
||||
&self.http
|
||||
}
|
||||
|
||||
/// Create an authenticated request builder
|
||||
pub fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
|
||||
let url = format!("{}/api{}", self.config.base_url, path);
|
||||
let mut builder = self.http.request(method, &url);
|
||||
|
||||
// Add API key authentication
|
||||
if let Some(ref api_key) = self.config.api_key {
|
||||
builder = builder.header("x-api-key", api_key);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Execute a request and handle common error cases
|
||||
pub async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response> {
|
||||
let response = self.http.execute(request).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(ImmichError::from_response(response).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a GET request
|
||||
pub fn get(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
self.request(reqwest::Method::GET, path)
|
||||
}
|
||||
|
||||
/// Make a POST request
|
||||
pub fn post(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
self.request(reqwest::Method::POST, path)
|
||||
}
|
||||
|
||||
/// Make a PUT request
|
||||
pub fn put(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
self.request(reqwest::Method::PUT, path)
|
||||
}
|
||||
|
||||
/// Make a DELETE request
|
||||
pub fn delete(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
self.request(reqwest::Method::DELETE, path)
|
||||
}
|
||||
|
||||
/// Make a PATCH request
|
||||
pub fn patch(&self, path: &str) -> reqwest::RequestBuilder {
|
||||
self.request(reqwest::Method::PATCH, path)
|
||||
}
|
||||
|
||||
/// Access the albums API
|
||||
pub fn albums(&self) -> AlbumsApi {
|
||||
AlbumsApi::new(self.clone())
|
||||
}
|
||||
|
||||
/// Access the assets API
|
||||
pub fn assets(&self) -> AssetsApi {
|
||||
AssetsApi::new(self.clone())
|
||||
}
|
||||
|
||||
/// Access the server API
|
||||
pub fn server(&self) -> ServerApi {
|
||||
ServerApi::new(self.clone())
|
||||
}
|
||||
|
||||
/// Access the timeline API
|
||||
pub fn timeline(&self) -> TimelineApi {
|
||||
TimelineApi::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_builder() {
|
||||
let config = Config::new("https://example.com")
|
||||
.with_api_key("test-key")
|
||||
.with_timeout(Duration::from_secs(60));
|
||||
|
||||
assert_eq!(config.base_url, "https://example.com");
|
||||
assert_eq!(config.api_key, Some("test-key".to_string()));
|
||||
assert_eq!(config.timeout, Duration::from_secs(60));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_creation() {
|
||||
let client = Client::from_url("https://example.com").unwrap();
|
||||
assert_eq!(client.base_url(), "https://example.com");
|
||||
assert!(!client.has_api_key());
|
||||
|
||||
let config = Config::new("https://example.com").with_api_key("test-key");
|
||||
let client = Client::new(config).unwrap();
|
||||
assert!(client.has_api_key());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_normalization() {
|
||||
let client = Client::from_url("https://example.com/").unwrap();
|
||||
assert_eq!(client.base_url(), "https://example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_url_error() {
|
||||
let result = Client::from_url("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
113
src/error.rs
Normal file
113
src/error.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Error types for the Immich SDK
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Main error type for the Immich SDK
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ImmichError {
|
||||
/// HTTP request failed
|
||||
#[error("HTTP request failed: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
/// Authentication failed
|
||||
#[error("Authentication failed: {0}")]
|
||||
Authentication(String),
|
||||
|
||||
/// Resource not found
|
||||
#[error("Resource not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// Rate limit exceeded
|
||||
#[error("Rate limit exceeded. Retry after: {0:?}")]
|
||||
RateLimited(Option<std::time::Duration>),
|
||||
|
||||
/// API returned an error
|
||||
#[error("API error: {status} - {message}")]
|
||||
Api {
|
||||
/// HTTP status code
|
||||
status: reqwest::StatusCode,
|
||||
/// Error message from API
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Serialization/deserialization error
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// URL parsing error
|
||||
#[error("URL error: {0}")]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
/// Invalid configuration
|
||||
#[error("Invalid configuration: {0}")]
|
||||
Config(String),
|
||||
|
||||
/// Validation error
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
|
||||
/// File I/O error
|
||||
#[error("File error: {0}")]
|
||||
File(#[from] std::io::Error),
|
||||
|
||||
/// Unknown error
|
||||
#[error("Unknown error: {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
/// Result type alias for Immich SDK
|
||||
pub type Result<T> = std::result::Result<T, ImmichError>;
|
||||
|
||||
/// HTTP status code mapping for common errors
|
||||
impl ImmichError {
|
||||
/// Create an error from an HTTP response
|
||||
pub async fn from_response(response: reqwest::Response) -> Self {
|
||||
let status = response.status();
|
||||
let message = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
|
||||
match status {
|
||||
reqwest::StatusCode::UNAUTHORIZED => ImmichError::Authentication(message),
|
||||
reqwest::StatusCode::NOT_FOUND => ImmichError::NotFound(message),
|
||||
reqwest::StatusCode::TOO_MANY_REQUESTS => {
|
||||
// Parse retry-after header if present
|
||||
let retry_after = None; // Could parse header here
|
||||
ImmichError::RateLimited(retry_after)
|
||||
}
|
||||
_ => ImmichError::Api { status, message },
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the error is a rate limit error
|
||||
pub fn is_rate_limited(&self) -> bool {
|
||||
matches!(self, ImmichError::RateLimited(_))
|
||||
}
|
||||
|
||||
/// Check if the error is an authentication error
|
||||
pub fn is_auth_error(&self) -> bool {
|
||||
matches!(self, ImmichError::Authentication(_))
|
||||
}
|
||||
|
||||
/// Check if the error is a "not found" error
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
matches!(self, ImmichError::NotFound(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_types() {
|
||||
let auth_err = ImmichError::Authentication("Invalid API key".to_string());
|
||||
assert!(auth_err.is_auth_error());
|
||||
assert!(!auth_err.is_not_found());
|
||||
|
||||
let not_found_err = ImmichError::NotFound("Asset not found".to_string());
|
||||
assert!(not_found_err.is_not_found());
|
||||
assert!(!not_found_err.is_auth_error());
|
||||
}
|
||||
}
|
||||
58
src/lib.rs
Normal file
58
src/lib.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! # Immich SDK
|
||||
//!
|
||||
//! A modern Rust SDK for the [Immich](https://immich.app/) photo and video management server.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **Async-first**: Built on `tokio` and `reqwest` for modern async Rust
|
||||
//! - **Builder pattern**: Ergonomic API with fluent builders
|
||||
//! - **Type-safe**: Strongly typed models
|
||||
//! - **Error handling**: Comprehensive error types with `thiserror`
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use immich_sdk::Client;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Create a client
|
||||
//! let client = Client::from_url("https://immich.example.com")?
|
||||
//! .with_api_key("your-api-key");
|
||||
//!
|
||||
//! // List albums
|
||||
//! let albums = client.albums().list().execute().await?;
|
||||
//! println!("Found {} albums", albums.len());
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Authentication
|
||||
//!
|
||||
//! The SDK supports API key authentication:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use immich_sdk::Client;
|
||||
//!
|
||||
//! let client = Client::from_url("https://immich.example.com")?
|
||||
//! .with_api_key("your-api-key-here");
|
||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod apis;
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
|
||||
// Re-export main types
|
||||
pub use client::{Client, Config};
|
||||
pub use error::{ImmichError, Result};
|
||||
|
||||
// Re-export models
|
||||
pub use models::*;
|
||||
|
||||
/// Immich API version this SDK targets
|
||||
pub const IMMICH_API_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
353
src/models/mod.rs
Normal file
353
src/models/mod.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
//! Data models for the Immich API
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Asset ID type alias
|
||||
pub type AssetId = Uuid;
|
||||
|
||||
/// Album ID type alias
|
||||
pub type AlbumId = Uuid;
|
||||
|
||||
/// User ID type alias
|
||||
pub type UserId = Uuid;
|
||||
|
||||
/// Asset response from the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetResponse {
|
||||
/// Asset ID
|
||||
pub id: AssetId,
|
||||
/// Device asset ID
|
||||
pub device_asset_id: String,
|
||||
/// Device ID
|
||||
pub device_id: String,
|
||||
/// Asset type (IMAGE or VIDEO)
|
||||
#[serde(rename = "type")]
|
||||
pub asset_type: AssetType,
|
||||
/// Original file name
|
||||
pub original_file_name: String,
|
||||
/// Original mime type
|
||||
pub original_mime_type: String,
|
||||
/// File size in bytes
|
||||
pub exif_info: Option<ExifInfo>,
|
||||
/// Whether asset is a favorite
|
||||
pub is_favorite: bool,
|
||||
/// Whether asset is archived
|
||||
pub is_archived: bool,
|
||||
/// Whether asset is trashed
|
||||
pub is_trashed: bool,
|
||||
/// Created at timestamp
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Updated at timestamp
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Asset type enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum AssetType {
|
||||
/// Image file
|
||||
Image,
|
||||
/// Video file
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Asset visibility enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssetVisibility {
|
||||
/// Visible in timeline
|
||||
Timeline,
|
||||
/// Archived
|
||||
Archived,
|
||||
/// Hidden
|
||||
Hidden,
|
||||
/// Locked
|
||||
Locked,
|
||||
}
|
||||
|
||||
/// EXIF information for an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExifInfo {
|
||||
/// File size in bytes
|
||||
pub file_size_in_byte: Option<i64>,
|
||||
/// Image dimensions
|
||||
pub exif_image_height: Option<i32>,
|
||||
/// Image width
|
||||
pub exif_image_width: Option<i32>,
|
||||
/// Orientation
|
||||
pub orientation: Option<String>,
|
||||
/// Date taken
|
||||
pub date_time_original: Option<DateTime<Utc>>,
|
||||
/// GPS latitude
|
||||
pub latitude: Option<f64>,
|
||||
/// GPS longitude
|
||||
pub longitude: Option<f64>,
|
||||
/// Camera make
|
||||
pub make: Option<String>,
|
||||
/// Camera model
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
/// Album response from the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumResponse {
|
||||
/// Album ID
|
||||
pub id: AlbumId,
|
||||
/// Album name
|
||||
pub album_name: String,
|
||||
/// Album description
|
||||
pub description: String,
|
||||
/// Album cover thumbnail asset ID
|
||||
pub album_thumbnail_asset_id: Option<AssetId>,
|
||||
/// Number of assets in album
|
||||
pub asset_count: i64,
|
||||
/// Assets in the album
|
||||
pub assets: Vec<AssetResponse>,
|
||||
/// Created at timestamp
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Updated at timestamp
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// Owner ID
|
||||
pub owner_id: UserId,
|
||||
}
|
||||
|
||||
/// User response from the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserResponse {
|
||||
/// User ID
|
||||
pub id: UserId,
|
||||
/// User email
|
||||
pub email: String,
|
||||
/// User name
|
||||
pub name: String,
|
||||
/// Whether user is admin
|
||||
pub is_admin: bool,
|
||||
/// Whether user has OAuth enabled
|
||||
pub oauth_enabled: bool,
|
||||
/// Storage usage in bytes
|
||||
pub storage_usage_in_bytes: i64,
|
||||
/// Created at timestamp
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Server version information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerVersion {
|
||||
/// Major version
|
||||
pub major: i32,
|
||||
/// Minor version
|
||||
pub minor: i32,
|
||||
/// Patch version
|
||||
pub patch: i32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Server features information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerFeatures {
|
||||
/// Whether OAuth is enabled
|
||||
pub oauth: bool,
|
||||
/// Whether OAuth auto launch is enabled
|
||||
pub oauth_auto_launch: bool,
|
||||
/// Whether password login is enabled
|
||||
pub password_login: bool,
|
||||
/// Whether config file is present
|
||||
pub config_file: bool,
|
||||
}
|
||||
|
||||
/// Server about information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerAbout {
|
||||
/// Version information
|
||||
pub version: ServerVersion,
|
||||
/// Version string
|
||||
pub version_url: String,
|
||||
}
|
||||
|
||||
/// Create album request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateAlbumRequest {
|
||||
/// Album name
|
||||
pub album_name: String,
|
||||
/// Asset IDs to add to album
|
||||
pub asset_ids: Vec<AssetId>,
|
||||
/// User IDs to share with
|
||||
pub album_users: Vec<AlbumUserCreate>,
|
||||
}
|
||||
|
||||
/// Album user creation info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AlbumUserCreate {
|
||||
/// User ID
|
||||
pub user_id: UserId,
|
||||
/// Role (viewer or editor)
|
||||
pub role: AlbumUserRole,
|
||||
}
|
||||
|
||||
/// Album user role
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AlbumUserRole {
|
||||
/// Can only view
|
||||
Viewer,
|
||||
/// Can edit
|
||||
Editor,
|
||||
}
|
||||
|
||||
/// Update album request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateAlbumRequest {
|
||||
/// Album name
|
||||
pub album_name: Option<String>,
|
||||
/// Description
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Asset upload response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetUploadResponse {
|
||||
/// Upload status
|
||||
pub status: AssetUploadStatus,
|
||||
/// Asset ID if successful
|
||||
pub id: Option<AssetId>,
|
||||
/// Duplicate ID if duplicate
|
||||
pub duplicate: Option<AssetId>,
|
||||
}
|
||||
|
||||
/// Asset upload status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum AssetUploadStatus {
|
||||
/// Upload created new asset
|
||||
Created,
|
||||
/// Asset already exists
|
||||
Duplicate,
|
||||
/// Upload rejected
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// Delete assets request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteAssetsRequest {
|
||||
/// Asset IDs to delete
|
||||
pub ids: Vec<AssetId>,
|
||||
/// Force delete (skip trash)
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// API key response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeyResponse {
|
||||
/// Key ID
|
||||
pub id: String,
|
||||
/// Key name
|
||||
pub name: String,
|
||||
/// Created at
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Updated at
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Asset order enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum AssetOrder {
|
||||
/// Oldest first
|
||||
Asc,
|
||||
/// Newest first
|
||||
Desc,
|
||||
}
|
||||
|
||||
/// Time bucket response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimeBucketResponse {
|
||||
/// Number of assets in this time bucket
|
||||
pub count: i64,
|
||||
/// Time bucket identifier in YYYY-MM-DD format
|
||||
pub time_bucket: String,
|
||||
}
|
||||
|
||||
/// Time bucket asset response - contains arrays of asset data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimeBucketAssetResponse {
|
||||
/// Array of city names extracted from EXIF GPS data
|
||||
pub city: Vec<Option<String>>,
|
||||
/// Array of country names extracted from EXIF GPS data
|
||||
pub country: Vec<Option<String>>,
|
||||
/// Array of video durations in HH:MM:SS format (null for images)
|
||||
pub duration: Vec<Option<String>>,
|
||||
/// Array of file creation timestamps in UTC
|
||||
pub file_created_at: Vec<String>,
|
||||
/// Array of asset IDs in the time bucket
|
||||
pub id: Vec<String>,
|
||||
/// Array indicating whether each asset is favorited
|
||||
pub is_favorite: Vec<bool>,
|
||||
/// Array indicating whether each asset is an image (false for videos)
|
||||
pub is_image: Vec<bool>,
|
||||
/// Array indicating whether each asset is in the trash
|
||||
pub is_trashed: Vec<bool>,
|
||||
/// Array of latitude coordinates extracted from EXIF GPS data
|
||||
pub latitude: Vec<Option<f64>>,
|
||||
/// Array of live photo video asset IDs (null for non-live photos)
|
||||
pub live_photo_video_id: Vec<Option<String>>,
|
||||
/// Array of UTC offset hours at the time each photo was taken
|
||||
pub local_offset_hours: Vec<f64>,
|
||||
/// Array of longitude coordinates extracted from EXIF GPS data
|
||||
pub longitude: Vec<Option<f64>>,
|
||||
/// Array of owner IDs for each asset
|
||||
pub owner_id: Vec<String>,
|
||||
/// Array of projection types for 360° content
|
||||
pub projection_type: Vec<Option<String>>,
|
||||
/// Array of aspect ratios (width/height) for each asset
|
||||
pub ratio: Vec<f64>,
|
||||
/// Array of stack information as [stackId, assetCount] tuples
|
||||
pub stack: Vec<Option<Vec<String>>>,
|
||||
/// Array of BlurHash strings for generating asset previews
|
||||
pub thumbhash: Vec<Option<String>>,
|
||||
/// Array of visibility statuses for each asset
|
||||
pub visibility: Vec<AssetVisibility>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_asset_type_serialization() {
|
||||
let asset_type = AssetType::Image;
|
||||
let json = serde_json::to_string(&asset_type).unwrap();
|
||||
assert_eq!(json, r#""IMAGE""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_version_display() {
|
||||
let version = ServerVersion {
|
||||
major: 1,
|
||||
minor: 137,
|
||||
patch: 0,
|
||||
};
|
||||
assert_eq!(version.to_string(), "1.137.0");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user