feat: register non-video files — graceful probe fallback for svg/pdf/docx/pages etc
This commit is contained in:
@@ -971,79 +971,71 @@ async fn register_single_file(
|
|||||||
&final_name,
|
&final_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 5: Probe
|
// Step 5: Probe (gracefully handle non-video files)
|
||||||
let probe_result = match crate::core::probe::probe_video(&canonical_path) {
|
let probe_result = crate::core::probe::probe_video(&canonical_path).ok();
|
||||||
Ok(r) => r,
|
let file_meta = std::fs::metadata(&canonical_path).ok();
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let has_video = probe_result
|
let probe_json: Option<serde_json::Value> = probe_result.as_ref().map(|r| serde_json::to_value(r)).and_then(|r| r.ok()).or_else(|| {
|
||||||
.streams
|
// Minimal probe info for non-video files
|
||||||
.iter()
|
file_meta.map(|m| serde_json::json!({
|
||||||
.any(|s| s.codec_type.as_deref() == Some("video"));
|
"format": {
|
||||||
let has_audio = probe_result
|
"size": m.len().to_string(),
|
||||||
.streams
|
"filename": &canonical_path,
|
||||||
.iter()
|
"format_name": "unknown"
|
||||||
.any(|s| s.codec_type.as_deref() == Some("audio"));
|
},
|
||||||
|
"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 {
|
let final_file_type = if has_video {
|
||||||
Some("video".to_string())
|
Some("video".to_string())
|
||||||
} else if has_audio {
|
} else if has_audio {
|
||||||
Some("audio".to_string())
|
Some("audio".to_string())
|
||||||
} else {
|
} 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
|
let duration = probe_result.as_ref()
|
||||||
.format
|
.and_then(|r| r.format.duration.as_ref())
|
||||||
.duration
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.parse::<f64>().ok())
|
.and_then(|s| s.parse::<f64>().ok())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let mut width = 0u32;
|
let mut width = 0u32;
|
||||||
let mut height = 0u32;
|
let mut height = 0u32;
|
||||||
let mut fps = 0.0;
|
let mut fps = 0.0;
|
||||||
let mut total_frames = 0u64;
|
let mut total_frames = 0u64;
|
||||||
if let Some(s) = probe_result
|
if let Some(ref probe) = probe_result {
|
||||||
.streams
|
if let Some(s) = probe.streams.iter().find(|s| s.codec_type.as_deref() == Some("video")) {
|
||||||
.iter()
|
width = s.width.unwrap_or(0);
|
||||||
.find(|s| s.codec_type.as_deref() == Some("video"))
|
height = s.height.unwrap_or(0);
|
||||||
{
|
if let Some(fps_str) = &s.r_frame_rate {
|
||||||
width = s.width.unwrap_or(0);
|
if let Some((num, den)) = fps_str.split_once('/') {
|
||||||
height = s.height.unwrap_or(0);
|
if let (Ok(n), Ok(d)) = (num.parse::<f64>(), den.parse::<f64>()) {
|
||||||
if let Some(fps_str) = &s.r_frame_rate {
|
if d > 0.0 {
|
||||||
if let Some((num, den)) = fps_str.split_once('/') {
|
fps = n / d;
|
||||||
if let (Ok(n), Ok(d)) = (num.parse::<f64>(), den.parse::<f64>()) {
|
}
|
||||||
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 videos_table = schema::table_name("videos");
|
||||||
let probe_json = serde_json::to_value(&probe_result).ok();
|
|
||||||
let status = "pending";
|
let status = "pending";
|
||||||
let _ = sqlx::query(&format!(
|
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",
|
"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
|
// 更新 DB: cut_done, scene_done, audio_tracks
|
||||||
let audio_tracks: Vec<serde_json::Value> = probe_result.streams.iter()
|
let audio_tracks: Vec<serde_json::Value> = probe_result.as_ref().map_or(vec![], |pr| {
|
||||||
.filter(|s| s.codec_type.as_deref() == Some("audio"))
|
pr.streams.iter()
|
||||||
.map(|s| {
|
.filter(|s| s.codec_type.as_deref() == Some("audio"))
|
||||||
serde_json::json!({
|
.map(|s| {
|
||||||
"index": s.index,
|
serde_json::json!({
|
||||||
"codec": s.codec_name,
|
"index": s.index,
|
||||||
"channels": s.channels,
|
"codec": s.codec_name,
|
||||||
"sample_rate": s.sample_rate,
|
"channels": s.channels,
|
||||||
"language": s.tags.as_ref().and_then(|t| t.get("language")).unwrap_or(&serde_json::Value::Null),
|
"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();
|
let audio_tracks_json = serde_json::to_value(&audio_tracks).ok();
|
||||||
// 計算 cut_count 與 cut_max_duration
|
// 計算 cut_count 與 cut_max_duration
|
||||||
let cut_path = std::path::Path::new(
|
let cut_path = std::path::Path::new(
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ impl IngestionService {
|
|||||||
|
|
||||||
pub async fn ingest(&self, file_path: &str) -> Result<Option<String>> {
|
pub async fn ingest(&self, file_path: &str) -> Result<Option<String>> {
|
||||||
let path = Path::new(file_path);
|
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 canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||||
let filename = path
|
let filename = path
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -110,13 +105,11 @@ impl IngestionService {
|
|||||||
|
|
||||||
info!("Starting ingestion for: {} ({})", path.display(), uuid);
|
info!("Starting ingestion for: {} ({})", path.display(), uuid);
|
||||||
|
|
||||||
let probe_result = probe::probe_video(file_path)
|
let probe_result = probe::probe_video(file_path).ok();
|
||||||
.with_context(|| format!("Failed to probe video: {}", file_path))?;
|
let file_meta = std::fs::metadata(&canonical_path).ok();
|
||||||
|
|
||||||
let duration = probe_result
|
let duration = probe_result.as_ref()
|
||||||
.format
|
.and_then(|r| r.format.duration.as_ref())
|
||||||
.duration
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.parse::<f64>().ok())
|
.and_then(|s| s.parse::<f64>().ok())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
@@ -124,15 +117,17 @@ impl IngestionService {
|
|||||||
let mut height = 0u32;
|
let mut height = 0u32;
|
||||||
let mut fps = 0.0;
|
let mut fps = 0.0;
|
||||||
|
|
||||||
for stream in &probe_result.streams {
|
if let Some(ref probe) = probe_result {
|
||||||
if stream.codec_type.as_deref() == Some("video") {
|
for stream in &probe.streams {
|
||||||
width = stream.width.unwrap_or(0);
|
if stream.codec_type.as_deref() == Some("video") {
|
||||||
height = stream.height.unwrap_or(0);
|
width = stream.width.unwrap_or(0);
|
||||||
if let Some(fps_str) = &stream.r_frame_rate {
|
height = stream.height.unwrap_or(0);
|
||||||
if let Some((num, den)) = fps_str.split_once('/') {
|
if let Some(fps_str) = &stream.r_frame_rate {
|
||||||
if let (Ok(n), Ok(d)) = (num.parse::<f64>(), den.parse::<f64>()) {
|
if let Some((num, den)) = fps_str.split_once('/') {
|
||||||
if d > 0.0 {
|
if let (Ok(n), Ok(d)) = (num.parse::<f64>(), den.parse::<f64>()) {
|
||||||
fps = n / d;
|
if d > 0.0 {
|
||||||
|
fps = n / d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +136,10 @@ impl IngestionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let file_manager = FileManager::new(std::path::PathBuf::from("."));
|
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) {
|
if let Err(e) = file_manager.save_json(&uuid, "probe", &probe_json_str) {
|
||||||
warn!("Failed to save probe JSON for {}: {}", uuid, e);
|
warn!("Failed to save probe JSON for {}: {}", uuid, e);
|
||||||
@@ -150,10 +148,7 @@ impl IngestionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let total_frames = {
|
let total_frames = {
|
||||||
let video_stream = probe_result
|
let video_stream = probe_result.as_ref().and_then(|pr| pr.streams.iter().find(|s| s.codec_type.as_deref() == Some("video")));
|
||||||
.streams
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.codec_type.as_deref() == Some("video"));
|
|
||||||
|
|
||||||
if let Some(stream) = video_stream {
|
if let Some(stream) = video_stream {
|
||||||
if let Some(nb_frames_str) = &stream.nb_frames {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user