pinakes-core: update remaining modules and tests
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9e0ff5ea33a5cf697473423e88f167ce6a6a6964
This commit is contained in:
parent
c8425a4c34
commit
3d9f8933d2
44 changed files with 1207 additions and 578 deletions
|
|
@ -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,
|
||||
)?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue