Serve video gifs when requested

This commit is contained in:
Cameron
2025-07-02 15:48:49 -04:00
parent 3fbdba2b9c
commit e5afdd909b
7 changed files with 187 additions and 41 deletions

77
Cargo.lock generated
View File

@@ -320,7 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.15",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@@ -599,7 +599,7 @@ checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"blowfish", "blowfish",
"getrandom", "getrandom 0.2.15",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -1037,12 +1037,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1214,10 +1214,22 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@@ -1632,6 +1644,7 @@ dependencies = [
"rayon", "rayon",
"serde", "serde",
"serde_json", "serde_json",
"tempfile",
"tokio", "tokio",
"walkdir", "walkdir",
] ]
@@ -1802,9 +1815,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.164" version = "0.2.173"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -1839,9 +1852,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "litemap" name = "litemap"
@@ -1974,7 +1987,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -1987,7 +2000,7 @@ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -2432,6 +2445,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -2459,7 +2478,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
] ]
[[package]] [[package]]
@@ -2627,7 +2646,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.15",
"libc", "libc",
"spin", "spin",
"untrusted", "untrusted",
@@ -2651,15 +2670,15 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.41" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -2917,12 +2936,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.14.0" version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [ dependencies = [
"cfg-if",
"fastrand", "fastrand",
"getrandom 0.3.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.95" version = "0.2.95"
@@ -3606,6 +3634,15 @@ dependencies = [
"memchr", "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]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"

View File

@@ -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-otlp = { version = "0.28.0", features = ["default", "metrics", "tracing", "grpc-tonic"] }
opentelemetry-stdout = "0.28.0" opentelemetry-stdout = "0.28.0"
opentelemetry-appender-log = "0.28.0" opentelemetry-appender-log = "0.28.0"
tempfile = "3.20.0"

View File

@@ -134,10 +134,20 @@ pub enum PhotoSize {
Thumb, Thumb,
} }
#[derive(Deserialize)] #[derive(Debug, Deserialize)]
pub struct ThumbnailRequest { pub struct ThumbnailRequest {
pub path: String, pub(crate) path: String,
pub size: Option<PhotoSize>, pub(crate)size: Option<PhotoSize>,
#[serde(default)]
pub(crate)format: Option<ThumbnailFormat>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub enum ThumbnailFormat {
#[serde(rename = "gif")]
Gif,
#[serde(rename = "image")]
Image,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -45,6 +45,7 @@ use crate::tags::*;
use crate::video::actors::{ use crate::video::actors::{
create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage, create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage,
}; };
use crate::video::generate_video_gifs;
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer}; use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
use opentelemetry::{global, KeyValue}; use opentelemetry::{global, KeyValue};
@@ -98,23 +99,29 @@ async fn get_image(
.expect("Error stripping base path prefix from thumbnail"); .expect("Error stripping base path prefix from thumbnail");
let thumbs = &app_state.thumbnail_path; 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); trace!("Thumbnail path: {:?}", thumb_path);
if let Ok(file) = NamedFile::open(&thumb_path) { if let Ok(file) = NamedFile::open(&thumb_path) {
span.set_status(Status::Ok); span.set_status(Status::Ok);
file.into_response(&request) // The NamedFile will automatically set the correct content-type
} else { return file.into_response(&request);
span.set_status(Status::error("Not found"));
HttpResponse::NotFound().finish()
} }
} 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 { } else {
span.set_status(Status::error("Bad photos request")); span.set_status(Status::error("Bad photos request"));
error!("Bad photos request: {}", req.path); 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")] #[get("/image/metadata")]
async fn get_file_metadata( async fn get_file_metadata(
_: Claims, _: Claims,
@@ -591,6 +609,7 @@ fn main() -> std::io::Result<()> {
} }
create_thumbnails(); create_thumbnails();
generate_video_gifs().await;
let app_data = Data::new(AppState::default()); let app_data = Data::new(AppState::default());

View File

@@ -8,6 +8,7 @@ pub struct AppState {
pub base_path: String, pub base_path: String,
pub thumbnail_path: String, pub thumbnail_path: String,
pub video_path: String, pub video_path: String,
pub gif_path: String,
} }
impl AppState { impl AppState {
@@ -16,6 +17,7 @@ impl AppState {
base_path: String, base_path: String,
thumbnail_path: String, thumbnail_path: String,
video_path: String, video_path: String,
gif_path: String,
) -> Self { ) -> Self {
let playlist_generator = PlaylistGenerator::new(); let playlist_generator = PlaylistGenerator::new();
let video_playlist_manager = let video_playlist_manager =
@@ -27,6 +29,7 @@ impl AppState {
base_path, base_path,
thumbnail_path, thumbnail_path,
video_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("BASE_PATH").expect("BASE_PATH was not set in the env"),
env::var("THUMBNAILS").expect("THUMBNAILS 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("VIDEO_PATH").expect("VIDEO_PATH was not set in the env"),
env::var("GIFS_DIRECTORY").expect("GIFS_DIRECTORY was not set in the env"),
) )
} }
} }

View File

@@ -156,7 +156,7 @@ impl Ffmpeg {
} }
async fn create_gif_from_frames(&self, frame_base_dir: &str, output_file: &str) -> Result<i32> { async fn create_gif_from_frames(&self, frame_base_dir: &str, output_file: &str) -> Result<i32> {
Command::new("ffmpeg") let output = Command::new("ffmpeg")
.arg("-y") .arg("-y")
.args(["-framerate", "4"]) .args(["-framerate", "4"])
.args(["-i", &format!("{}/frame_%03d.jpg", frame_base_dir)]) .args(["-i", &format!("{}/frame_%03d.jpg", frame_base_dir)])
@@ -169,10 +169,20 @@ impl Ffmpeg {
.args(["-loop", "0"]) // loop forever .args(["-loop", "0"]) // loop forever
.args(["-final_delay", "75"]) .args(["-final_delay", "75"])
.arg(output_file) .arg(output_file)
.stderr(Stdio::null()) .stderr(Stdio::piped()) // Change this to capture stderr
.status() .stdout(Stdio::piped()) // Optionally capture stdout too
.map_ok(|out| out.code().unwrap_or(-1)) .output()
.inspect_ok(|output| debug!("ffmpeg gif create exit code: {:?}", output)) .await?;
.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))
} }
} }

View File

@@ -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 actors;
pub mod ffmpeg; 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);
});
}