Fix examples and SDK for Immich v2 API compatibility
Some checks failed
Integration Tests / integration-test (push) Failing after 6s

- Fix AssetUploadStatus serialization to use snake_case (created/duplicate/rejected)
- Fix AssetOrder serialization to use lowercase (asc/desc)
- Add file_created_at/file_modified_at to UploadAssetBuilder
- Fix AddAssetsBuilder response type to Vec<Value> instead of AlbumResponse
- Fix ServerAbout model - version is String not ServerVersion struct
- Update upload_photos.rs to handle duplicate responses correctly
- Update server_info.rs to display new ServerAbout fields
- Update AGENTS.md with new example list
This commit is contained in:
Joakim Hulthe
2026-04-14 20:18:41 +00:00
parent 2e7db3b35a
commit c39e0a5058
6 changed files with 104 additions and 11 deletions

View File

@@ -30,6 +30,11 @@ All examples should be run using the provided wrapper script:
./scripts/run-example.sh upload_photos ./scripts/run-example.sh upload_photos
./scripts/run-example.sh download_asset ./scripts/run-example.sh download_asset
./scripts/run-example.sh thumbnail ./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: The wrapper handles everything automatically:

View File

@@ -38,6 +38,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let about = client.server().about().await?; let about = client.server().about().await?;
println!(" Version: {}", about.version); println!(" Version: {}", about.version);
println!(" Version URL: {}", about.version_url); 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(()) Ok(())
} }

View File

@@ -60,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()
@@ -68,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])

View File

@@ -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)
} }
} }

View File

@@ -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, MetadataSearchRequest, SearchResponse}, 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
@@ -161,7 +164,7 @@ 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 req = self.client.post("/search/metadata"); let req = self.client.post("/search/metadata");
let mut body = MetadataSearchRequest::default(); let mut body = MetadataSearchRequest::default();
if let Some(album_id) = self.album_id { if let Some(album_id) = self.album_id {
@@ -215,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 {
@@ -226,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,
} }
} }
@@ -253,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
@@ -282,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?;

View File

@@ -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,