From e5afdd909b479589606330a06357bb85fe3c4995 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 2 Jul 2025 15:48:49 -0400 Subject: [PATCH] Serve video gifs when requested --- Cargo.lock | 77 +++++++++++++++++++++++++++++++++------------ Cargo.toml | 1 + src/data/mod.rs | 16 ++++++++-- src/main.rs | 41 +++++++++++++++++------- src/state.rs | 4 +++ src/video/ffmpeg.rs | 24 +++++++++----- src/video/mod.rs | 65 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 187 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71d6d3e..fa96ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,7 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -599,7 +599,7 @@ checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e" dependencies = [ "base64 0.22.1", "blowfish", - "getrandom", + "getrandom 0.2.15", "subtle", "zeroize", ] @@ -1037,12 +1037,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1214,10 +1214,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1632,6 +1644,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "tempfile", "tokio", "walkdir", ] @@ -1802,9 +1815,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "libfuzzer-sys" @@ -1839,9 +1852,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -1974,7 +1987,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -1987,7 +2000,7 @@ dependencies = [ "hermit-abi", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2432,6 +2445,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -2459,7 +2478,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2627,7 +2646,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -2651,15 +2670,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2917,12 +2936,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3324,6 +3343,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -3606,6 +3634,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 7c98e1d..a3c12e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ opentelemetry_sdk = { version = "0.28.0", features = ["default", "rt-tokio-curre opentelemetry-otlp = { version = "0.28.0", features = ["default", "metrics", "tracing", "grpc-tonic"] } opentelemetry-stdout = "0.28.0" opentelemetry-appender-log = "0.28.0" +tempfile = "3.20.0" \ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs index b6e7973..4be379d 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -134,10 +134,20 @@ pub enum PhotoSize { Thumb, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct ThumbnailRequest { - pub path: String, - pub size: Option, + pub(crate) path: String, + pub(crate)size: Option, + #[serde(default)] + pub(crate)format: Option, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub enum ThumbnailFormat { + #[serde(rename = "gif")] + Gif, + #[serde(rename = "image")] + Image, } #[derive(Deserialize)] diff --git a/src/main.rs b/src/main.rs index eda3aae..aea239a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,7 @@ use crate::tags::*; use crate::video::actors::{ create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage, }; +use crate::video::generate_video_gifs; use log::{debug, error, info, trace, warn}; use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer}; use opentelemetry::{global, KeyValue}; @@ -98,23 +99,29 @@ async fn get_image( .expect("Error stripping base path prefix from thumbnail"); let thumbs = &app_state.thumbnail_path; - let thumb_path = Path::new(&thumbs).join(relative_path); + let mut thumb_path = Path::new(&thumbs).join(relative_path); + + // If it's a video and GIF format is requested, try to serve GIF thumbnail + if req.format == Some(ThumbnailFormat::Gif) && is_video_file(&path) { + thumb_path = Path::new(&app_state.gif_path).join(relative_path); + thumb_path.set_extension("gif"); + } trace!("Thumbnail path: {:?}", thumb_path); if let Ok(file) = NamedFile::open(&thumb_path) { span.set_status(Status::Ok); - file.into_response(&request) - } else { - span.set_status(Status::error("Not found")); - HttpResponse::NotFound().finish() + // The NamedFile will automatically set the correct content-type + return file.into_response(&request); } - } else if let Ok(file) = NamedFile::open(path) { - span.set_status(Status::Ok); - file.into_response(&request) - } else { - span.set_status(Status::error("Not found")); - HttpResponse::NotFound().finish() } + + if let Ok(file) = NamedFile::open(&path) { + span.set_status(Status::Ok); + return file.into_response(&request); + } + + span.set_status(Status::error("Not found")); + HttpResponse::NotFound().finish() } else { span.set_status(Status::error("Bad photos request")); error!("Bad photos request: {}", req.path); @@ -122,6 +129,17 @@ async fn get_image( } } +fn is_video_file(path: &Path) -> bool { + if let Some(extension) = path.extension() { + matches!( + extension.to_str().unwrap_or("").to_lowercase().as_str(), + "mp4" | "mov" | "avi" | "mkv" + ) + } else { + false + } +} + #[get("/image/metadata")] async fn get_file_metadata( _: Claims, @@ -591,6 +609,7 @@ fn main() -> std::io::Result<()> { } create_thumbnails(); + generate_video_gifs().await; let app_data = Data::new(AppState::default()); diff --git a/src/state.rs b/src/state.rs index 7497285..9c013a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,6 +8,7 @@ pub struct AppState { pub base_path: String, pub thumbnail_path: String, pub video_path: String, + pub gif_path: String, } impl AppState { @@ -16,6 +17,7 @@ impl AppState { base_path: String, thumbnail_path: String, video_path: String, + gif_path: String, ) -> Self { let playlist_generator = PlaylistGenerator::new(); let video_playlist_manager = @@ -27,6 +29,7 @@ impl AppState { base_path, thumbnail_path, video_path, + gif_path, } } } @@ -38,6 +41,7 @@ impl Default for AppState { env::var("BASE_PATH").expect("BASE_PATH was not set in the env"), env::var("THUMBNAILS").expect("THUMBNAILS was not set in the env"), env::var("VIDEO_PATH").expect("VIDEO_PATH was not set in the env"), + env::var("GIFS_DIRECTORY").expect("GIFS_DIRECTORY was not set in the env"), ) } } diff --git a/src/video/ffmpeg.rs b/src/video/ffmpeg.rs index 21dccc7..289b120 100644 --- a/src/video/ffmpeg.rs +++ b/src/video/ffmpeg.rs @@ -156,7 +156,7 @@ impl Ffmpeg { } async fn create_gif_from_frames(&self, frame_base_dir: &str, output_file: &str) -> Result { - Command::new("ffmpeg") + let output = Command::new("ffmpeg") .arg("-y") .args(["-framerate", "4"]) .args(["-i", &format!("{}/frame_%03d.jpg", frame_base_dir)]) @@ -169,10 +169,20 @@ impl Ffmpeg { .args(["-loop", "0"]) // loop forever .args(["-final_delay", "75"]) .arg(output_file) - .stderr(Stdio::null()) - .status() - .map_ok(|out| out.code().unwrap_or(-1)) - .inspect_ok(|output| debug!("ffmpeg gif create exit code: {:?}", output)) - .await + .stderr(Stdio::piped()) // Change this to capture stderr + .stdout(Stdio::piped()) // Optionally capture stdout too + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("FFmpeg error: {}", stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + debug!("FFmpeg stdout: {}", stdout); + } else { + debug!("FFmpeg successful with exit code: {}", output.status); + } + + Ok(output.status.code().unwrap_or(-1)) } -} +} \ No newline at end of file diff --git a/src/video/mod.rs b/src/video/mod.rs index c3c0586..c7618e3 100644 --- a/src/video/mod.rs +++ b/src/video/mod.rs @@ -1,2 +1,67 @@ +use crate::otel::global_tracer; +use crate::video::ffmpeg::{Ffmpeg, GifType}; +use crate::{is_video, update_media_counts}; +use log::{info}; +use opentelemetry::trace::{Tracer}; +use std::path::{Path, PathBuf}; +use std::{fs}; +use walkdir::WalkDir; + pub mod actors; pub mod ffmpeg; + +pub async fn generate_video_gifs() { + tokio::spawn(async { + info!("Starting to make video gifs"); + + let start = std::time::Instant::now(); + let tracer = global_tracer(); + let span = tracer.start("creating video gifs"); + + let gif_base_path = &dotenv::var("GIFS_DIRECTORY").unwrap_or(String::from("gifs")); + let gif_directory: &Path = Path::new(gif_base_path); + fs::create_dir_all(gif_base_path).expect("There was an issue creating directory"); + + let files = PathBuf::from(dotenv::var("BASE_PATH").unwrap()); + + let ffmpeg = Ffmpeg; + for file in WalkDir::new(&files) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file()) + .filter(|entry| is_video(entry)) + .filter(|entry| { + let path = entry.path(); + let relative_path = &path.strip_prefix(&files).unwrap(); + let thumb_path = Path::new(gif_directory).join(relative_path); + let gif_path = thumb_path.with_extension("gif"); + !gif_path.exists() + }) + { + let path = file.path(); + let relative_path = &path.strip_prefix(&files).unwrap(); + let gif_path = Path::new(gif_directory).join(relative_path); + let gif_path = gif_path.with_extension("gif"); + if let Some(parent_dir) = gif_path.parent() { + fs::create_dir_all(parent_dir).expect(&format!( + "There was an issue creating gif directory {:?}", + gif_path + )); + } + info!("Generating gif for {:?}", path); + + ffmpeg + .generate_video_gif( + path.to_str().unwrap(), + gif_path.to_str().unwrap(), + GifType::Overview, + ) + .await + .expect("There was an issue generating the gif"); + } + + info!("Finished making video gifs in {:?}", start.elapsed()); + + update_media_counts(&files); + }); +}