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

View File

@@ -134,10 +134,20 @@ pub enum PhotoSize {
Thumb,
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub struct ThumbnailRequest {
pub path: String,
pub size: Option<PhotoSize>,
pub(crate) path: String,
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)]

View File

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

View File

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

View File

@@ -156,7 +156,7 @@ impl Ffmpeg {
}
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")
.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))
}
}
}

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 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);
});
}