use serde_json::Value;
use std::{fs, path::Path, process::Command};
use crate::{AudioMetadata, BasicMetadata, DEFAULT_AUDIO_SAMPLING_RATE, Ffmpeg, ImageMetadata};
pub fn get_video_metadata_json(ffmpeg: &Ffmpeg, path: &str) -> Result<Value, String> {
ffmpeg.get_video_info_json(path)
}
pub fn get_audio_metadata(ffmpeg: &Ffmpeg, path: &str) -> Result<AudioMetadata, String> {
ffmpeg.get_audio_metadata(path)
}
pub fn get_basic_metadata(ffmpeg: &Ffmpeg, path: &str) -> Result<BasicMetadata, String> {
let metadata = ffmpeg.get_video_metadata(path)?;
Ok(BasicMetadata {
duration: metadata.duration,
width: metadata.width,
height: metadata.height,
fps: metadata.fps,
codec: metadata.codec,
bitrate: metadata.bitrate,
})
}
pub fn get_image_metadata(
ffmpeg: &Ffmpeg,
path: &str,
is_gif: bool,
) -> Result<ImageMetadata, String> {
ffmpeg.get_image_metadata(path, is_gif)
}
pub fn extract_frame_data(
ffmpeg: &Ffmpeg,
source_path: &str,
time: f64,
) -> Result<Vec<u8>, String> {
ffmpeg.extract_frame(source_path, time)
}
pub fn generate_waveform_image(
_ffmpeg: &Ffmpeg,
source_path: &str,
output_path: &Path,
width: u32,
height: u32,
color: &str,
) -> Result<(), String> {
if !Path::new(source_path).exists() {
return Err(format!("Source file not found: {}", source_path));
}
if let Some(parent) = output_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create output directory: {}", e))?;
}
}
let probe_output = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"a:0",
"-show_entries",
"stream=sample_rate,channels",
"-of",
"default=noprint_wrappers=1:nokey=1",
source_path,
])
.output()
.map_err(|e| format!("Failed to probe audio: {}", e))?;
let info = String::from_utf8_lossy(&probe_output.stdout);
let lines: Vec<&str> = info.lines().collect();
let sample_rate = if lines.len() >= 1 {
lines[0]
.parse::<u32>()
.unwrap_or(DEFAULT_AUDIO_SAMPLING_RATE)
} else {
DEFAULT_AUDIO_SAMPLING_RATE
};
let filter = if sample_rate != DEFAULT_AUDIO_SAMPLING_RATE {
format!(
"aresample={},showwavespic=s={}x{}:colors={}",
DEFAULT_AUDIO_SAMPLING_RATE, width, height, color
)
} else {
format!("showwavespic=s={}x{}:colors={}", width, height, color)
};
let output = Command::new("ffmpeg")
.args([
"-i",
source_path,
"-filter_complex",
&filter,
"-frames:v",
"1",
"-y",
output_path.to_str().unwrap(),
])
.output()
.map_err(|e| format!("Failed to generate waveform: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("FFmpeg waveform generation failed: {}", stderr));
}
if !output_path.exists() || output_path.metadata().map(|m| m.len() == 0).unwrap_or(true) {
return Err(format!(
"Waveform file not created or empty: {:?}",
output_path
));
}
Ok(())
}
pub fn generate_text_thumbnail(
_ffmpeg: &Ffmpeg,
content: &str,
output_path: &Path,
width: f64,
height: f64,
font_size: u32,
font_color: &str,
font_path: Option<&str>,
) -> Result<(), String> {
let display_text = if content.len() > 100 {
format!("{}...", &content[..100])
} else {
content.to_string()
};
let escaped_text = display_text
.replace("'", "'\\\\\\''")
.replace(":", "\\:")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("=", "\\=");
let default_font = if cfg!(target_os = "macos") {
"/System/Library/Fonts/Helvetica.ttc"
} else if cfg!(target_os = "windows") {
"C:\\\\Windows\\\\Fonts\\\\arial.ttf"
} else {
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
};
let font = font_path.unwrap_or(default_font);
let output = Command::new("ffmpeg")
.args([
"-f",
"lavfi",
"-i",
&format!("color=c=black:s={}x{}:d=1", width, height),
"-vf",
&format!(
"drawtext=text='{}':fontcolor={}:fontsize={}:x=(w-text_w)/2:y=(h-text_h)/2:fontfile={}",
escaped_text, font_color, font_size, font
),
"-frames:v",
"1",
"-y",
output_path.to_str().unwrap(),
])
.output()
.map_err(|e| format!("Failed to generate text thumbnail: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"FFmpeg text thumbnail generation failed: {}",
stderr
));
}
Ok(())
}
pub fn read_cached_thumbnail(cache_path: &Path) -> Option<String> {
if !cache_path.exists() {
return None;
}
match fs::read(cache_path) {
Ok(data) => {
let base64_str = base64::encode(&data);
Some(format!("data:image/jpeg;base64,{}", base64_str))
}
Err(_) => None,
}
}
pub fn save_thumbnail_to_cache(cache_path: &Path, data: &[u8]) -> Result<(), String> {
if let Some(parent) = cache_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
}
}
fs::write(cache_path, data).map_err(|e| format!("Failed to write thumbnail: {}", e))
}
pub fn is_gif_file(file_name: &str) -> bool {
file_name.to_lowercase().ends_with(".gif")
}
pub fn detect_codec_from_extension(file_name: &str) -> String {
let ext = file_name.to_lowercase();
if ext.ends_with(".png") {
"png".to_string()
} else if ext.ends_with(".jpg") || ext.ends_with(".jpeg") {
"jpeg".to_string()
} else if ext.ends_with(".webp") {
"webp".to_string()
} else if ext.ends_with(".bmp") {
"bmp".to_string()
} else if ext.ends_with(".svg") {
"svg".to_string()
} else if ext.ends_with(".gif") {
"gif".to_string()
} else {
"image".to_string()
}
}