feat: add options to previews

video: position
image: size, quality
audio: size, color

TODO: make options an actual struct to be Deserialized?
feat/type-attributes
Tomáš Mládek 2022-09-18 13:10:01 +02:00
parent b04a00c660
commit 8e3ea0f574
5 changed files with 62 additions and 19 deletions

View File

@ -1,4 +1,5 @@
use anyhow::anyhow; use anyhow::anyhow;
use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@ -12,16 +13,26 @@ pub struct AudioPath<'a>(pub &'a Path);
const COLOR: &str = "#dc322f"; // solarized red const COLOR: &str = "#dc322f"; // solarized red
impl<'a> Previewable for AudioPath<'a> { impl<'a> Previewable for AudioPath<'a> {
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>> { fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?; 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") let thumbnail_cmd = Command::new("ffmpeg")
.args(["-i", &self.0.to_string_lossy()]) .args(["-i", &self.0.to_string_lossy()])
.args([ .args([
"-filter_complex", "-filter_complex",
&format!( &format!(
"[0:a]aformat=channel_layouts=mono, compand=gain=-2, "[0:a]aformat=channel_layouts=mono, compand=gain=-2,
showwavespic=s=860x256:colors={COLOR}, showwavespic=s={size}:colors={color},
drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={COLOR}" drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}"
), ),
]) ])
.args(["-vframes", "1"]) .args(["-vframes", "1"])

View File

@ -2,7 +2,7 @@
use anyhow::anyhow; use anyhow::anyhow;
#[cfg(feature = "previews-image")] #[cfg(feature = "previews-image")]
use image::{io::Reader as ImageReader, GenericImageView}; use image::{io::Reader as ImageReader, GenericImageView};
use std::{cmp, path::Path}; use std::{cmp, collections::HashMap, path::Path};
use anyhow::Result; use anyhow::Result;
@ -11,7 +11,7 @@ use super::Previewable;
pub struct ImagePath<'a>(pub &'a Path); pub struct ImagePath<'a>(pub &'a Path);
impl<'a> Previewable for ImagePath<'a> { impl<'a> Previewable for ImagePath<'a> {
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>> { fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
#[cfg(feature = "previews-image")] #[cfg(feature = "previews-image")]
{ {
let file = std::fs::File::open(&self.0)?; let file = std::fs::File::open(&self.0)?;
@ -36,13 +36,29 @@ impl<'a> Previewable for ImagePath<'a> {
Some(8) => image.rotate270(), Some(8) => image.rotate270(),
_ => image, _ => image,
}; };
let (w, h) = image.dimensions(); let (w, h) = image.dimensions();
if cmp::max(w, h) > 1024 { let max_dimension = {
let thumbnail = image.thumbnail(1024, 1024); 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 thumbnail = thumbnail.into_rgba8();
let (w, h) = thumbnail.dimensions(); let (w, h) = thumbnail.dimensions();
let encoder = webp::Encoder::from_rgba(&thumbnail, w, h); 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())) Ok(Some(result.to_vec()))
} else { } else {
Ok(None) Ok(None)

View File

@ -24,7 +24,7 @@ pub mod text;
pub mod video; pub mod video;
pub trait Previewable { pub trait Previewable {
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>>; fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>>;
} }
type HashWithOptions = (Hash, String); type HashWithOptions = (Hash, String);
@ -47,10 +47,12 @@ impl PreviewStore {
fn get_path(&self, hash: &Hash, options: &HashMap<String, String>) -> Arc<Mutex<PathBuf>> { fn get_path(&self, hash: &Hash, options: &HashMap<String, String>) -> Arc<Mutex<PathBuf>> {
let mut locks = self.locks.lock().unwrap(); let mut locks = self.locks.lock().unwrap();
let options_concat = options let mut options_strs = options
.iter() .iter()
.map(|(k, v)| format!("{k}{v}")) .map(|(k, v)| format!("{k}{v}"))
.collect::<String>(); .collect::<Vec<String>>();
options_strs.sort();
let options_concat = options_strs.concat();
if let Some(path) = locks.get(&(hash.clone(), options_concat.clone())) { if let Some(path) = locks.get(&(hash.clone(), options_concat.clone())) {
path.clone() path.clone()
} else { } else {
@ -101,14 +103,18 @@ impl PreviewStore {
}; };
let preview = match mime_type { 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" => { 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" => { 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)), Some(unknown) => Err(anyhow!("No capability for {:?} thumbnails.", unknown)),
_ => Err(anyhow!("Unknown file type, or file doesn't exist.")), _ => Err(anyhow!("Unknown file type, or file doesn't exist.")),
}; };

View File

@ -1,5 +1,5 @@
use anyhow::Result; 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; use super::Previewable;
@ -8,7 +8,7 @@ pub struct TextPath<'a>(pub &'a Path);
const PREVIEW_SIZE: usize = 1024; const PREVIEW_SIZE: usize = 1024;
impl<'a> Previewable for TextPath<'a> { impl<'a> Previewable for TextPath<'a> {
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>> { fn get_thumbnail(&self, _options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
let mut file = File::open(self.0)?; let mut file = File::open(self.0)?;
let size: usize = file.metadata()?.len().try_into()?; let size: usize = file.metadata()?.len().try_into()?;
if size > PREVIEW_SIZE { if size > PREVIEW_SIZE {

View File

@ -1,4 +1,5 @@
use anyhow::anyhow; use anyhow::anyhow;
use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@ -10,7 +11,7 @@ use super::Previewable;
pub struct VideoPath<'a>(pub &'a Path); pub struct VideoPath<'a>(pub &'a Path);
impl<'a> Previewable for VideoPath<'a> { impl<'a> Previewable for VideoPath<'a> {
fn get_thumbnail(&self) -> Result<Option<Vec<u8>>> { fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
let duration_cmd = Command::new("ffprobe") let duration_cmd = Command::new("ffprobe")
.args(["-threads", "1"]) .args(["-threads", "1"])
.args(["-v", "error"]) .args(["-v", "error"])
@ -24,15 +25,24 @@ impl<'a> Previewable for VideoPath<'a> {
String::from_utf8_lossy(&duration_cmd.stderr) String::from_utf8_lossy(&duration_cmd.stderr)
)); ));
} }
let duration = String::from_utf8_lossy(&duration_cmd.stdout) let duration = String::from_utf8_lossy(&duration_cmd.stdout)
.trim() .trim()
.parse::<f64>()?; .parse::<f64>()?;
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 outfile = tempfile::Builder::new().suffix(".webp").tempfile()?;
let thumbnail_cmd = Command::new("ffmpeg") let thumbnail_cmd = Command::new("ffmpeg")
.args(["-threads", "1"]) .args(["-threads", "1"])
.args(["-discard", "nokey"]) .args(["-discard", "nokey"])
.args(["-noaccurate_seek"]) .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(["-i", &self.0.to_string_lossy()])
.args(["-vframes", "1"]) .args(["-vframes", "1"])
.args(["-vsync", "passthrough"]) .args(["-vsync", "passthrough"])