diff --git a/crates/pinakes-core/src/media_type/builtin.rs b/crates/pinakes-core/src/media_type/builtin.rs index 67f1d68..bd84aca 100644 --- a/crates/pinakes-core/src/media_type/builtin.rs +++ b/crates/pinakes-core/src/media_type/builtin.rs @@ -61,9 +61,40 @@ pub enum MediaCategory { } impl BuiltinMediaType { - /// Get the unique ID for this media type - pub fn id(&self) -> String { - format!("{:?}", self).to_lowercase() + /// Get the unique, stable ID for this media type. + pub fn id(&self) -> &'static str { + match self { + Self::Mp3 => "mp3", + Self::Flac => "flac", + Self::Ogg => "ogg", + Self::Wav => "wav", + Self::Aac => "aac", + Self::Opus => "opus", + Self::Mp4 => "mp4", + Self::Mkv => "mkv", + Self::Avi => "avi", + Self::Webm => "webm", + Self::Pdf => "pdf", + Self::Epub => "epub", + Self::Djvu => "djvu", + Self::Markdown => "markdown", + Self::PlainText => "plaintext", + Self::Jpeg => "jpeg", + Self::Png => "png", + Self::Gif => "gif", + Self::Webp => "webp", + Self::Svg => "svg", + Self::Avif => "avif", + Self::Tiff => "tiff", + Self::Bmp => "bmp", + Self::Cr2 => "cr2", + Self::Nef => "nef", + Self::Arw => "arw", + Self::Dng => "dng", + Self::Orf => "orf", + Self::Rw2 => "rw2", + Self::Heic => "heic", + } } /// Get the display name for this media type diff --git a/crates/pinakes-core/src/media_type/mod.rs b/crates/pinakes-core/src/media_type/mod.rs index 7c5ed9f..9d221c5 100644 --- a/crates/pinakes-core/src/media_type/mod.rs +++ b/crates/pinakes-core/src/media_type/mod.rs @@ -1,6 +1,6 @@ -//! Extensible media type system +//! Media types //! -//! This module provides an extensible media type system that supports both +//! Supports both //! built-in media types and plugin-registered custom types. use std::path::Path; @@ -13,7 +13,7 @@ pub mod registry; pub use builtin::{BuiltinMediaType, MediaCategory}; pub use registry::{MediaTypeDescriptor, MediaTypeRegistry}; -/// Media type identifier - can be either built-in or custom +/// Media type identifier, can be either built-in or custom #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(untagged)] pub enum MediaType { @@ -33,7 +33,7 @@ impl MediaType { /// Get the type ID as a string pub fn id(&self) -> String { match self { - Self::Builtin(b) => b.id(), + Self::Builtin(b) => b.id().to_string(), Self::Custom(id) => id.clone(), } } @@ -61,8 +61,8 @@ impl MediaType { } /// Get the category for this media type - /// For custom types without a registry, returns MediaCategory::Document as - /// default + /// For custom types without a registry, returns [`MediaCategory::Document`] + /// as default pub fn category(&self) -> MediaCategory { match self { Self::Builtin(b) => b.category(), @@ -192,7 +192,7 @@ impl MediaType { } } -// Implement From for easier conversion +// Implement `From` for easier conversion impl From for MediaType { fn from(builtin: BuiltinMediaType) -> Self { Self::Builtin(builtin) diff --git a/crates/pinakes-core/src/thumbnail.rs b/crates/pinakes-core/src/thumbnail.rs index 60d4e37..7dd7852 100644 --- a/crates/pinakes-core/src/thumbnail.rs +++ b/crates/pinakes-core/src/thumbnail.rs @@ -12,6 +12,25 @@ use crate::{ model::MediaId, }; +/// RAII guard that removes a file on drop. +struct TempFileGuard(PathBuf); + +impl TempFileGuard { + fn new(path: PathBuf) -> Self { + Self(path) + } + + fn path(&self) -> &Path { + &self.0 + } +} + +impl Drop for TempFileGuard { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + /// Generate a thumbnail for a media file and return the path to the thumbnail. /// /// Supports images (via `image` crate), videos (via ffmpeg), PDFs (via @@ -145,7 +164,6 @@ fn generate_pdf_thumbnail( dest: &Path, config: &ThumbnailConfig, ) -> Result<()> { - // Use pdftoppm to render first page, then resize with image crate let temp_prefix = dest.with_extension("tmp"); let status = Command::new("pdftoppm") .args(["-jpeg", "-f", "1", "-l", "1", "-singlefile"]) @@ -167,11 +185,10 @@ fn generate_pdf_thumbnail( ))); } - // pdftoppm outputs .jpg - let rendered = temp_prefix.with_extension("jpg"); - if rendered.exists() { - // Resize to thumbnail size - let img = image::open(&rendered).map_err(|e| { + // pdftoppm outputs .jpg -- guard ensures cleanup on all paths + let rendered = TempFileGuard::new(temp_prefix.with_extension("jpg")); + if rendered.path().exists() { + let img = image::open(rendered.path()).map_err(|e| { PinakesError::MetadataExtraction(format!("pdf thumbnail open: {e}")) })?; let thumb = img.thumbnail(config.size, config.size); @@ -183,7 +200,7 @@ fn generate_pdf_thumbnail( thumb.write_with_encoder(encoder).map_err(|e| { PinakesError::MetadataExtraction(format!("pdf thumbnail encode: {e}")) })?; - let _ = std::fs::remove_file(&rendered); + // Guard drops here and cleans up the temp file Ok(()) } else { Err(PinakesError::MetadataExtraction( @@ -235,12 +252,11 @@ fn generate_raw_thumbnail( dest: &Path, config: &ThumbnailConfig, ) -> Result<()> { - // Try dcraw to extract embedded JPEG preview, then resize - let temp_ppm = dest.with_extension("ppm"); + let temp_ppm = TempFileGuard::new(dest.with_extension("ppm")); let status = Command::new("dcraw") .args(["-e", "-c"]) .arg(source) - .stdout(std::fs::File::create(&temp_ppm).map_err(|e| { + .stdout(std::fs::File::create(temp_ppm.path()).map_err(|e| { PinakesError::MetadataExtraction(format!( "failed to create temp file: {e}" )) @@ -254,18 +270,15 @@ fn generate_raw_thumbnail( })?; if !status.success() { - let _ = std::fs::remove_file(&temp_ppm); + // Guard drops and cleans up temp_ppm return Err(PinakesError::MetadataExtraction(format!( "dcraw exited with status {}", status ))); } - // The extracted preview is typically a JPEG; try loading it - if temp_ppm.exists() { - let result = image::open(&temp_ppm); - let _ = std::fs::remove_file(&temp_ppm); - let img = result.map_err(|e| { + if temp_ppm.path().exists() { + let img = image::open(temp_ppm.path()).map_err(|e| { PinakesError::MetadataExtraction(format!("raw preview decode: {e}")) })?; let thumb = img.thumbnail(config.size, config.size); @@ -277,6 +290,7 @@ fn generate_raw_thumbnail( thumb.write_with_encoder(encoder).map_err(|e| { PinakesError::MetadataExtraction(format!("raw thumbnail encode: {e}")) })?; + // Guard drops and cleans up temp_ppm Ok(()) } else { Err(PinakesError::MetadataExtraction( @@ -290,11 +304,10 @@ fn generate_heic_thumbnail( dest: &Path, config: &ThumbnailConfig, ) -> Result<()> { - // Use heif-convert to convert to JPEG, then resize - let temp_jpg = dest.with_extension("tmp.jpg"); + let temp_jpg = TempFileGuard::new(dest.with_extension("tmp.jpg")); let status = Command::new("heif-convert") .arg(source) - .arg(&temp_jpg) + .arg(temp_jpg.path()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() @@ -305,17 +318,15 @@ fn generate_heic_thumbnail( })?; if !status.success() { - let _ = std::fs::remove_file(&temp_jpg); + // Guard drops and cleans up temp_jpg return Err(PinakesError::MetadataExtraction(format!( "heif-convert exited with status {}", status ))); } - if temp_jpg.exists() { - let result = image::open(&temp_jpg); - let _ = std::fs::remove_file(&temp_jpg); - let img = result.map_err(|e| { + if temp_jpg.path().exists() { + let img = image::open(temp_jpg.path()).map_err(|e| { PinakesError::MetadataExtraction(format!("heic decode: {e}")) })?; let thumb = img.thumbnail(config.size, config.size); @@ -327,6 +338,7 @@ fn generate_heic_thumbnail( thumb.write_with_encoder(encoder).map_err(|e| { PinakesError::MetadataExtraction(format!("heic thumbnail encode: {e}")) })?; + // Guard drops and cleans up temp_jpg Ok(()) } else { Err(PinakesError::MetadataExtraction( @@ -449,14 +461,11 @@ pub fn extract_epub_cover(epub_path: &Path) -> Result>> { /// Extract full-size cover from a PDF file (first page) pub fn extract_pdf_cover(pdf_path: &Path) -> Result>> { - // Use pdftoppm to extract the first page at high resolution - let pdftoppm = "pdftoppm"; - let temp_dir = std::env::temp_dir(); let temp_prefix = temp_dir.join(format!("pdf_cover_{}", uuid::Uuid::new_v4())); - let status = Command::new(pdftoppm) + let status = Command::new("pdftoppm") .args(["-jpeg", "-f", "1", "-l", "1", "-scale-to", "1200"]) .arg(pdf_path) .arg(&temp_prefix) @@ -472,13 +481,15 @@ pub fn extract_pdf_cover(pdf_path: &Path) -> Result>> { ))); } - // pdftoppm outputs files like prefix-1.jpg - let output_path = format!("{}-1.jpg", temp_prefix.display()); - let output_pathbuf = PathBuf::from(&output_path); + // pdftoppm outputs files like prefix-1.jpg -- guard ensures cleanup + let output = TempFileGuard::new(PathBuf::from(format!( + "{}-1.jpg", + temp_prefix.display() + ))); - if output_pathbuf.exists() { - let data = std::fs::read(&output_pathbuf)?; - let _ = std::fs::remove_file(&output_pathbuf); + if output.path().exists() { + let data = std::fs::read(output.path())?; + // Guard drops and cleans up the temp file Ok(Some(data)) } else { Ok(None)