Unifi asset download and thumbnail download response type
Some checks failed
Integration Tests / integration-test (push) Failing after 19s

This commit is contained in:
2026-05-24 13:52:38 +02:00
parent bb1d839c44
commit c0bde4f8bd
3 changed files with 40 additions and 22 deletions

1
Cargo.lock generated
View File

@@ -1016,6 +1016,7 @@ dependencies = [
"bytes",
"chrono",
"image",
"mime",
"reqwest",
"serde",
"serde_json",

View File

@@ -24,6 +24,7 @@ image = "0.25"
url = "2.5"
bytes = "1.10"
async-trait = "0.1"
mime = "0.3.17"
[dev-dependencies]
tokio-test = "0.4"

View File

@@ -1,7 +1,10 @@
//! Assets API - Manage photos and videos
use std::io::Cursor;
use std::path::Path;
use std::{io::Cursor, str::FromStr};
use mime::Mime;
use reqwest::header::CONTENT_TYPE;
use crate::{
Client,
@@ -14,17 +17,17 @@ use crate::{
/// Response from downloading a thumbnail containing image data and metadata
#[derive(Debug, Clone)]
pub struct ThumbnailResponse {
/// The image data as bytes
pub struct AssetDownload {
/// The asset data as bytes
pub data: bytes::Bytes,
/// The MIME type of the image (e.g., "image/jpeg", "image/png", "image/webp")
pub content_type: String,
/// The MIME type of the asset (e.g., "image/jpeg", "image/png", "image/webp")
pub mime: Mime,
}
impl ThumbnailResponse {
impl AssetDownload {
/// Get the file extension based on content type
pub fn extension(&self) -> Option<&str> {
match self.content_type.as_str() {
match self.mime.essence_str() {
"image/jpeg" | "image/jpg" => Some("jpg"),
"image/png" => Some("png"),
"image/webp" => Some("webp"),
@@ -35,8 +38,8 @@ impl ThumbnailResponse {
}
/// Get the content type
pub fn content_type(&self) -> &str {
&self.content_type
pub fn content_type(&self) -> &Mime {
&self.mime
}
/// Get the data
@@ -52,7 +55,7 @@ impl ThumbnailResponse {
/// Decode the image data into a DynamicImage
///
/// # Errors
/// Returns an error if the content type is not supported or if decoding fails
/// Returns an error if the asset is not an image, content type is not supported, or if decoding fails
///
/// # Example
/// ```rust,ignore
@@ -69,8 +72,8 @@ impl ThumbnailResponse {
/// ```
pub fn decode(&self) -> Result<image::DynamicImage> {
// Get image format from content type
let format = image::ImageFormat::from_mime_type(&self.content_type).ok_or_else(|| {
ImmichError::Image(format!("Unsupported content type: {}", self.content_type))
let format = image::ImageFormat::from_mime_type(&self.mime).ok_or_else(|| {
ImmichError::Image(format!("Unsupported content type: {}", self.mime))
})?;
// Create reader with the format and decode
@@ -303,7 +306,10 @@ impl UploadAssetBuilder {
// 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(
"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);
@@ -386,7 +392,7 @@ impl DownloadAssetBuilder {
}
/// Execute the download
pub async fn execute(self) -> Result<bytes::Bytes> {
pub async fn execute(self) -> Result<AssetDownload> {
let path = format!("/assets/{}/original", self.id);
let mut req = self.client.get(&path);
@@ -395,8 +401,18 @@ impl DownloadAssetBuilder {
}
let response = self.client.execute(req.build()?).await?;
let bytes = response.bytes().await?;
Ok(bytes)
// Get content type from response headers
let mime = response
.headers()
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| Mime::from_str(ct).ok())
.ok_or(ImmichError::Image("Missing or invalid content-type".into()))?;
let data = response.bytes().await?;
Ok(AssetDownload { data, mime })
}
}
@@ -433,7 +449,7 @@ impl ThumbnailBuilder {
}
/// Execute the request
pub async fn execute(self) -> Result<ThumbnailResponse> {
pub async fn execute(self) -> Result<AssetDownload> {
let path = format!("/assets/{}/thumbnail", self.id);
let mut req = self.client.get(&path);
@@ -454,15 +470,15 @@ impl ThumbnailBuilder {
let response = self.client.execute(req.build()?).await?;
// Get content type from response headers
let content_type = response
let mime = response
.headers()
.get("content-type")
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.map(String::from)
.unwrap_or_else(|| "application/octet-stream".to_string());
.and_then(|ct| Mime::from_str(ct).ok())
.ok_or(ImmichError::Image("Missing or invalid content-type".into()))?;
let data = response.bytes().await?;
Ok(ThumbnailResponse { data, content_type })
Ok(AssetDownload { data, mime })
}
}