diff --git a/Cargo.lock b/Cargo.lock index 0b4a9f7..720481e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,6 +1016,7 @@ dependencies = [ "bytes", "chrono", "image", + "mime", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e0f0bb4..17ec539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/apis/assets.rs b/src/apis/assets.rs index 5b4ddc1..6bc3dc9 100644 --- a/src/apis/assets.rs +++ b/src/apis/assets.rs @@ -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 { // 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 { + pub async fn execute(self) -> Result { 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 { + pub async fn execute(self) -> Result { 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 }) } }