Serve video gifs when requested
This commit is contained in:
@@ -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)]
|
||||
|
||||
41
src/main.rs
41
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());
|
||||
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user