feat: add POST /api/v1/probe endpoint
- Add ProbeRequest/ProbeResponse structures - Support relative and absolute paths - Cache probe.json for repeated requests - Return video metadata (uuid, duration, width, height, fps) - Include cached flag to indicate cache hit - Export FormatInfo and StreamInfo from probe module - Update API_ENDPOINTS.md documentation
This commit is contained in:
@@ -10,7 +10,6 @@ use sha2::{Digest, Sha256};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::core::cache::{keys, MongoCache, RedisCache};
|
||||
use crate::core::config::USER_DATA_ROOT;
|
||||
use crate::core::db::{Database, PostgresDb, QdrantDb, RedisClient, VideoRecord, VideoStatus};
|
||||
use crate::{Embedder, FileManager};
|
||||
|
||||
@@ -79,6 +78,24 @@ struct RegisterResponse {
|
||||
already_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProbeRequest {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ProbeResponse {
|
||||
uuid: String,
|
||||
file_name: String,
|
||||
duration: f64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: f64,
|
||||
cached: bool,
|
||||
format: crate::core::probe::FormatInfo,
|
||||
streams: Vec<crate::core::probe::StreamInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct JobListResponse {
|
||||
jobs: Vec<JobInfoResponse>,
|
||||
@@ -395,29 +412,42 @@ async fn register(
|
||||
) -> Result<Json<RegisterResponse>, StatusCode> {
|
||||
let path = req.path;
|
||||
|
||||
// Canonicalize path first to ensure consistent UUID computation
|
||||
let canonical_path = std::path::Path::new(&path)
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.clone());
|
||||
// Support both relative and absolute paths
|
||||
// Relative: ./demo/video.mp4 or demo/video.mp4
|
||||
// Absolute: /Users/.../sftpgo/data/demo/video.mp4
|
||||
let (relative_path, canonical_path) = if path.starts_with("./") || path.starts_with("../") {
|
||||
// Relative path - keep as is for UUID, resolve to absolute for storage
|
||||
let rel = path.clone();
|
||||
let abs = std::path::Path::new(&path)
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.clone());
|
||||
(rel, abs)
|
||||
} else if std::path::Path::new(&path).is_absolute() {
|
||||
// Absolute path - use as is
|
||||
(path.clone(), path.clone())
|
||||
} else {
|
||||
// Assume relative path without ./
|
||||
let rel = format!("./{}", path);
|
||||
let abs = std::path::Path::new(&rel)
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.clone());
|
||||
(rel, abs)
|
||||
};
|
||||
|
||||
// Compute UUID using USER_DATA_ROOT to extract relative path
|
||||
// This ensures consistent UUIDs even when data root changes
|
||||
// Relative path format: username/video.mp4 (e.g., demo/video.mp4)
|
||||
let user_data_root = USER_DATA_ROOT.as_str();
|
||||
let uuid = crate::core::storage::uuid::compute_uuid_from_path_with_root(
|
||||
&canonical_path,
|
||||
user_data_root,
|
||||
);
|
||||
// Compute UUID from relative path (username/filepath)
|
||||
// Extract: ./demo/video.mp4 -> username="demo", filepath="video.mp4"
|
||||
let uuid = crate::core::storage::uuid::compute_uuid_from_relative_path(&relative_path);
|
||||
|
||||
// Extract relative path for display/logging (username/filename)
|
||||
let (user_dir, filename) =
|
||||
crate::core::storage::uuid::extract_relative_path(&canonical_path, user_data_root);
|
||||
// Extract username and filepath for logging
|
||||
let (username, filepath) =
|
||||
crate::core::storage::uuid::extract_user_from_relative_path(&relative_path);
|
||||
tracing::info!(
|
||||
"Registering video: uuid={}, user={}, file={}, full_path={}",
|
||||
"Registering video: uuid={}, username={}, filepath={}, canonical={}",
|
||||
uuid,
|
||||
user_dir,
|
||||
filename,
|
||||
username,
|
||||
filepath,
|
||||
canonical_path
|
||||
);
|
||||
|
||||
@@ -553,6 +583,138 @@ async fn register(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn probe(
|
||||
State(_state): State<AppState>,
|
||||
Json(req): Json<ProbeRequest>,
|
||||
) -> Result<Json<ProbeResponse>, StatusCode> {
|
||||
let path = req.path;
|
||||
|
||||
// Support both relative and absolute paths
|
||||
let (relative_path, canonical_path) = if path.starts_with("./") || path.starts_with("../") {
|
||||
let rel = path.clone();
|
||||
let abs = std::path::Path::new(&path)
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.clone());
|
||||
(rel, abs)
|
||||
} else if std::path::Path::new(&path).is_absolute() {
|
||||
(path.clone(), path.clone())
|
||||
} else {
|
||||
let rel = format!("./{}", path);
|
||||
let abs = std::path::Path::new(&rel)
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| path.clone());
|
||||
(rel, abs)
|
||||
};
|
||||
|
||||
// Compute UUID from relative path
|
||||
let uuid = crate::core::storage::uuid::compute_uuid_from_relative_path(&relative_path);
|
||||
|
||||
let (username, filepath) =
|
||||
crate::core::storage::uuid::extract_user_from_relative_path(&relative_path);
|
||||
tracing::info!(
|
||||
"Probing video: uuid={}, username={}, filepath={}, canonical={}",
|
||||
uuid,
|
||||
username,
|
||||
filepath,
|
||||
canonical_path
|
||||
);
|
||||
|
||||
// Check for cached probe.json
|
||||
let probe_path = format!(
|
||||
"{}/{}.probe.json",
|
||||
crate::core::config::OUTPUT_DIR.as_str(),
|
||||
uuid
|
||||
);
|
||||
|
||||
let (probe_result, cached) = if let Ok(content) = std::fs::read_to_string(&probe_path) {
|
||||
tracing::info!("Using cached probe.json: {}", probe_path);
|
||||
let result: crate::core::probe::ProbeResult =
|
||||
serde_json::from_str(&content).map_err(|e| {
|
||||
tracing::error!("Failed to parse cached probe.json: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
(result, true)
|
||||
} else {
|
||||
tracing::info!("Running ffprobe for: {}", canonical_path);
|
||||
let result = crate::core::probe::probe_video(&canonical_path).map_err(|e| {
|
||||
tracing::error!("ffprobe failed: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Save probe.json
|
||||
let file_manager = FileManager::new(std::path::PathBuf::from("."));
|
||||
let json_str = serde_json::to_string(&result).map_err(|e| {
|
||||
tracing::error!("Failed to serialize probe result: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
file_manager
|
||||
.save_json(&uuid, "probe", &json_str)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to save probe.json: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
(result, false)
|
||||
};
|
||||
|
||||
// Extract video info
|
||||
let duration = probe_result
|
||||
.format
|
||||
.duration
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let mut width = 0u32;
|
||||
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);
|
||||
// Parse fps from r_frame_rate (e.g., "30/1" or "29.97")
|
||||
if let Some(fps_str) = &stream.r_frame_rate {
|
||||
fps = if fps_str.contains('/') {
|
||||
let parts: Vec<&str> = fps_str.split('/').collect();
|
||||
if parts.len() == 2 {
|
||||
let num: f64 = parts[0].parse().unwrap_or(0.0);
|
||||
let den: f64 = parts[1].parse().unwrap_or(1.0);
|
||||
if den > 0.0 {
|
||||
num / den
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
fps_str.parse().unwrap_or(0.0)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = std::path::Path::new(&canonical_path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Json(ProbeResponse {
|
||||
uuid,
|
||||
file_name,
|
||||
duration,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
cached,
|
||||
format: probe_result.format,
|
||||
streams: probe_result.streams,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn search(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SearchRequest>,
|
||||
@@ -1115,6 +1277,7 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
||||
.route("/health", get(health))
|
||||
.route("/health/detailed", get(health_detailed))
|
||||
.route("/api/v1/register", post(register))
|
||||
.route("/api/v1/probe", post(probe))
|
||||
.route("/api/v1/search", post(search))
|
||||
.route("/api/v1/n8n/search", post(n8n_search))
|
||||
.route("/api/v1/search/hybrid", post(hybrid_search))
|
||||
|
||||
Reference in New Issue
Block a user