diff --git a/AGENTS.md b/AGENTS.md index a2785fa..163cda6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,11 @@ All examples should be run using the provided wrapper script: ./scripts/run-example.sh upload_photos ./scripts/run-example.sh download_asset ./scripts/run-example.sh thumbnail +./scripts/run-example.sh search_metadata +./scripts/run-example.sh album_management +./scripts/run-example.sh timeline_browsing +./scripts/run-example.sh delete_assets +./scripts/run-example.sh server_info ``` The wrapper handles everything automatically: diff --git a/examples/server_info.rs b/examples/server_info.rs index 02ea818..33064fc 100644 --- a/examples/server_info.rs +++ b/examples/server_info.rs @@ -38,6 +38,13 @@ async fn main() -> Result<(), Box> { 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(()) } diff --git a/examples/upload_photos.rs b/examples/upload_photos.rs index 7efc591..a827bac 100644 --- a/examples/upload_photos.rs +++ b/examples/upload_photos.rs @@ -60,6 +60,7 @@ async fn main() -> Result<(), Box> { } // 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 { let album = client .albums() @@ -68,7 +69,7 @@ async fn main() -> Result<(), Box> { .execute() .await?; - client + let _add_result = client .albums() .add_assets(album.id) .asset_ids([asset_id]) diff --git a/src/apis/albums.rs b/src/apis/albums.rs index 5bdaa05..1fd87c3 100644 --- a/src/apis/albums.rs +++ b/src/apis/albums.rs @@ -241,13 +241,13 @@ impl AddAssetsBuilder { } /// Execute the request - pub async fn execute(self) -> Result { + pub async fn execute(self) -> Result> { 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) + let result: Vec = response.json().await?; + Ok(result) } } diff --git a/src/apis/assets.rs b/src/apis/assets.rs index f31d819..5b4ddc1 100644 --- a/src/apis/assets.rs +++ b/src/apis/assets.rs @@ -6,7 +6,10 @@ use std::path::Path; use crate::{ Client, error::{ImmichError, Result}, - models::{AssetId, AssetMediaSize, AssetResponse, AssetUploadResponse, DeleteAssetsRequest, MetadataSearchRequest, SearchResponse}, + models::{ + AssetId, AssetMediaSize, AssetResponse, AssetUploadResponse, DeleteAssetsRequest, + MetadataSearchRequest, SearchResponse, + }, }; /// Response from downloading a thumbnail containing image data and metadata @@ -161,7 +164,7 @@ impl ListAssetsBuilder { /// Execute the request pub async fn execute(self) -> Result> { let req = self.client.post("/search/metadata"); - + let mut body = MetadataSearchRequest::default(); if let Some(album_id) = self.album_id { @@ -215,6 +218,8 @@ pub struct UploadAssetBuilder { device_asset_id: Option, device_id: Option, is_favorite: bool, + file_created_at: Option, + file_modified_at: Option, } impl UploadAssetBuilder { @@ -226,6 +231,8 @@ impl UploadAssetBuilder { device_asset_id: None, device_id: None, is_favorite: false, + file_created_at: None, + file_modified_at: None, } } @@ -253,6 +260,18 @@ impl UploadAssetBuilder { self } + /// Set file creation timestamp (ISO 8601 format) + pub fn file_created_at(mut self, timestamp: impl Into) -> 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) -> Self { + self.file_modified_at = Some(timestamp.into()); + self + } + /// Execute the upload pub async fn execute(self) -> Result { let file_path = self @@ -282,6 +301,11 @@ impl UploadAssetBuilder { 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 response = self.client.execute(req.build()?).await?; let result: AssetUploadResponse = response.json().await?; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6cbd565..d9db54e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -186,10 +186,66 @@ pub struct ServerFeatures { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ServerAbout { - /// Version information - pub version: ServerVersion, - /// Version string + /// Server version (e.g., "v2.7.5") + pub version: String, + /// URL to version information pub version_url: String, + /// Build identifier + #[serde(skip_serializing_if = "Option::is_none")] + pub build: Option, + /// Build URL + #[serde(skip_serializing_if = "Option::is_none")] + pub build_url: Option, + /// Build image name + #[serde(skip_serializing_if = "Option::is_none")] + pub build_image: Option, + /// Build image URL + #[serde(skip_serializing_if = "Option::is_none")] + pub build_image_url: Option, + /// ExifTool version + #[serde(skip_serializing_if = "Option::is_none")] + pub exiftool: Option, + /// FFmpeg version + #[serde(skip_serializing_if = "Option::is_none")] + pub ffmpeg: Option, + /// ImageMagick version + #[serde(skip_serializing_if = "Option::is_none")] + pub imagemagick: Option, + /// libvips version + #[serde(skip_serializing_if = "Option::is_none")] + pub libvips: Option, + /// Node.js version + #[serde(skip_serializing_if = "Option::is_none")] + pub nodejs: Option, + /// Repository name + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// Repository URL + #[serde(skip_serializing_if = "Option::is_none")] + pub repository_url: Option, + /// Source commit hash + #[serde(skip_serializing_if = "Option::is_none")] + pub source_commit: Option, + /// Source reference (branch/tag) + #[serde(skip_serializing_if = "Option::is_none")] + pub source_ref: Option, + /// Source URL + #[serde(skip_serializing_if = "Option::is_none")] + pub source_url: Option, + /// Third-party bug/feature URL + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_bug_feature_url: Option, + /// Third-party documentation URL + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_documentation_url: Option, + /// Third-party source URL + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_source_url: Option, + /// Third-party support URL + #[serde(skip_serializing_if = "Option::is_none")] + pub third_party_support_url: Option, + /// Whether the server is licensed + pub licensed: bool, } /// Create album request @@ -248,7 +304,7 @@ pub struct AssetUploadResponse { /// Asset upload status #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "snake_case")] pub enum AssetUploadStatus { /// Upload created new asset Created, @@ -284,7 +340,7 @@ pub struct ApiKeyResponse { /// Asset order enumeration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "lowercase")] pub enum AssetOrder { /// Oldest first Asc,