diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index cd8671b..371240e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -27,6 +27,7 @@ struct FileInfo { struct PersonInfo { identity_uuid: String, name: String, + starred: bool, } #[derive(Serialize)] @@ -34,6 +35,8 @@ struct FaceInfo { id: i64, file_uuid: String, frame_number: i64, + timestamp_secs: f64, + face_id: Option, confidence: f64, } @@ -47,6 +50,27 @@ struct TraceInfo { frame_count: i64, } +#[derive(Serialize)] +struct FaceCandidate { + id: i64, + face_id: Option, + file_uuid: String, + frame_number: i64, + confidence: f64, +} + +#[derive(Serialize)] +struct SearchIdentityResult { + identity_id: i64, + name: String, + source: String, + tmdb_id: Option, + file_uuid: Option, + start_time: f64, + end_time: f64, + text_content: Option, +} + const CORE_API: &str = "http://localhost:3002"; const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"; @@ -133,9 +157,6 @@ async fn get_people(page: usize, per_page: usize) -> Result, Str let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?; eprintln!("[get_people] identities count: {}", json["identities"].as_array().map(|a| a.len()).unwrap_or(0)); - let response = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; - let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?; - let people: Vec = json["identities"] .as_array() .unwrap_or(&vec![]) @@ -144,6 +165,7 @@ async fn get_people(page: usize, per_page: usize) -> Result, Str Some(PersonInfo { identity_uuid: i["identity_uuid"].as_str()?.to_string(), name: i["name"].as_str()?.to_string(), + starred: i["metadata"]["starred"].as_bool().unwrap_or(false), }) }) .collect(); @@ -160,7 +182,7 @@ async fn get_faces(uuid: String, per_page: usize) -> Result, Strin let response = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?; - let faces: Vec = json["faces"] + let faces: Vec = json["data"] .as_array() .unwrap_or(&vec![]) .iter() @@ -169,6 +191,8 @@ async fn get_faces(uuid: String, per_page: usize) -> Result, Strin id: f["id"].as_i64().unwrap_or(0), file_uuid: f["file_uuid"].as_str()?.to_string(), frame_number: f["frame_number"].as_i64().unwrap_or(0), + timestamp_secs: f["timestamp_secs"].as_f64().unwrap_or(0.0), + face_id: f["face_id"].as_str().map(|s| s.to_string()), confidence: f["confidence"].as_f64().unwrap_or(0.0), }) }) @@ -221,7 +245,13 @@ async fn get_video_stream(uuid: String, start_time: f64, end_time: f64) -> Resul .map_err(|e| format!("Video request failed: {}", e))? .bytes().await .map_err(|e| format!("Video read failed: {}", e))?; - Ok(format!("data:video/mp4;base64,{}", STANDARD.encode(&bytes))) + + let tmp_dir = std::env::temp_dir(); + let file_path = tmp_dir.join(format!("momentry_{}.mp4", uuid)); + std::fs::write(&file_path, &bytes) + .map_err(|e| format!("Write temp file failed: {}", e))?; + + Ok(format!("file://{}", file_path.display())) } const PROFILE_DIRS: &[&str] = &[ @@ -274,6 +304,98 @@ async fn delete_identity(uuid: String) -> Result<(), String> { Ok(()) } +#[tauri::command] +async fn search_identities(query: String, limit: usize) -> Result, String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identities/search?api_key={}&q={}", CORE_API, API_KEY, query); + let resp = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; + let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?; + let results: Vec = json["results"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .take(limit) + .filter_map(|r| { + Some(SearchIdentityResult { + identity_id: r["identity_id"].as_i64().unwrap_or(0), + name: r["name"].as_str()?.to_string(), + source: r["source"].as_str().unwrap_or("").to_string(), + tmdb_id: r["tmdb_id"].as_i64(), + file_uuid: r["file_uuid"].as_str().map(|s| s.to_string()), + start_time: r["start_time"].as_f64().unwrap_or(0.0), + end_time: r["end_time"].as_f64().unwrap_or(0.0), + text_content: r["text_content"].as_str().map(|s| s.to_string()), + }) + }) + .collect(); + Ok(results) +} + +#[tauri::command] +async fn get_face_candidates(page: usize, per_page: usize) -> Result, String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/faces/candidates?api_key={}&page={}&page_size={}", CORE_API, API_KEY, page, per_page); + let resp = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; + let json: serde_json::Value = resp.json().await.map_err(|e| format!("Parse failed: {}", e))?; + let candidates: Vec = json["candidates"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|c| { + Some(FaceCandidate { + id: c["id"].as_i64().unwrap_or(0), + face_id: c["face_id"].as_str().map(|s| s.to_string()), + file_uuid: c["file_uuid"].as_str()?.to_string(), + frame_number: c["frame_number"].as_i64().unwrap_or(0), + confidence: c["confidence"].as_f64().unwrap_or(0.0), + }) + }) + .collect(); + Ok(candidates) +} + +#[tauri::command] +async fn merge_identities(uuid: String, into_uuid: String) -> Result<(), String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identity/{}/mergeinto?api_key={}", CORE_API, uuid, API_KEY); + let resp = client.post(&url) + .json(&serde_json::json!({"into_uuid": into_uuid})) + .send().await + .map_err(|e| format!("Request failed: {}", e))?; + if !resp.status().is_success() { + return Err(format!("Merge failed: {}", resp.status())); + } + Ok(()) +} + +#[tauri::command] +async fn bind_face(uuid: String, face_id: String, file_uuid: String) -> Result<(), String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identity/{}/bind?api_key={}", CORE_API, uuid, API_KEY); + let resp = client.post(&url) + .json(&serde_json::json!({"face_id": face_id, "file_uuid": file_uuid})) + .send().await + .map_err(|e| format!("Request failed: {}", e))?; + if !resp.status().is_success() { + return Err(format!("Bind failed: {}", resp.status())); + } + Ok(()) +} + +#[tauri::command] +async fn unbind_face(uuid: String, face_id: String, file_uuid: String) -> Result<(), String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identity/{}/unbind?api_key={}", CORE_API, uuid, API_KEY); + let resp = client.post(&url) + .json(&serde_json::json!({"face_id": face_id, "file_uuid": file_uuid})) + .send().await + .map_err(|e| format!("Request failed: {}", e))?; + if !resp.status().is_success() { + return Err(format!("Unbind failed: {}", resp.status())); + } + Ok(()) +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) @@ -287,7 +409,12 @@ fn main() { get_video_stream, get_identity_profile, update_identity_name, - delete_identity + delete_identity, + search_identities, + get_face_candidates, + merge_identities, + bind_face, + unbind_face ]) .run(tauri::generate_context!()) .expect("error while running tauri application");