- Remove /api/v1/register (replaced by /api/v1/files/register) - Remove /api/v1/probe (replaced by /api/v1/files/:uuid) - Remove /api/v1/n8n/... (n8n workflow only) - Remove /api/v1/unregister (high risk) - Remove /api/v1/videos list (replaced by /api/v1/files) - Remove /api/v1/people (merged into /api/v1/identities) - Clean up dead code and unused structs
541 lines
15 KiB
Rust
541 lines
15 KiB
Rust
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::Json,
|
|
routing::{get, post},
|
|
Router,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
use crate::core::db::ResourceRecord;
|
|
|
|
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
|
Router::new()
|
|
.route("/api/v1/files", get(list_files))
|
|
.route("/api/v1/files/:uuid", get(get_file_detail))
|
|
.route("/api/v1/files/:uuid/identities", get(get_file_identities))
|
|
.route("/api/v1/identities/:uuid", get(get_identity_detail))
|
|
.route("/api/v1/identities/:uuid/files", get(get_identity_files))
|
|
.route("/api/v1/identities/:uuid/chunks", get(get_identity_chunks))
|
|
.route("/api/v1/resources/register", post(register_resource))
|
|
.route("/api/v1/resources/heartbeat", post(heartbeat_resource))
|
|
.route("/api/v1/resources", get(list_resources))
|
|
}
|
|
|
|
// --- Files Endpoints ---
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FilesQuery {
|
|
page: Option<usize>,
|
|
page_size: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FilesResponse {
|
|
pub success: bool,
|
|
pub total: i64,
|
|
pub page: usize,
|
|
pub page_size: usize,
|
|
pub data: Vec<FileItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileItem {
|
|
pub file_uuid: String,
|
|
pub file_name: String,
|
|
pub file_path: String,
|
|
pub status: String, // From probe or processing status
|
|
}
|
|
|
|
async fn list_files(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Query(params): Query<FilesQuery>,
|
|
) -> Result<Json<FilesResponse>, (StatusCode, String)> {
|
|
let page = params.page.unwrap_or(1);
|
|
let page_size = params.page_size.unwrap_or(20);
|
|
let offset = ((page - 1) as i64) * (page_size as i64);
|
|
|
|
let records = state
|
|
.db
|
|
.list_files(page_size as i32, offset)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data = records
|
|
.into_iter()
|
|
.map(|r| FileItem {
|
|
file_uuid: r.file_uuid,
|
|
file_name: r.file_name,
|
|
file_path: r.file_path,
|
|
status: "ready".to_string(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(FilesResponse {
|
|
success: true,
|
|
total: 0, // TODO
|
|
page,
|
|
page_size,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileDetailResponse {
|
|
pub success: bool,
|
|
pub file_uuid: String,
|
|
pub file_name: String,
|
|
pub file_path: String,
|
|
pub metadata: Option<serde_json::Value>,
|
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
async fn get_file_detail(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid): Path<String>,
|
|
) -> Result<Json<FileDetailResponse>, (StatusCode, String)> {
|
|
let file = state
|
|
.db
|
|
.get_file_by_uuid(&uuid)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
match file {
|
|
Some(f) => Ok(Json(FileDetailResponse {
|
|
success: true,
|
|
file_uuid: f.file_uuid,
|
|
file_name: f.file_name,
|
|
file_path: f.file_path,
|
|
metadata: f.probe_json,
|
|
created_at: f.created_at,
|
|
})),
|
|
None => Err((StatusCode::NOT_FOUND, format!("File not found: {}", uuid))),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileIdentitiesResponse {
|
|
pub success: bool,
|
|
pub file_uuid: String,
|
|
pub total: i64,
|
|
pub page: usize,
|
|
pub page_size: usize,
|
|
pub data: Vec<FileIdentityItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileIdentityItem {
|
|
pub identity_id: i32,
|
|
pub name: String,
|
|
pub metadata: serde_json::Value,
|
|
pub face_count: Option<i32>,
|
|
pub speaker_count: Option<i32>,
|
|
pub first_appearance: Option<f64>,
|
|
pub last_appearance: Option<f64>,
|
|
pub confidence: Option<f64>,
|
|
}
|
|
|
|
async fn get_file_identities(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid): Path<String>,
|
|
Query(params): Query<FilesQuery>,
|
|
) -> Result<Json<FileIdentitiesResponse>, (StatusCode, String)> {
|
|
let page = params.page.unwrap_or(1);
|
|
let page_size = params.page_size.unwrap_or(20);
|
|
let offset = ((page - 1) as i64) * (page_size as i64);
|
|
|
|
let records = state
|
|
.db
|
|
.get_file_identities(&uuid, page_size as i32, offset)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data: Vec<FileIdentityItem> = records
|
|
.into_iter()
|
|
.map(|r| FileIdentityItem {
|
|
identity_id: r.identity_id,
|
|
name: r.name,
|
|
metadata: r.metadata,
|
|
face_count: r.face_count,
|
|
speaker_count: r.speaker_count,
|
|
first_appearance: r.first_appearance,
|
|
last_appearance: r.last_appearance,
|
|
confidence: r.confidence,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(FileIdentitiesResponse {
|
|
success: true,
|
|
file_uuid: uuid,
|
|
total: data.len() as i64,
|
|
page,
|
|
page_size,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityDetailResponse {
|
|
pub success: bool,
|
|
pub uuid: Uuid,
|
|
pub name: String,
|
|
pub identity_type: Option<String>,
|
|
pub source: Option<String>,
|
|
pub status: Option<String>,
|
|
pub metadata: serde_json::Value,
|
|
pub reference_data: serde_json::Value,
|
|
pub tmdb_id: Option<i32>,
|
|
pub tmdb_profile: Option<String>,
|
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
async fn get_identity_detail(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid_str): Path<String>,
|
|
) -> Result<Json<IdentityDetailResponse>, (StatusCode, String)> {
|
|
let uuid = Uuid::parse_str(&uuid_str)
|
|
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
|
|
|
let identity = state
|
|
.db
|
|
.get_identity_by_uuid(&uuid)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
match identity {
|
|
Some(i) => Ok(Json(IdentityDetailResponse {
|
|
success: true,
|
|
uuid: i.uuid,
|
|
name: i.name,
|
|
identity_type: i.identity_type,
|
|
source: i.source,
|
|
status: i.status,
|
|
metadata: i.metadata,
|
|
reference_data: i.reference_data,
|
|
tmdb_id: i.tmdb_id,
|
|
tmdb_profile: i.tmdb_profile,
|
|
created_at: i.created_at,
|
|
updated_at: i.updated_at,
|
|
})),
|
|
None => Err((
|
|
StatusCode::NOT_FOUND,
|
|
format!("Identity not found: {}", uuid),
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityFilesResponse {
|
|
pub success: bool,
|
|
pub identity_uuid: Uuid,
|
|
pub total: i64,
|
|
pub page: usize,
|
|
pub page_size: usize,
|
|
pub data: Vec<IdentityFileItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityFileItem {
|
|
pub file_uuid: String,
|
|
pub file_name: String,
|
|
pub file_path: String,
|
|
pub status: String,
|
|
pub face_count: Option<i32>,
|
|
pub speaker_count: Option<i32>,
|
|
pub first_appearance: Option<f64>,
|
|
pub last_appearance: Option<f64>,
|
|
pub confidence: Option<f64>,
|
|
}
|
|
|
|
async fn get_identity_files(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid_str): Path<String>,
|
|
Query(params): Query<FilesQuery>,
|
|
) -> Result<Json<IdentityFilesResponse>, (StatusCode, String)> {
|
|
let uuid = Uuid::parse_str(&uuid_str)
|
|
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
|
|
|
let page = params.page.unwrap_or(1);
|
|
let page_size = params.page_size.unwrap_or(20);
|
|
let offset = ((page - 1) as i64) * (page_size as i64);
|
|
|
|
let records = state
|
|
.db
|
|
.get_identity_files(&uuid, page_size as i32, offset)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data: Vec<IdentityFileItem> = records
|
|
.into_iter()
|
|
.map(|r| IdentityFileItem {
|
|
file_uuid: r.file_uuid,
|
|
file_name: r.file_name,
|
|
file_path: r.file_path,
|
|
status: r.status,
|
|
face_count: r.face_count,
|
|
speaker_count: r.speaker_count,
|
|
first_appearance: r.first_appearance,
|
|
last_appearance: r.last_appearance,
|
|
confidence: r.confidence,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(IdentityFilesResponse {
|
|
success: true,
|
|
identity_uuid: uuid,
|
|
total: data.len() as i64,
|
|
page,
|
|
page_size,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityFacesResponse {
|
|
pub success: bool,
|
|
pub identity_uuid: Uuid,
|
|
pub total: i64,
|
|
pub page: usize,
|
|
pub page_size: usize,
|
|
pub data: Vec<IdentityFaceItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityFaceItem {
|
|
pub id: i64,
|
|
pub file_uuid: String,
|
|
pub frame_number: i64,
|
|
pub timestamp_secs: f64,
|
|
pub face_id: Option<String>,
|
|
pub bbox: BBox,
|
|
pub confidence: f64,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct BBox {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
pub width: f64,
|
|
pub height: f64,
|
|
}
|
|
|
|
async fn get_identity_faces(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid_str): Path<String>,
|
|
Query(params): Query<FilesQuery>,
|
|
) -> Result<Json<IdentityFacesResponse>, (StatusCode, String)> {
|
|
let uuid = Uuid::parse_str(&uuid_str)
|
|
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
|
|
|
let page = params.page.unwrap_or(1);
|
|
let page_size = params.page_size.unwrap_or(50);
|
|
let offset = ((page - 1) as i64) * (page_size as i64);
|
|
|
|
let records = state
|
|
.db
|
|
.get_identity_faces(&uuid, page_size as i32, offset)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data: Vec<IdentityFaceItem> = records
|
|
.into_iter()
|
|
.map(|r| IdentityFaceItem {
|
|
id: r.id,
|
|
file_uuid: r.file_uuid,
|
|
frame_number: r.frame_number,
|
|
timestamp_secs: r.timestamp_secs,
|
|
face_id: r.face_id,
|
|
bbox: BBox {
|
|
x: r.x,
|
|
y: r.y,
|
|
width: r.width,
|
|
height: r.height,
|
|
},
|
|
confidence: r.confidence,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(IdentityFacesResponse {
|
|
success: true,
|
|
identity_uuid: uuid,
|
|
total: data.len() as i64,
|
|
page,
|
|
page_size,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityChunksResponse {
|
|
pub success: bool,
|
|
pub identity_uuid: Uuid,
|
|
pub total: i64,
|
|
pub page: usize,
|
|
pub page_size: usize,
|
|
pub data: Vec<IdentityChunkItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct IdentityChunkItem {
|
|
pub id: i64,
|
|
pub file_uuid: String,
|
|
pub chunk_id: String,
|
|
pub chunk_type: String,
|
|
pub start_time: Option<f64>,
|
|
pub end_time: Option<f64>,
|
|
pub text_content: Option<String>,
|
|
}
|
|
|
|
async fn get_identity_chunks(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Path(uuid_str): Path<String>,
|
|
Query(params): Query<FilesQuery>,
|
|
) -> Result<Json<IdentityChunksResponse>, (StatusCode, String)> {
|
|
let uuid = Uuid::parse_str(&uuid_str)
|
|
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
|
|
|
let page = params.page.unwrap_or(1);
|
|
let page_size = params.page_size.unwrap_or(20);
|
|
let offset = ((page - 1) as i64) * (page_size as i64);
|
|
|
|
let records = state
|
|
.db
|
|
.get_identity_chunks(&uuid, page_size as i32, offset)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data: Vec<IdentityChunkItem> = records
|
|
.into_iter()
|
|
.map(|r| IdentityChunkItem {
|
|
id: r.id,
|
|
file_uuid: r.file_uuid,
|
|
chunk_id: r.chunk_id,
|
|
chunk_type: r.chunk_type,
|
|
start_time: r.start_time,
|
|
end_time: r.end_time,
|
|
text_content: r.text_content,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(IdentityChunksResponse {
|
|
success: true,
|
|
identity_uuid: uuid,
|
|
total: data.len() as i64,
|
|
page,
|
|
page_size,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
// --- Resource Registry Endpoints (Phase 5) ---
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RegisterResourceRequest {
|
|
pub resource_id: String,
|
|
pub resource_type: String,
|
|
pub category: String,
|
|
pub capabilities: Option<serde_json::Value>,
|
|
pub config: Option<serde_json::Value>,
|
|
pub metadata: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ResourceResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub data: Option<ResourceItem>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ResourceItem {
|
|
pub resource_id: String,
|
|
pub resource_type: String,
|
|
pub category: String,
|
|
pub capabilities: Option<serde_json::Value>,
|
|
pub status: String,
|
|
pub last_heartbeat: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
async fn register_resource(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Json(req): Json<RegisterResourceRequest>,
|
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
|
let resource = ResourceRecord {
|
|
resource_id: req.resource_id.clone(),
|
|
resource_type: req.resource_type.clone(),
|
|
category: req.category.clone(),
|
|
capabilities: req.capabilities,
|
|
config: req.config,
|
|
metadata: req.metadata,
|
|
status: "online".to_string(),
|
|
last_heartbeat: None,
|
|
created_at: None,
|
|
};
|
|
|
|
state
|
|
.db
|
|
.register_resource(resource)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
Ok(Json(ResourceResponse {
|
|
success: true,
|
|
message: "Resource registered successfully".to_string(),
|
|
data: None, // We could return the full record, but simplified for now
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct HeartbeatRequest {
|
|
pub resource_id: String,
|
|
pub status: Option<String>,
|
|
}
|
|
|
|
async fn heartbeat_resource(
|
|
State(state): State<crate::api::server::AppState>,
|
|
Json(req): Json<HeartbeatRequest>,
|
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
|
let status = req.status.unwrap_or("online".to_string());
|
|
state
|
|
.db
|
|
.heartbeat_resource(&req.resource_id, &status)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
Ok(Json(ResourceResponse {
|
|
success: true,
|
|
message: "Heartbeat received".to_string(),
|
|
data: None,
|
|
}))
|
|
}
|
|
|
|
async fn list_resources(
|
|
State(state): State<crate::api::server::AppState>,
|
|
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
|
let records = state
|
|
.db
|
|
.list_resources()
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
|
|
let data: Vec<ResourceItem> = records
|
|
.into_iter()
|
|
.map(|r| ResourceItem {
|
|
resource_id: r.resource_id,
|
|
resource_type: r.resource_type,
|
|
category: r.category,
|
|
capabilities: r.capabilities,
|
|
status: r.status,
|
|
last_heartbeat: r.last_heartbeat,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(ResourceResponse {
|
|
success: true,
|
|
message: "Resources listed".to_string(),
|
|
data: None,
|
|
}))
|
|
}
|