diff --git a/src/api/server.rs b/src/api/server.rs index ab27640..7250820 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -971,79 +971,71 @@ async fn register_single_file( &final_name, ); - // Step 5: Probe - let probe_result = match crate::core::probe::probe_video(&canonical_path) { - Ok(r) => r, - Err(e) => { - return RegisterFileResponse { - success: false, - file_uuid, - file_name, - file_path: canonical_path, - file_type: None, - duration: 0.0, - width: 0, - height: 0, - fps: 0.0, - total_frames: 0, - registration_time: None, - already_exists: false, - message: format!("Probe failed: {}", e), - }; - } - }; + // Step 5: Probe (gracefully handle non-video files) + let probe_result = crate::core::probe::probe_video(&canonical_path).ok(); + let file_meta = std::fs::metadata(&canonical_path).ok(); - let has_video = probe_result - .streams - .iter() - .any(|s| s.codec_type.as_deref() == Some("video")); - let has_audio = probe_result - .streams - .iter() - .any(|s| s.codec_type.as_deref() == Some("audio")); + let probe_json: Option = probe_result.as_ref().map(|r| serde_json::to_value(r)).and_then(|r| r.ok()).or_else(|| { + // Minimal probe info for non-video files + file_meta.map(|m| serde_json::json!({ + "format": { + "size": m.len().to_string(), + "filename": &canonical_path, + "format_name": "unknown" + }, + "streams": [] + })) + }); + + let has_video = probe_result.as_ref().map_or(false, |r| r.streams.iter().any(|s| s.codec_type.as_deref() == Some("video"))); + let has_audio = probe_result.as_ref().map_or(false, |r| r.streams.iter().any(|s| s.codec_type.as_deref() == Some("audio"))); + + // Determine file_type: check ffprobe result, then extension let final_file_type = if has_video { Some("video".to_string()) } else if has_audio { Some("audio".to_string()) } else { - None + let ext = std::path::Path::new(&canonical_path).extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()); + match ext.as_deref() { + Some("jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" | "svg") => Some("image".to_string()), + Some("pdf") => Some("document".to_string()), + Some("doc" | "docx") => Some("document".to_string()), + Some("pages") => Some("document".to_string()), + Some("xls" | "xlsx" | "numbers") => Some("spreadsheet".to_string()), + Some("ppt" | "pptx" | "key") => Some("presentation".to_string()), + _ => probe_result.as_ref().and_then(|r| { + if r.streams.is_empty() && r.format.duration.is_some() { Some("unknown".to_string()) } else { None } + }), + } }; - let duration = probe_result - .format - .duration - .as_ref() + let duration = probe_result.as_ref() + .and_then(|r| r.format.duration.as_ref()) .and_then(|s| s.parse::().ok()) .unwrap_or(0.0); let mut width = 0u32; let mut height = 0u32; let mut fps = 0.0; let mut total_frames = 0u64; - if let Some(s) = probe_result - .streams - .iter() - .find(|s| s.codec_type.as_deref() == Some("video")) - { - width = s.width.unwrap_or(0); - height = s.height.unwrap_or(0); - if let Some(fps_str) = &s.r_frame_rate { - if let Some((num, den)) = fps_str.split_once('/') { - if let (Ok(n), Ok(d)) = (num.parse::(), den.parse::()) { - if d > 0.0 { - fps = n / d; + if let Some(ref probe) = probe_result { + if let Some(s) = probe.streams.iter().find(|s| s.codec_type.as_deref() == Some("video")) { + width = s.width.unwrap_or(0); + height = s.height.unwrap_or(0); + if let Some(fps_str) = &s.r_frame_rate { + if let Some((num, den)) = fps_str.split_once('/') { + if let (Ok(n), Ok(d)) = (num.parse::(), den.parse::()) { + if d > 0.0 { + fps = n / d; + } } } } + total_frames = s.nb_frames.as_ref().and_then(|s| s.parse().ok()).unwrap_or((duration * fps) as u64); } - total_frames = s - .nb_frames - .as_ref() - .and_then(|s| s.parse().ok()) - .unwrap_or((duration * fps) as u64); } let videos_table = schema::table_name("videos"); - let probe_json = serde_json::to_value(&probe_result).ok(); let status = "pending"; let _ = sqlx::query(&format!( "INSERT INTO {} (file_uuid, file_path, file_name, file_type, duration, width, height, fps, probe_json, status, content_hash, registration_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW()) ON CONFLICT (file_uuid) DO UPDATE SET file_path = EXCLUDED.file_path, file_name = EXCLUDED.file_name, status = EXCLUDED.status, content_hash = EXCLUDED.content_hash", @@ -1135,18 +1127,20 @@ async fn register_single_file( } // 更新 DB: cut_done, scene_done, audio_tracks - let audio_tracks: Vec = probe_result.streams.iter() - .filter(|s| s.codec_type.as_deref() == Some("audio")) - .map(|s| { - serde_json::json!({ - "index": s.index, - "codec": s.codec_name, - "channels": s.channels, - "sample_rate": s.sample_rate, - "language": s.tags.as_ref().and_then(|t| t.get("language")).unwrap_or(&serde_json::Value::Null), + let audio_tracks: Vec = probe_result.as_ref().map_or(vec![], |pr| { + pr.streams.iter() + .filter(|s| s.codec_type.as_deref() == Some("audio")) + .map(|s| { + serde_json::json!({ + "index": s.index, + "codec": s.codec_name, + "channels": s.channels, + "sample_rate": s.sample_rate, + "language": s.tags.as_ref().and_then(|t| t.get("language")).unwrap_or(&serde_json::Value::Null), + }) }) - }) - .collect(); + .collect() + }); let audio_tracks_json = serde_json::to_value(&audio_tracks).ok(); // 計算 cut_count 與 cut_max_duration let cut_path = std::path::Path::new( diff --git a/src/core/ingestion.rs b/src/core/ingestion.rs index 00c63ec..b31992e 100644 --- a/src/core/ingestion.rs +++ b/src/core/ingestion.rs @@ -48,11 +48,6 @@ impl IngestionService { pub async fn ingest(&self, file_path: &str) -> Result> { let path = Path::new(file_path); - - if !is_video_extension(path) { - return Ok(None); - } - let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); let filename = path .file_name() @@ -110,13 +105,11 @@ impl IngestionService { info!("Starting ingestion for: {} ({})", path.display(), uuid); - let probe_result = probe::probe_video(file_path) - .with_context(|| format!("Failed to probe video: {}", file_path))?; + let probe_result = probe::probe_video(file_path).ok(); + let file_meta = std::fs::metadata(&canonical_path).ok(); - let duration = probe_result - .format - .duration - .as_ref() + let duration = probe_result.as_ref() + .and_then(|r| r.format.duration.as_ref()) .and_then(|s| s.parse::().ok()) .unwrap_or(0.0); @@ -124,15 +117,17 @@ impl IngestionService { let mut height = 0u32; let mut fps = 0.0; - for stream in &probe_result.streams { - if stream.codec_type.as_deref() == Some("video") { - width = stream.width.unwrap_or(0); - height = stream.height.unwrap_or(0); - if let Some(fps_str) = &stream.r_frame_rate { - if let Some((num, den)) = fps_str.split_once('/') { - if let (Ok(n), Ok(d)) = (num.parse::(), den.parse::()) { - if d > 0.0 { - fps = n / d; + if let Some(ref probe) = probe_result { + for stream in &probe.streams { + if stream.codec_type.as_deref() == Some("video") { + width = stream.width.unwrap_or(0); + height = stream.height.unwrap_or(0); + if let Some(fps_str) = &stream.r_frame_rate { + if let Some((num, den)) = fps_str.split_once('/') { + if let (Ok(n), Ok(d)) = (num.parse::(), den.parse::()) { + if d > 0.0 { + fps = n / d; + } } } } @@ -141,7 +136,10 @@ impl IngestionService { } let file_manager = FileManager::new(std::path::PathBuf::from(".")); - let probe_json_str = serde_json::to_string_pretty(&probe_result)?; + let probe_json_val: serde_json::Value = probe_result.as_ref().map(|r| serde_json::to_value(r)).and_then(|r| r.ok()).unwrap_or_else(|| { + serde_json::json!({"format": {"size": file_meta.map(|m| m.len().to_string()).unwrap_or_default(), "filename": &canonical_path.to_string_lossy().to_string(), "format_name": "unknown"}, "streams": []}) + }); + let probe_json_str = serde_json::to_string_pretty(&probe_json_val)?; if let Err(e) = file_manager.save_json(&uuid, "probe", &probe_json_str) { warn!("Failed to save probe JSON for {}: {}", uuid, e); @@ -150,10 +148,7 @@ impl IngestionService { } let total_frames = { - let video_stream = probe_result - .streams - .iter() - .find(|s| s.codec_type.as_deref() == Some("video")); + let video_stream = probe_result.as_ref().and_then(|pr| pr.streams.iter().find(|s| s.codec_type.as_deref() == Some("video"))); if let Some(stream) = video_stream { if let Some(nb_frames_str) = &stream.nb_frames { @@ -249,11 +244,4 @@ impl IngestionService { } } -fn is_video_extension(path: &Path) -> bool { - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - let ext = ext.to_lowercase(); - matches!(ext.as_str(), "mp4" | "mov" | "mkv" | "avi" | "webm" | "m4v") - } else { - false - } -} +