feat: update core API, database layer, and worker modules
- Remove unused imports (n8n_search, universal_search, Client, Arc, etc.) - Update API endpoints for identity, face recognition, search - Fix postgres_db.rs search_videos parent_uuid column - Add snapshot API and identity agent API - Clean up backup files (.bak, .bak2)
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
use sqlx;
|
||||
use std::path::Path;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::core::db::{Database, PostgresDb, VideoRecord, VideoStatus};
|
||||
use crate::core::db::{PostgresDb, VideoRecord, VideoStatus};
|
||||
use crate::core::probe;
|
||||
use crate::core::storage::uuid as uuid_utils;
|
||||
use crate::core::storage::FileManager;
|
||||
use crate::uuid as uuid_utils;
|
||||
|
||||
/// Handles the automatic ingestion of video files.
|
||||
/// This service is responsible for:
|
||||
/// 1. Running `ffprobe` (Pre-processing)
|
||||
/// 2. Saving probe JSON
|
||||
/// 3. Registering the video in the database (making it visible in the API)
|
||||
pub struct IngestionService {
|
||||
db: PostgresDb,
|
||||
}
|
||||
@@ -21,20 +18,56 @@ impl IngestionService {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Registers a video file found in the watched directory.
|
||||
/// This function is idempotent: if the video (UUID) already exists, it skips.
|
||||
pub async fn ingest(&self, file_path: &str) -> Result<Option<String>> {
|
||||
let path = Path::new(file_path);
|
||||
|
||||
// 1. Validate extension
|
||||
if !is_video_extension(path) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 2. Compute UUID
|
||||
let uuid = uuid_utils::compute_uuid_from_path(file_path);
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let filename = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Stable UUID based on MAC + Birthday + Filename.
|
||||
// Moving the file (path change) keeps the SAME identity.
|
||||
|
||||
// 1. Look for existing Birthday (Identity Anchor)
|
||||
// If the file (by name) was registered before, use its original birth time.
|
||||
let birthday = sqlx::query_scalar::<_, chrono::DateTime<chrono::Utc>>(
|
||||
"SELECT registration_time FROM dev.videos WHERE file_name = $1 AND registration_time IS NOT NULL LIMIT 1"
|
||||
)
|
||||
.bind(&filename)
|
||||
.fetch_optional(self.db.pool())
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|t| t.to_rfc3339())
|
||||
.unwrap_or_else(|| Utc::now().to_rfc3339());
|
||||
|
||||
let parent = canonical_path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 2. Compute UUID
|
||||
let uuid = uuid_utils::compute_birth_uuid(
|
||||
&uuid_utils::get_mac_address(),
|
||||
&birthday,
|
||||
&canonical_path.to_string_lossy(),
|
||||
&filename,
|
||||
);
|
||||
|
||||
let parent = canonical_path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let username = uuid_utils::extract_username_from_path(&parent);
|
||||
|
||||
// 3. Check if already registered
|
||||
if let Ok(Some(_)) = self.db.get_video_by_uuid(&uuid).await {
|
||||
info!(
|
||||
"Video already registered: {} ({})",
|
||||
@@ -46,11 +79,9 @@ impl IngestionService {
|
||||
|
||||
info!("Starting ingestion for: {} ({})", path.display(), uuid);
|
||||
|
||||
// 4. Run ffprobe
|
||||
let probe_result = probe::probe_video(file_path)
|
||||
.with_context(|| format!("Failed to probe video: {}", file_path))?;
|
||||
|
||||
// 5. Extract metadata
|
||||
let duration = probe_result
|
||||
.format
|
||||
.duration
|
||||
@@ -78,7 +109,6 @@ impl IngestionService {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Save Probe JSON
|
||||
let file_manager = FileManager::new(std::path::PathBuf::from("."));
|
||||
let probe_json_str = serde_json::to_string_pretty(&probe_result)?;
|
||||
|
||||
@@ -88,33 +118,72 @@ impl IngestionService {
|
||||
info!("Probe JSON saved for {}", uuid);
|
||||
}
|
||||
|
||||
// 7. Create Record
|
||||
// Use absolute path for safety
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let total_frames = {
|
||||
let video_stream = probe_result
|
||||
.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 {
|
||||
if let Ok(nb_frames) = nb_frames_str.parse::<u64>() {
|
||||
info!(
|
||||
"Using nb_frames from ffprobe: {} frames for {}",
|
||||
nb_frames,
|
||||
path.display()
|
||||
);
|
||||
Some(nb_frames)
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to parse nb_frames, using duration * fps fallback for {}",
|
||||
path.display()
|
||||
);
|
||||
Some((duration * fps).floor() as u64)
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"nb_frames not available, using duration * fps fallback for {}",
|
||||
path.display()
|
||||
);
|
||||
Some((duration * fps).floor() as u64)
|
||||
}
|
||||
} else {
|
||||
warn!("No video stream found for {}", path.display());
|
||||
Some(0)
|
||||
}
|
||||
};
|
||||
|
||||
let birth_registration = serde_json::json!({
|
||||
"registration_source": {
|
||||
"username": username,
|
||||
"original_path": parent,
|
||||
"original_filename": filename
|
||||
}
|
||||
});
|
||||
|
||||
let record = VideoRecord {
|
||||
id: 0,
|
||||
uuid: uuid.clone(),
|
||||
file_uuid: uuid.clone(),
|
||||
file_path: canonical_path.to_string_lossy().to_string(),
|
||||
file_name: path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
file_name: filename,
|
||||
file_type: None,
|
||||
duration,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
probe_json: Some(probe_json_str),
|
||||
storage: Default::default(),
|
||||
status: VideoStatus::Pending, // Ready for processing
|
||||
status: VideoStatus::Pending,
|
||||
processing_status: Some(serde_json::json!({"phase": "REGISTERED"})),
|
||||
birth_registration: None,
|
||||
user_id: None,
|
||||
job_id: None,
|
||||
created_at: String::new(),
|
||||
registration_time: None,
|
||||
total_frames: total_frames.unwrap_or(0),
|
||||
parent_uuid: None,
|
||||
};
|
||||
|
||||
// 8. Insert DB
|
||||
self.db
|
||||
.register_video(&record)
|
||||
.await
|
||||
@@ -125,9 +194,14 @@ impl IngestionService {
|
||||
.await
|
||||
.with_context(|| "Failed to set registration_time")?;
|
||||
|
||||
self.db
|
||||
.update_birth_registration(&uuid, &birth_registration)
|
||||
.await
|
||||
.with_context(|| "Failed to set birth_registration")?;
|
||||
|
||||
info!(
|
||||
"Successfully registered video: {} (UUID: {})",
|
||||
record.file_name, uuid
|
||||
"Successfully registered video: {} (UUID: {}, Birth UUID: {})",
|
||||
record.file_name, uuid, uuid
|
||||
);
|
||||
Ok(Some(uuid))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user