diff --git a/src/previews/audio.rs b/src/previews/audio.rs index 686b769..d3dd3aa 100644 --- a/src/previews/audio.rs +++ b/src/previews/audio.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use std::collections::HashMap; use std::io::Read; use std::path::Path; use std::process::Command; @@ -12,16 +13,26 @@ pub struct AudioPath<'a>(pub &'a Path); const COLOR: &str = "#dc322f"; // solarized red impl<'a> Previewable for AudioPath<'a> { - fn get_thumbnail(&self) -> Result>> { + fn get_thumbnail(&self, options: HashMap) -> Result>> { let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?; + + let color = options + .get("color") + .map(String::to_owned) + .unwrap_or_else(|| COLOR.into()); + let size = options + .get("size") + .map(String::to_owned) + .unwrap_or_else(|| "860x256".into()); + let thumbnail_cmd = Command::new("ffmpeg") .args(["-i", &self.0.to_string_lossy()]) .args([ "-filter_complex", &format!( "[0:a]aformat=channel_layouts=mono, compand=gain=-2, - showwavespic=s=860x256:colors={COLOR}, - drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={COLOR}" + showwavespic=s={size}:colors={color}, + drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}" ), ]) .args(["-vframes", "1"]) diff --git a/src/previews/image.rs b/src/previews/image.rs index 389fbf2..922d9e2 100644 --- a/src/previews/image.rs +++ b/src/previews/image.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; #[cfg(feature = "previews-image")] use image::{io::Reader as ImageReader, GenericImageView}; -use std::{cmp, path::Path}; +use std::{cmp, collections::HashMap, path::Path}; use anyhow::Result; @@ -11,7 +11,7 @@ use super::Previewable; pub struct ImagePath<'a>(pub &'a Path); impl<'a> Previewable for ImagePath<'a> { - fn get_thumbnail(&self) -> Result>> { + fn get_thumbnail(&self, options: HashMap) -> Result>> { #[cfg(feature = "previews-image")] { let file = std::fs::File::open(&self.0)?; @@ -36,13 +36,29 @@ impl<'a> Previewable for ImagePath<'a> { Some(8) => image.rotate270(), _ => image, }; + let (w, h) = image.dimensions(); - if cmp::max(w, h) > 1024 { - let thumbnail = image.thumbnail(1024, 1024); + let max_dimension = { + if let Some(str_size) = options.get("size") { + str_size.parse()? + } else { + 1024 + } + }; + let quality = { + if let Some(str_quality) = options.get("quality") { + str_quality.parse()? + } else { + 90.0 + } + }; + + if cmp::max(w, h) > max_dimension { + let thumbnail = image.thumbnail(max_dimension, max_dimension); let thumbnail = thumbnail.into_rgba8(); let (w, h) = thumbnail.dimensions(); let encoder = webp::Encoder::from_rgba(&thumbnail, w, h); - let result = encoder.encode(90.0); + let result = encoder.encode(quality); Ok(Some(result.to_vec())) } else { Ok(None) diff --git a/src/previews/mod.rs b/src/previews/mod.rs index ff4d229..bd86096 100644 --- a/src/previews/mod.rs +++ b/src/previews/mod.rs @@ -24,7 +24,7 @@ pub mod text; pub mod video; pub trait Previewable { - fn get_thumbnail(&self) -> Result>>; + fn get_thumbnail(&self, options: HashMap) -> Result>>; } type HashWithOptions = (Hash, String); @@ -47,10 +47,12 @@ impl PreviewStore { fn get_path(&self, hash: &Hash, options: &HashMap) -> Arc> { let mut locks = self.locks.lock().unwrap(); - let options_concat = options + let mut options_strs = options .iter() .map(|(k, v)| format!("{k}{v}")) - .collect::(); + .collect::>(); + options_strs.sort(); + let options_concat = options_strs.concat(); if let Some(path) = locks.get(&(hash.clone(), options_concat.clone())) { path.clone() } else { @@ -101,14 +103,18 @@ impl PreviewStore { }; let preview = match mime_type { - Some(tm) if tm.starts_with("text") => TextPath(file_path).get_thumbnail(), + Some(tm) if tm.starts_with("text") => { + TextPath(file_path).get_thumbnail(options) + } Some(tm) if tm.starts_with("video") || tm == "application/x-matroska" => { - VideoPath(file_path).get_thumbnail() + VideoPath(file_path).get_thumbnail(options) } Some(tm) if tm.starts_with("audio") || tm == "application/x-riff" => { - AudioPath(file_path).get_thumbnail() + AudioPath(file_path).get_thumbnail(options) + } + Some(tm) if tm.starts_with("image") => { + ImagePath(file_path).get_thumbnail(options) } - Some(tm) if tm.starts_with("image") => ImagePath(file_path).get_thumbnail(), Some(unknown) => Err(anyhow!("No capability for {:?} thumbnails.", unknown)), _ => Err(anyhow!("Unknown file type, or file doesn't exist.")), }; diff --git a/src/previews/text.rs b/src/previews/text.rs index ed54df4..03f45ca 100644 --- a/src/previews/text.rs +++ b/src/previews/text.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::{convert::TryInto, fs::File, io::Read, path::Path}; +use std::{collections::HashMap, convert::TryInto, fs::File, io::Read, path::Path}; use super::Previewable; @@ -8,7 +8,7 @@ pub struct TextPath<'a>(pub &'a Path); const PREVIEW_SIZE: usize = 1024; impl<'a> Previewable for TextPath<'a> { - fn get_thumbnail(&self) -> Result>> { + fn get_thumbnail(&self, _options: HashMap) -> Result>> { let mut file = File::open(self.0)?; let size: usize = file.metadata()?.len().try_into()?; if size > PREVIEW_SIZE { diff --git a/src/previews/video.rs b/src/previews/video.rs index 2b8d989..f82cfa6 100644 --- a/src/previews/video.rs +++ b/src/previews/video.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use std::collections::HashMap; use std::io::Read; use std::path::Path; use std::process::Command; @@ -10,7 +11,7 @@ use super::Previewable; pub struct VideoPath<'a>(pub &'a Path); impl<'a> Previewable for VideoPath<'a> { - fn get_thumbnail(&self) -> Result>> { + fn get_thumbnail(&self, options: HashMap) -> Result>> { let duration_cmd = Command::new("ffprobe") .args(["-threads", "1"]) .args(["-v", "error"]) @@ -24,15 +25,24 @@ impl<'a> Previewable for VideoPath<'a> { String::from_utf8_lossy(&duration_cmd.stderr) )); } + let duration = String::from_utf8_lossy(&duration_cmd.stdout) .trim() .parse::()?; + let position = { + if let Some(str_position) = options.get("position") { + str_position.parse()? + } else { + 90.0f64 + } + }; + let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?; let thumbnail_cmd = Command::new("ffmpeg") .args(["-threads", "1"]) .args(["-discard", "nokey"]) .args(["-noaccurate_seek"]) - .args(["-ss", &(90f64.min(duration / 2.0)).to_string()]) + .args(["-ss", &(position.min(duration / 2.0)).to_string()]) .args(["-i", &self.0.to_string_lossy()]) .args(["-vframes", "1"]) .args(["-vsync", "passthrough"])