feat: unified probe — dispatcher detects category, runs ffprobe/Python/meta per file type
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
pub mod ffprobe;
|
||||
pub mod unified;
|
||||
|
||||
pub use ffprobe::{probe_video, FormatInfo, ProbeResult, StreamInfo};
|
||||
|
||||
135
src/core/probe/unified.rs
Normal file
135
src/core/probe/unified.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// File category derived from extension
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FileCategory {
|
||||
Video,
|
||||
Image,
|
||||
Document,
|
||||
Spreadsheet,
|
||||
Presentation,
|
||||
Archive,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Detect file category from path extension
|
||||
pub fn detect_category(path: &Path) -> FileCategory {
|
||||
let ext = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_lowercase());
|
||||
match ext.as_deref() {
|
||||
Some("mp4" | "mov" | "mkv" | "avi" | "webm" | "m4v" | "mpeg") => FileCategory::Video,
|
||||
Some("jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "svg" | "heic" | "tiff") => FileCategory::Image,
|
||||
Some("pdf" | "doc" | "docx" | "odt" | "pages" | "rtf" | "txt" | "md" | "rst") => FileCategory::Document,
|
||||
Some("xls" | "xlsx" | "csv" | "ods" | "numbers") => FileCategory::Spreadsheet,
|
||||
Some("ppt" | "pptx" | "odp" | "key") => FileCategory::Presentation,
|
||||
Some("zip" | "tar" | "gz" | "tgz" | "7z" | "rar") => FileCategory::Archive,
|
||||
_ => FileCategory::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build universal format info from filesystem metadata
|
||||
pub fn base_format_info(path: &Path) -> serde_json::Value {
|
||||
let meta = std::fs::metadata(path).ok();
|
||||
let size = meta.as_ref().map(|m| m.len()).unwrap_or(0);
|
||||
let mtime = meta.as_ref()
|
||||
.and_then(|m| m.modified().ok())
|
||||
.and_then(|t| {
|
||||
let secs = t.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_secs() as i64;
|
||||
chrono::DateTime::from_timestamp(secs, 0)
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let fname = path.to_string_lossy().to_string();
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
|
||||
let cat = detect_category(path);
|
||||
let file_type = match cat {
|
||||
FileCategory::Video => "video",
|
||||
FileCategory::Image => "image",
|
||||
FileCategory::Document => "document",
|
||||
FileCategory::Spreadsheet => "spreadsheet",
|
||||
FileCategory::Presentation => "presentation",
|
||||
FileCategory::Archive => "archive",
|
||||
FileCategory::Unknown => "unknown",
|
||||
};
|
||||
serde_json::json!({
|
||||
"filename": fname,
|
||||
"format_name": ext,
|
||||
"file_type": file_type,
|
||||
"size": size.to_string(),
|
||||
"mtime": mtime,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run ffprobe for video/image files
|
||||
fn ffprobe_probe(path: &Path, format_base: serde_json::Value) -> serde_json::Value {
|
||||
let canonical = path.to_string_lossy();
|
||||
if let Ok(result) = crate::core::probe::probe_video(&canonical) {
|
||||
if let Ok(mut val) = serde_json::to_value(&result) {
|
||||
if let Some(obj) = val.as_object_mut() {
|
||||
obj.insert("format".to_string(), format_base);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
// ffprobe failed — return minimal
|
||||
serde_json::json!({
|
||||
"format": format_base,
|
||||
"streams": []
|
||||
})
|
||||
}
|
||||
|
||||
/// Run Python probe for document/spreadsheet/presentation files
|
||||
fn python_probe(path: &Path, category: &FileCategory, scripts_dir: &str, python_path: &str, format_base: serde_json::Value) -> serde_json::Value {
|
||||
let script = format!("{}/probe_file.py", scripts_dir);
|
||||
if !std::path::Path::new(&script).exists() {
|
||||
return minimal_probe(format_base);
|
||||
}
|
||||
match std::process::Command::new(python_path)
|
||||
.arg(&script)
|
||||
.arg(path.to_string_lossy().as_ref())
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(mut result) = serde_json::from_str::<serde_json::Value>(&stdout) {
|
||||
if let Some(obj) = result.as_object_mut() {
|
||||
obj.insert("format".to_string(), format_base);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
minimal_probe(format_base)
|
||||
}
|
||||
_ => minimal_probe(format_base),
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal fallback — filesystem metadata only
|
||||
fn minimal_probe(format_base: serde_json::Value) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"format": format_base,
|
||||
"streams": []
|
||||
})
|
||||
}
|
||||
|
||||
/// Unified probe: dispatches to the right probe based on file type
|
||||
/// Returns a probe_json-compatible Value
|
||||
pub async fn unified_probe(
|
||||
path: &Path,
|
||||
scripts_dir: &str,
|
||||
python_path: &str,
|
||||
) -> serde_json::Value {
|
||||
let cat = detect_category(path);
|
||||
let format_base = base_format_info(path);
|
||||
|
||||
match cat {
|
||||
FileCategory::Video | FileCategory::Image => {
|
||||
ffprobe_probe(path, format_base)
|
||||
}
|
||||
FileCategory::Document | FileCategory::Spreadsheet | FileCategory::Presentation => {
|
||||
python_probe(path, &cat, scripts_dir, python_path, format_base)
|
||||
}
|
||||
_ => minimal_probe(format_base),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user