From 8a7ffc94e42e34b3e25322a55ebd1534679a40b3 Mon Sep 17 00:00:00 2001 From: Accusys Date: Fri, 15 May 2026 13:07:45 +0800 Subject: [PATCH] fix: register uses birthday from pre.json (not DB registration_time) for UUID stability - Step 4 UUID computation now reuses birthday from pre.json or file creation time - Removed DB birthday query that overwrote the correct birthday with NOW() - End-to-end verified: watcher UUID now matches registration UUID --- src/api/server.rs | 82 ++++++++++++++++++++++++++++-------------- src/watcher/watcher.rs | 3 +- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/api/server.rs b/src/api/server.rs index e68a3ba..ea5ce4f 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -913,8 +913,44 @@ async fn register_single_file( } }; - // Step 1: Compute SHA256 of full file (or use provided hash) - let content_hash = provided_hash.filter(|h| !h.is_empty()).unwrap_or_else(|| sha256_file(&path).unwrap_or_default()); + // Step 1: Try to load pre-computed data from .pre.json + let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR") + .unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string()); + + let birthday = std::fs::metadata(&path) + .ok() + .and_then(|m| m.created().ok()) + .map(|t| { + let secs = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + chrono::DateTime::from_timestamp(secs as i64, 0) + .map(|dt| dt.to_rfc3339()) + .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()) + }) + .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()); + + let mac_address = crate::core::storage::uuid::get_mac_address(); + let pre_file_uuid = crate::core::storage::uuid::compute_birth_uuid( + &mac_address, &birthday, &canonical_path, &file_name, + ); + let pre_path = std::path::Path::new(&output_dir).join(format!("{}.pre.json", pre_file_uuid)); + let pre_data: Option = std::fs::read_to_string(&pre_path).ok() + .and_then(|s| serde_json::from_str(&s).ok()); + + // Extract content_hash from pre.json or compute fresh + let (content_hash, birthday, _pre_file_uuid) = if let Some(ref pre) = pre_data { + let h = pre.get("content_hash").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let b = pre.get("birthday").and_then(|v| v.as_str()).unwrap_or(&birthday).to_string(); + let u = pre.get("file_uuid").and_then(|v| v.as_str()).unwrap_or(&pre_file_uuid).to_string(); + (h, b, u) + } else { + let h = provided_hash.filter(|h| !h.is_empty()).unwrap_or_else(|| sha256_file(&path).unwrap_or_default()); + (h, birthday, pre_file_uuid) + }; + // Recompute UUID with the resolved birthday + let file_uuid = crate::core::storage::uuid::compute_birth_uuid( + &mac_address, &birthday, &canonical_path, &file_name, + ); + tracing::info!("[REGISTER] UUID inputs: mac={} birthday={} path={} name={} pre_found={} → {}", mac_address, birthday, canonical_path, file_name, pre_data.is_some(), file_uuid); // Step 2: Hash check — same content = already registered (regardless of name) let videos_table = schema::table_name("videos"); @@ -951,18 +987,7 @@ async fn register_single_file( // Step 3: Name check — same name but different content → auto-rename let final_name = resolve_filename(&db, &file_name, &content_hash).await; - // Step 4: Compute UUID (using final resolved name) - let videos_table = schema::table_name("videos"); - let birthday = sqlx::query_scalar::<_, chrono::DateTime>( - &format!("SELECT registration_time FROM {} WHERE file_name = $1 AND registration_time IS NOT NULL LIMIT 1", videos_table) - ) - .bind(&final_name) - .fetch_optional(db.pool()) - .await - .unwrap_or(None) - .map(|t| t.to_rfc3339()) - .unwrap_or_else(|| chrono::Utc::now().to_rfc3339()); - + // Step 4: Compute UUID using birthday from pre.json or file creation time (never DB registration_time) let mac_address = crate::core::storage::uuid::get_mac_address(); let file_uuid = crate::core::storage::uuid::compute_birth_uuid( &mac_address, @@ -971,21 +996,24 @@ async fn register_single_file( &final_name, ); - // Step 5: Probe (gracefully handle non-video files) - let probe_result = crate::core::probe::probe_video(&canonical_path).ok(); + // Step 5: Probe — use pre.json if available, otherwise run ffprobe + let cached_probe = pre_data.as_ref() + .and_then(|p| p.get("probe_json")) + .and_then(|v| serde_json::from_value::(v.clone()).ok()); + + let probe_result = cached_probe.or_else(|| crate::core::probe::probe_video(&canonical_path).ok()); let file_meta = std::fs::metadata(&canonical_path).ok(); - 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 probe_json: Option = if let Some(ref pre) = pre_data { + pre.get("probe_json").cloned() + } else { + probe_result.as_ref().map(|r| serde_json::to_value(r)).and_then(|r| r.ok()).or_else(|| { + 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"))); diff --git a/src/watcher/watcher.rs b/src/watcher/watcher.rs index 8031ffe..9fe8f35 100644 --- a/src/watcher/watcher.rs +++ b/src/watcher/watcher.rs @@ -109,9 +109,8 @@ async fn pre_process_new_files(directories: &[String]) { let file_uuid = crate::core::storage::uuid::compute_birth_uuid( &mac, &birthday, &canonical_str, &filename, ); - - // Check if .pre.json already exists let pre_path = std::path::PathBuf::from(&output_dir).join(format!("{}.pre.json", file_uuid)); + tracing::info!("[PRE-PROCESS] UUID inputs: mac={} birthday={} path={} name={} → {}", mac, birthday, canonical_str, filename, file_uuid); if pre_path.exists() { continue; // Already pre-processed }