feat: add options to previews
video: position image: size, quality audio: size, color TODO: make options an actual struct to be Deserialized?
This commit is contained in:
parent
b04a00c660
commit
8e3ea0f574
5 changed files with 62 additions and 19 deletions
|
@ -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<Option<Vec<u8>>> {
|
||||
fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
|
||||
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"])
|
||||
|
|
|
@ -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<Option<Vec<u8>>> {
|
||||
fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
|
||||
#[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)
|
||||
|
|
|
@ -24,7 +24,7 @@ pub mod text;
|
|||
pub mod video;
|
||||
|
||||
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);
|
||||
|
@ -47,10 +47,12 @@ impl PreviewStore {
|
|||
|
||||
fn get_path(&self, hash: &Hash, options: &HashMap<String, String>) -> Arc<Mutex<PathBuf>> {
|
||||
let mut locks = self.locks.lock().unwrap();
|
||||
let options_concat = options
|
||||
let mut options_strs = options
|
||||
.iter()
|
||||
.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())) {
|
||||
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.")),
|
||||
};
|
||||
|
|
|
@ -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<Option<Vec<u8>>> {
|
||||
fn get_thumbnail(&self, _options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
|
||||
let mut file = File::open(self.0)?;
|
||||
let size: usize = file.metadata()?.len().try_into()?;
|
||||
if size > PREVIEW_SIZE {
|
||||
|
|
|
@ -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<Option<Vec<u8>>> {
|
||||
fn get_thumbnail(&self, options: HashMap<String, String>) -> Result<Option<Vec<u8>>> {
|
||||
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::<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 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"])
|
||||
|
|
Loading…
Reference in a new issue