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 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"])

View File

@ -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)

View File

@ -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.")),
};

View File

@ -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 {

View File

@ -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"])