pinakes-core: update remaining modules and tests

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9e0ff5ea33a5cf697473423e88f167ce6a6a6964
This commit is contained in:
raf 2026-03-08 00:42:29 +03:00
commit 3d9f8933d2
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
44 changed files with 1207 additions and 578 deletions

View file

@ -16,7 +16,7 @@ use crate::{
struct TempFileGuard(PathBuf);
impl TempFileGuard {
fn new(path: PathBuf) -> Self {
const fn new(path: PathBuf) -> Self {
Self(path)
}
@ -35,10 +35,14 @@ impl Drop for TempFileGuard {
///
/// Supports images (via `image` crate), videos (via ffmpeg), PDFs (via
/// pdftoppm), and EPUBs (via cover image extraction).
///
/// # Errors
///
/// Returns [`PinakesError`] if thumbnail generation fails.
pub fn generate_thumbnail(
media_id: MediaId,
source_path: &Path,
media_type: MediaType,
media_type: &MediaType,
thumbnail_dir: &Path,
) -> Result<Option<PathBuf>> {
generate_thumbnail_with_config(
@ -50,21 +54,26 @@ pub fn generate_thumbnail(
)
}
/// Generate a thumbnail with custom configuration.
///
/// # Errors
///
/// Returns [`PinakesError`] if thumbnail generation fails.
pub fn generate_thumbnail_with_config(
media_id: MediaId,
source_path: &Path,
media_type: MediaType,
media_type: &MediaType,
thumbnail_dir: &Path,
config: &ThumbnailConfig,
) -> Result<Option<PathBuf>> {
std::fs::create_dir_all(thumbnail_dir)?;
let thumb_path = thumbnail_dir.join(format!("{}.jpg", media_id));
let thumb_path = thumbnail_dir.join(format!("{media_id}.jpg"));
let result = match media_type.category() {
MediaCategory::Image => {
if media_type.is_raw() {
generate_raw_thumbnail(source_path, &thumb_path, config)
} else if media_type == MediaType::Builtin(BuiltinMediaType::Heic) {
} else if *media_type == MediaType::Builtin(BuiltinMediaType::Heic) {
generate_heic_thumbnail(source_path, &thumb_path, config)
} else {
generate_image_thumbnail(source_path, &thumb_path, config)
@ -151,8 +160,7 @@ fn generate_video_thumbnail(
if !status.success() {
return Err(PinakesError::MetadataExtraction(format!(
"ffmpeg exited with status {}",
status
"ffmpeg exited with status {status}"
)));
}
@ -180,8 +188,7 @@ fn generate_pdf_thumbnail(
if !status.success() {
return Err(PinakesError::MetadataExtraction(format!(
"pdftoppm exited with status {}",
status
"pdftoppm exited with status {status}"
)));
}
@ -272,8 +279,7 @@ fn generate_raw_thumbnail(
if !status.success() {
// Guard drops and cleans up temp_ppm
return Err(PinakesError::MetadataExtraction(format!(
"dcraw exited with status {}",
status
"dcraw exited with status {status}"
)));
}
@ -320,8 +326,7 @@ fn generate_heic_thumbnail(
if !status.success() {
// Guard drops and cleans up temp_jpg
return Err(PinakesError::MetadataExtraction(format!(
"heif-convert exited with status {}",
status
"heif-convert exited with status {status}"
)));
}
@ -357,26 +362,32 @@ pub enum CoverSize {
}
impl CoverSize {
pub fn dimensions(&self) -> Option<(u32, u32)> {
#[must_use]
pub const fn dimensions(&self) -> Option<(u32, u32)> {
match self {
CoverSize::Tiny => Some((64, 64)),
CoverSize::Grid => Some((320, 320)),
CoverSize::Preview => Some((1024, 1024)),
CoverSize::Original => None, // No resizing
Self::Tiny => Some((64, 64)),
Self::Grid => Some((320, 320)),
Self::Preview => Some((1024, 1024)),
Self::Original => None, // No resizing
}
}
pub fn filename(&self) -> &'static str {
#[must_use]
pub const fn filename(&self) -> &'static str {
match self {
CoverSize::Tiny => "tiny.jpg",
CoverSize::Grid => "grid.jpg",
CoverSize::Preview => "preview.jpg",
CoverSize::Original => "original.jpg",
Self::Tiny => "tiny.jpg",
Self::Grid => "grid.jpg",
Self::Preview => "preview.jpg",
Self::Original => "original.jpg",
}
}
}
/// Generate multi-resolution covers for a book
/// Generate multi-resolution covers for a book.
///
/// # Errors
///
/// Returns [`PinakesError`] if the cover image cannot be decoded or encoded.
pub fn generate_book_covers(
media_id: MediaId,
source_image: &[u8],
@ -401,26 +412,23 @@ pub fn generate_book_covers(
] {
let cover_path = media_cover_dir.join(size.filename());
match size.dimensions() {
Some((width, height)) => {
// Generate thumbnail
let thumb = img.thumbnail(width, height);
let mut output = std::fs::File::create(&cover_path)?;
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 90);
thumb.write_with_encoder(encoder).map_err(|e| {
PinakesError::MetadataExtraction(format!("cover encode: {e}"))
})?;
},
None => {
// Save original
let mut output = std::fs::File::create(&cover_path)?;
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 95);
img.write_with_encoder(encoder).map_err(|e| {
PinakesError::MetadataExtraction(format!("cover encode: {e}"))
})?;
},
if let Some((width, height)) = size.dimensions() {
// Generate thumbnail
let thumb = img.thumbnail(width, height);
let mut output = std::fs::File::create(&cover_path)?;
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 90);
thumb.write_with_encoder(encoder).map_err(|e| {
PinakesError::MetadataExtraction(format!("cover encode: {e}"))
})?;
} else {
// Save original
let mut output = std::fs::File::create(&cover_path)?;
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 95);
img.write_with_encoder(encoder).map_err(|e| {
PinakesError::MetadataExtraction(format!("cover encode: {e}"))
})?;
}
results.push((size, cover_path));
@ -429,7 +437,11 @@ pub fn generate_book_covers(
Ok(results)
}
/// Extract full-size cover from an EPUB file
/// Extract full-size cover from an EPUB file.
///
/// # Errors
///
/// Returns [`PinakesError`] if the EPUB cannot be opened or read.
pub fn extract_epub_cover(epub_path: &Path) -> Result<Option<Vec<u8>>> {
let mut doc = epub::doc::EpubDoc::new(epub_path)
.map_err(|e| PinakesError::MetadataExtraction(format!("EPUB open: {e}")))?;
@ -459,7 +471,11 @@ pub fn extract_epub_cover(epub_path: &Path) -> Result<Option<Vec<u8>>> {
Ok(None)
}
/// Extract full-size cover from a PDF file (first page)
/// Extract full-size cover from a PDF file (first page).
///
/// # Errors
///
/// Returns [`PinakesError`] if pdftoppm cannot be executed or fails.
pub fn extract_pdf_cover(pdf_path: &Path) -> Result<Option<Vec<u8>>> {
let temp_dir = std::env::temp_dir();
let temp_prefix =
@ -476,8 +492,7 @@ pub fn extract_pdf_cover(pdf_path: &Path) -> Result<Option<Vec<u8>>> {
if !status.success() {
return Err(PinakesError::MetadataExtraction(format!(
"pdftoppm exited with status {}",
status
"pdftoppm exited with status {status}"
)));
}
@ -497,11 +512,13 @@ pub fn extract_pdf_cover(pdf_path: &Path) -> Result<Option<Vec<u8>>> {
}
/// Returns the default covers directory under the data dir
#[must_use]
pub fn default_covers_dir() -> PathBuf {
crate::config::Config::default_data_dir().join("covers")
}
/// Returns the default thumbnail directory under the data dir.
#[must_use]
pub fn default_thumbnail_dir() -> PathBuf {
crate::config::Config::default_data_dir().join("thumbnails")
}
@ -519,30 +536,37 @@ pub enum ThumbnailSize {
impl ThumbnailSize {
/// Get the pixel size for this thumbnail variant
pub fn pixels(&self) -> u32 {
#[must_use]
pub const fn pixels(&self) -> u32 {
match self {
ThumbnailSize::Tiny => 64,
ThumbnailSize::Grid => 320,
ThumbnailSize::Preview => 1024,
Self::Tiny => 64,
Self::Grid => 320,
Self::Preview => 1024,
}
}
/// Get the subdirectory name for this size
pub fn subdir_name(&self) -> &'static str {
#[must_use]
pub const fn subdir_name(&self) -> &'static str {
match self {
ThumbnailSize::Tiny => "tiny",
ThumbnailSize::Grid => "grid",
ThumbnailSize::Preview => "preview",
Self::Tiny => "tiny",
Self::Grid => "grid",
Self::Preview => "preview",
}
}
}
/// Generate all thumbnail sizes for a media file
/// Returns paths to the generated thumbnails (tiny, grid, preview)
/// Generate all thumbnail sizes for a media file.
///
/// Returns paths to the generated thumbnails (tiny, grid, preview).
///
/// # Errors
///
/// Returns [`PinakesError`] if thumbnail generation fails.
pub fn generate_all_thumbnail_sizes(
media_id: MediaId,
source_path: &Path,
media_type: MediaType,
media_type: &MediaType,
thumbnail_base_dir: &Path,
) -> Result<(Option<PathBuf>, Option<PathBuf>, Option<PathBuf>)> {
let sizes = [
@ -564,7 +588,7 @@ pub fn generate_all_thumbnail_sizes(
let result = generate_thumbnail_with_config(
media_id,
source_path,
media_type.clone(),
media_type,
&size_dir,
&config,
)?;