Improve media indexing and processing flow
This commit is contained in:
+151
-62
@@ -1,10 +1,150 @@
|
||||
use crate::media::MediaTools;
|
||||
use anyhow::{anyhow, Result};
|
||||
use fast_image_resize as fir;
|
||||
use fast_image_resize::IntoImageView;
|
||||
use image::ImageDecoder;
|
||||
use image::ImageFormat;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub const THUMB_SIZE: u32 = 320;
|
||||
|
||||
pub struct GeneratedThumbnail {
|
||||
pub path: PathBuf,
|
||||
pub width: Option<i64>,
|
||||
pub height: Option<i64>,
|
||||
}
|
||||
|
||||
pub fn generate_image_thumbnail(image_path: &Path, cache_dir: &Path) -> Result<GeneratedThumbnail> {
|
||||
let path_str = image_path.to_string_lossy();
|
||||
let out_path = thumb_path(cache_dir, &path_str);
|
||||
|
||||
let original_dimensions = image::image_dimensions(image_path)
|
||||
.ok()
|
||||
.map(|(width, height)| (Some(width as i64), Some(height as i64)))
|
||||
.unwrap_or((None, None));
|
||||
|
||||
if out_path.exists() {
|
||||
return Ok(GeneratedThumbnail {
|
||||
path: out_path,
|
||||
width: original_dimensions.0,
|
||||
height: original_dimensions.1,
|
||||
});
|
||||
}
|
||||
|
||||
let reader = image::ImageReader::open(image_path)?.with_guessed_format()?;
|
||||
let mut decoder = reader.into_decoder()?;
|
||||
let orientation = decoder.orientation()?;
|
||||
let mut img = image::DynamicImage::from_decoder(decoder)?;
|
||||
img.apply_orientation(orientation);
|
||||
|
||||
let src = image::DynamicImage::ImageRgba8(img.into_rgba8());
|
||||
let (dst_width, dst_height) = fit_dimensions(src.width(), src.height(), THUMB_SIZE);
|
||||
|
||||
let mut dst = fir::images::Image::new(dst_width, dst_height, src.pixel_type().unwrap());
|
||||
let mut resizer = fir::Resizer::new();
|
||||
let options = fir::ResizeOptions::new()
|
||||
.resize_alg(fir::ResizeAlg::Convolution(fir::FilterType::Lanczos3));
|
||||
resizer.resize(&src, &mut dst, Some(&options))?;
|
||||
|
||||
let thumb = image::RgbaImage::from_raw(dst_width, dst_height, dst.buffer().to_vec())
|
||||
.ok_or_else(|| anyhow!("failed to construct resized thumbnail buffer"))?;
|
||||
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
image::DynamicImage::ImageRgba8(thumb).save_with_format(&out_path, ImageFormat::WebP)?;
|
||||
Ok(GeneratedThumbnail {
|
||||
path: out_path,
|
||||
width: original_dimensions.0,
|
||||
height: original_dimensions.1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_video_thumbnail(
|
||||
tools: &MediaTools,
|
||||
video_path: &Path,
|
||||
cache_dir: &Path,
|
||||
) -> Result<GeneratedThumbnail> {
|
||||
let path_str = video_path.to_string_lossy();
|
||||
let out_path = video_poster_path(cache_dir, &path_str);
|
||||
|
||||
if out_path.exists() {
|
||||
return Ok(GeneratedThumbnail {
|
||||
path: out_path,
|
||||
width: None,
|
||||
height: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let output_path = out_path.to_string_lossy().into_owned();
|
||||
let attempts: [&[&str]; 3] = [
|
||||
&[
|
||||
"-y",
|
||||
"-ss",
|
||||
"00:00:00.000",
|
||||
"-i",
|
||||
path_str.as_ref(),
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"thumbnail,scale=320:-1:force_original_aspect_ratio=decrease",
|
||||
"-q:v",
|
||||
"4",
|
||||
&output_path,
|
||||
],
|
||||
&[
|
||||
"-y",
|
||||
"-ss",
|
||||
"00:00:00.250",
|
||||
"-i",
|
||||
path_str.as_ref(),
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"thumbnail,scale=320:-1:force_original_aspect_ratio=decrease",
|
||||
"-q:v",
|
||||
"4",
|
||||
&output_path,
|
||||
],
|
||||
&[
|
||||
"-y",
|
||||
"-i",
|
||||
path_str.as_ref(),
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"thumbnail,scale=320:-1:force_original_aspect_ratio=decrease",
|
||||
"-q:v",
|
||||
"4",
|
||||
&output_path,
|
||||
],
|
||||
];
|
||||
|
||||
let mut last_error = String::new();
|
||||
for args in attempts {
|
||||
let output = tools.ffmpeg_command().args(args).output()?;
|
||||
if output.status.success() && out_path.exists() {
|
||||
return Ok(GeneratedThumbnail {
|
||||
path: out_path,
|
||||
width: None,
|
||||
height: None,
|
||||
});
|
||||
}
|
||||
last_error = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"ffmpeg failed generating poster for {}: {}",
|
||||
path_str,
|
||||
last_error
|
||||
))
|
||||
}
|
||||
|
||||
pub fn thumb_path(cache_dir: &Path, image_path: &str) -> PathBuf {
|
||||
thumb_path_with_ext(cache_dir, image_path, "webp")
|
||||
}
|
||||
@@ -19,70 +159,19 @@ fn thumb_path_with_ext(cache_dir: &Path, input_path: &str, extension: &str) -> P
|
||||
}
|
||||
|
||||
fn hash_path(s: &str) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
s.hash(&mut hasher);
|
||||
format!("{:016x}", hasher.finish())
|
||||
format!("{:016x}", xxhash_rust::xxh3::xxh3_64(s.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn generate_thumbnail(image_path: &Path, cache_dir: &Path) -> Result<PathBuf> {
|
||||
let path_str = image_path.to_string_lossy();
|
||||
let out_path = thumb_path(cache_dir, &path_str);
|
||||
|
||||
if out_path.exists() {
|
||||
return Ok(out_path);
|
||||
fn fit_dimensions(width: u32, height: u32, max_size: u32) -> (u32, u32) {
|
||||
if width <= max_size && height <= max_size {
|
||||
return (width.max(1), height.max(1));
|
||||
}
|
||||
|
||||
let img = image::open(image_path)?;
|
||||
let thumb = img.thumbnail(THUMB_SIZE, THUMB_SIZE);
|
||||
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
if width >= height {
|
||||
let scaled_height = ((height as f64 / width as f64) * max_size as f64).round() as u32;
|
||||
(max_size, scaled_height.max(1))
|
||||
} else {
|
||||
let scaled_width = ((width as f64 / height as f64) * max_size as f64).round() as u32;
|
||||
(scaled_width.max(1), max_size)
|
||||
}
|
||||
|
||||
thumb.save_with_format(&out_path, ImageFormat::WebP)?;
|
||||
Ok(out_path)
|
||||
}
|
||||
|
||||
pub fn generate_video_poster(video_path: &Path, cache_dir: &Path) -> Result<PathBuf> {
|
||||
let path_str = video_path.to_string_lossy();
|
||||
let out_path = video_poster_path(cache_dir, &path_str);
|
||||
|
||||
if out_path.exists() {
|
||||
return Ok(out_path);
|
||||
}
|
||||
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let status = Command::new("ffmpeg")
|
||||
.args([
|
||||
"-y",
|
||||
"-ss",
|
||||
"00:00:01.000",
|
||||
"-i",
|
||||
&path_str,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"scale=320:-1:force_original_aspect_ratio=decrease",
|
||||
"-q:v",
|
||||
"4",
|
||||
out_path.to_string_lossy().as_ref(),
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow!("ffmpeg failed generating poster for {}", path_str));
|
||||
}
|
||||
|
||||
Ok(out_path)
|
||||
}
|
||||
|
||||
/// Gets image dimensions without fully decoding.
|
||||
pub fn get_dimensions(image_path: &Path) -> Option<(u32, u32)> {
|
||||
image::image_dimensions(image_path).ok()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user