use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{anyhow, Result}; use log::{info, error}; pub struct UploadHook { enabled: bool, video_probe_path: PathBuf, video_register_cli: PathBuf, video_register_dir: PathBuf, video_extensions: Vec, } impl UploadHook { pub fn new( enabled: bool, video_probe_path: PathBuf, video_register_cli: PathBuf, video_register_dir: PathBuf, video_extensions: Vec, ) -> Self { Self { enabled, video_probe_path, video_register_cli, video_register_dir, video_extensions, } } pub fn trigger(&self, file_path: &Path, user_uuid: &str) -> Result<()> { if !self.enabled { return Ok(()); } if !self.is_video_file(file_path) { info!("UploadHook: Skipping non-video file: {:?}", file_path); return Ok(()); } info!("UploadHook: Triggering for file {:?} (user={})", file_path, user_uuid); let probe_json = self.run_video_probe(file_path)?; let video_uuid = self.run_video_register(&probe_json, user_uuid)?; info!("UploadHook: Video registered successfully (UUID={})", video_uuid); Ok(()) } fn run_video_probe(&self, file_path: &Path) -> Result { info!("UploadHook: Running video_probe on {:?}", file_path); let output = Command::new(&self.video_probe_path) .arg(file_path) .output() .map_err(|e| anyhow!("Failed to execute video_probe: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!("UploadHook: video_probe failed: {}", stderr); return Err(anyhow!("video_probe failed with status {}", output.status)); } let stdout = String::from_utf8_lossy(&output.stdout); info!("UploadHook: video_probe output: {}", stdout); let probe_json = file_path.with_extension("probe.json"); Ok(probe_json) } fn run_video_register(&self, probe_json: &Path, user_uuid: &str) -> Result { info!("UploadHook: Running video_register on {:?}", probe_json); let output = Command::new("python3") .arg(&self.video_register_cli) .arg("register") .arg(probe_json) .current_dir(&self.video_register_dir) .env("USER_UUID", user_uuid) .output() .map_err(|e| anyhow!("Failed to execute video_register: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!("UploadHook: video_register failed: {}", stderr); return Err(anyhow!("video_register failed with status {}", output.status)); } let stdout = String::from_utf8_lossy(&output.stdout); info!("UploadHook: video_register output: {}", stdout); let uuid = stdout .lines() .find(|line| line.contains("UUID:") || line.contains("uuid")) .and_then(|line| { if line.contains("UUID:") { line.split(':').nth(1).map(|s| s.trim().to_string()) } else { Some(line.trim().to_string()) } }) .unwrap_or_else(|| "unknown".to_string()); Ok(uuid) } fn is_video_file(&self, path: &Path) -> bool { path.extension() .and_then(|e| e.to_str()) .map(|ext| self.video_extensions.contains(&ext.to_lowercase())) .unwrap_or(false) } } impl Default for UploadHook { fn default() -> Self { Self::new( false, PathBuf::from("/Users/accusys/momentry_core_project/video_probe/target/release/video_probe"), PathBuf::from("cli.py"), PathBuf::from("/Users/accusys/momentry_core_project/video_register"), vec![ "mp4".to_string(), "mov".to_string(), "avi".to_string(), "mkv".to_string(), "webm".to_string(), "flv".to_string(), "wmv".to_string(), "m4v".to_string(), ], ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_video_file() { let hook = UploadHook::default(); assert!(hook.is_video_file(Path::new("test.mp4"))); assert!(hook.is_video_file(Path::new("test.MOV"))); assert!(hook.is_video_file(Path::new("test.avi"))); assert!(!hook.is_video_file(Path::new("test.txt"))); assert!(!hook.is_video_file(Path::new("test.jpg"))); } #[test] fn test_disabled_hook() { let hook = UploadHook::new( false, PathBuf::from("/tmp/video_probe"), PathBuf::from("cli.py"), PathBuf::from("/tmp/video_register"), vec!["mp4".to_string()], ); let result = hook.trigger(Path::new("/tmp/test.mp4"), "user123"); assert!(result.is_ok()); } }