feat: frame-based positioning and mark system foundation
Core Changes: - Fix SearchView to use start_frame/end_frame directly (no time*fps conversion) - Add hard_delete support to delete_trace API - VideoPlayer: Main timeline + Mark system foundation - Proxy: Add local routes for auth, media, identity-matches, cluster-results - Add .gitignore to exclude build artifacts and dependencies Design Documents: - Multi-track Mark system design (.opencode/plans/) - Video editing positioning standards research Files Modified: - src/views/SearchView.vue: Frame positioning, ensureMinDuration (240 frames) - src/views/PeopleView.vue: batchDeleteGroups with hard_delete - src/api/index.ts: delete_trace with hard_delete body - src/components/VideoPlayer.vue: Timeline + Mark UI - src-tauri/src/proxy.rs: New local routes - AGENTS.md: Update documentation
This commit is contained in:
@@ -9,9 +9,12 @@ use tower_http::cors::{Any, CorsLayer};
|
||||
use crate::db;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use image::GenericImageView;
|
||||
use std::sync::Mutex;
|
||||
use image::DynamicImage;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::num::NonZeroUsize;
|
||||
use lru::LruCache;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
const CORE_API: &str = "http://localhost:3002";
|
||||
const API_KEY: &str = "muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69";
|
||||
@@ -25,6 +28,16 @@ const PROFILE_DIRS: [&str; 4] = [
|
||||
|
||||
static FACE_THUMB_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
||||
static PROFILE_PROXY_CACHE: Mutex<Option<LruCache<String, String>>> = Mutex::new(None);
|
||||
static RAW_DIM_PROXY_CACHE: Mutex<Option<HashMap<String, (u32, u32)>>> = Mutex::new(None);
|
||||
static THUMB_SEM_MAP_PROXY: Mutex<Option<HashMap<String, Arc<Semaphore>>>> = Mutex::new(None);
|
||||
|
||||
fn acquire_thumb_semaphore(uuid: &str) -> Arc<Semaphore> {
|
||||
let mut map = THUMB_SEM_MAP_PROXY.lock().unwrap();
|
||||
let map = map.get_or_insert_with(HashMap::new);
|
||||
map.entry(uuid.to_string())
|
||||
.or_insert_with(|| Arc::new(Semaphore::new(1)))
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn get_face_thumb_cache() -> std::sync::MutexGuard<'static, Option<LruCache<String, String>>> {
|
||||
let mut guard = FACE_THUMB_CACHE.lock().unwrap();
|
||||
@@ -67,7 +80,15 @@ pub async fn start_proxy_server() {
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", axum::routing::get(health_handler))
|
||||
.route("/api/v1/auth/login", axum::routing::post(login_handler))
|
||||
.route("/api/v1/face-thumbnail", axum::routing::get(get_face_thumbnail_handler))
|
||||
.route("/api/v1/media/frame", axum::routing::get(get_frame_handler))
|
||||
.route("/api/v1/identity-matches", axum::routing::get(get_identity_matches_handler))
|
||||
.route("/api/v1/cluster-results", axum::routing::get(get_cluster_results_handler))
|
||||
.route("/api/v1/processor-json", axum::routing::get(get_processor_json_handler))
|
||||
.route("/api/v1/file/thumbnail", axum::routing::get(get_file_thumbnail_by_path_handler))
|
||||
.route("/api/v1/file/:file_uuid/pose", axum::routing::get(get_pose_handler))
|
||||
.route("/api/v1/file/:file_uuid/appearance", axum::routing::get(get_appearance_handler))
|
||||
.fallback(fallback_handler)
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
@@ -91,6 +112,81 @@ async fn health_handler() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct LoginResponse {
|
||||
success: bool,
|
||||
user: Option<UserInfo>,
|
||||
token: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct UserInfo {
|
||||
id: String,
|
||||
username: String,
|
||||
display_name: Option<String>,
|
||||
role: String,
|
||||
}
|
||||
|
||||
async fn login_handler(body: axum::body::Bytes) -> Response {
|
||||
let req: Result<LoginRequest, _> = serde_json::from_slice(&body);
|
||||
|
||||
match req {
|
||||
Ok(login_req) => {
|
||||
match db::verify_user(&login_req.username, &login_req.password) {
|
||||
Ok(Some(user)) => {
|
||||
let token = uuid::Uuid::new_v4().to_string();
|
||||
let response = LoginResponse {
|
||||
success: true,
|
||||
user: Some(UserInfo {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
role: user.role,
|
||||
}),
|
||||
token: Some(token),
|
||||
error: None,
|
||||
};
|
||||
(StatusCode::OK, axum::Json(response)).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
let response = LoginResponse {
|
||||
success: false,
|
||||
user: None,
|
||||
token: None,
|
||||
error: Some("Invalid username or password".to_string()),
|
||||
};
|
||||
(StatusCode::UNAUTHORIZED, axum::Json(response)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let response = LoginResponse {
|
||||
success: false,
|
||||
user: None,
|
||||
token: None,
|
||||
error: Some(e),
|
||||
};
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, axum::Json(response)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let response = LoginResponse {
|
||||
success: false,
|
||||
user: None,
|
||||
token: None,
|
||||
error: Some(format!("Invalid request: {}", e)),
|
||||
};
|
||||
(StatusCode::BAD_REQUEST, axum::Json(response)).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fallback_handler(State(state): State<ProxyState>, req: axum::extract::Request) -> Response {
|
||||
let path = req.uri().path().to_string();
|
||||
let method = req.method().clone();
|
||||
@@ -483,6 +579,35 @@ async fn get_identity_profile_handler_inner(uuid: String) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read EXIF orientation and physically rotate pixels, stripping orientation tag.
|
||||
/// Fetch raw video dimensions from Core API with in-memory cache.
|
||||
async fn fetch_raw_dimensions(uuid: &str) -> Option<(u32, u32)> {
|
||||
{
|
||||
let cache = RAW_DIM_PROXY_CACHE.lock().unwrap();
|
||||
if let Some(cache) = cache.as_ref() {
|
||||
if let Some(dims) = cache.get(uuid) {
|
||||
return Some(*dims);
|
||||
}
|
||||
}
|
||||
}
|
||||
let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, uuid, API_KEY);
|
||||
let result = reqwest::get(&url).await.ok()?
|
||||
.json::<serde_json::Value>().await.ok()?;
|
||||
let w = result["width"].as_u64().unwrap_or(0) as u32;
|
||||
let h = result["height"].as_u64().unwrap_or(0) as u32;
|
||||
if w == 0 || h == 0 {
|
||||
return None;
|
||||
}
|
||||
{
|
||||
let mut cache = RAW_DIM_PROXY_CACHE.lock().unwrap();
|
||||
let cache = cache.get_or_insert_with(HashMap::new);
|
||||
cache.insert(uuid.to_string(), (w, h));
|
||||
}
|
||||
Some((w, h))
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn get_face_thumbnail_handler(State(state): State<ProxyState>, axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
|
||||
let start = std::time::Instant::now();
|
||||
let uuid = match params.get("uuid") {
|
||||
@@ -511,6 +636,17 @@ async fn get_face_thumbnail_handler(State(state): State<ProxyState>, axum::extra
|
||||
}
|
||||
}
|
||||
|
||||
let _permit = acquire_thumb_semaphore(&uuid).acquire_owned().await.unwrap();
|
||||
// Double-check cache after acquiring semaphore
|
||||
{
|
||||
let mut cache = get_face_thumb_cache();
|
||||
if let Some(c) = cache.as_mut() {
|
||||
if let Some(val) = c.get(&cache_key) {
|
||||
eprintln!("[proxy] <-- FACE-THUMB {} (cached after wait) {}ms", uuid, start.elapsed().as_millis());
|
||||
return ([("content-type", "text/plain")], val.clone()).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame);
|
||||
|
||||
let bytes = match state.client.get(&url).send().await {
|
||||
@@ -555,4 +691,328 @@ async fn get_face_thumbnail_handler(State(state): State<ProxyState>, axum::extra
|
||||
|
||||
eprintln!("[proxy] <-- FACE-THUMB {} {} {}ms", uuid, result.len(), start.elapsed().as_millis());
|
||||
([("content-type", "text/plain")], result).into_response()
|
||||
}
|
||||
|
||||
async fn get_identity_matches_handler(axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
|
||||
let file_hash = match params.get("file_hash") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(),
|
||||
};
|
||||
let output_bases = [
|
||||
"/Users/accusys/momentry/output",
|
||||
"/Users/accusys/momentry/output_dev",
|
||||
"/Volumes/external/momentry/output",
|
||||
"/Volumes/external/momentry/output_dev",
|
||||
];
|
||||
let mut match_path = None;
|
||||
for base in &output_bases {
|
||||
let path = format!("{}/{}/{}.identity_match_round1.json", base, file_hash, file_hash);
|
||||
if std::path::Path::new(&path).exists() {
|
||||
match_path = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match match_path {
|
||||
Some(path) => match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let parsed: serde_json::Value = match serde_json::from_str(&content) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Parse failed: {}", e)).into_response();
|
||||
}
|
||||
};
|
||||
axum::Json(parsed).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(),
|
||||
},
|
||||
None => (StatusCode::NOT_FOUND, "Identity match file not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cluster_results_handler(axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
|
||||
let file_hash = match params.get("file_hash") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{}/api/v1/file/{}/face-groups", CORE_API, file_hash);
|
||||
let resp = match client
|
||||
.get(&url)
|
||||
.header("X-API-Key", API_KEY)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Face groups request failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let status = resp.status();
|
||||
let body = match resp.text().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let axum_status = axum::http::StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
if !axum_status.is_success() {
|
||||
return (axum_status, body).into_response();
|
||||
}
|
||||
|
||||
let result: serde_json::Value = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON parse failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let clusters = result.get("face_groups").cloned().unwrap_or(serde_json::json!([]));
|
||||
axum::Json(serde_json::json!({
|
||||
"success": true,
|
||||
"clusters": clusters
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
async fn get_processor_json_handler(
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> Response {
|
||||
let file_hash = match params.get("file_hash") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing file_hash").into_response(),
|
||||
};
|
||||
let processor = match params.get("processor") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing processor").into_response(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let json_url = format!("{}/api/v1/file/{}/json/{}", CORE_API, file_hash, processor);
|
||||
let json_resp = match client
|
||||
.post(&json_url)
|
||||
.header("X-API-Key", API_KEY)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON request failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let status = json_resp.status();
|
||||
let body = match json_resp.text().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let axum_status = axum::http::StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
if axum_status.is_success() {
|
||||
match serde_json::from_str::<serde_json::Value>(&body) {
|
||||
Ok(v) => axum::Json(v).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON parse failed: {}", e)).into_response(),
|
||||
}
|
||||
} else {
|
||||
(axum_status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_file_thumbnail_by_path_handler(axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
|
||||
let path = match params.get("path") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing path").into_response(),
|
||||
};
|
||||
let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(30);
|
||||
|
||||
if !std::path::Path::new(&path).exists() {
|
||||
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
||||
}
|
||||
|
||||
let ext = path.rsplit('.').next().unwrap_or("").to_lowercase();
|
||||
let is_image = matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "gif" | "webp");
|
||||
|
||||
if is_image {
|
||||
// For images, read directly
|
||||
match std::fs::read(&path) {
|
||||
Ok(bytes) => {
|
||||
let mime = match ext.as_str() {
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
_ => "image/jpeg",
|
||||
};
|
||||
([( "content-type", mime )], axum::body::Body::from(bytes)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(),
|
||||
}
|
||||
} else {
|
||||
// For videos, extract frame using ffmpeg
|
||||
let time_sec = frame as f64 / 24.0;
|
||||
let output = tokio::process::Command::new("ffmpeg")
|
||||
.args([
|
||||
"-ss", &time_sec.to_string(),
|
||||
"-i", &path,
|
||||
"-vframes", "1",
|
||||
"-q:v", "2",
|
||||
"-f", "mjpeg",
|
||||
"-"
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
let jpeg = out.stdout;
|
||||
([("content-type", "image/jpeg")], axum::body::Body::from(jpeg)).into_response()
|
||||
}
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, "Thumbnail extraction failed").into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_frame_handler(State(state): State<ProxyState>, axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>) -> Response {
|
||||
let file_uuid = match params.get("file_uuid") {
|
||||
Some(v) => v.clone(),
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing file_uuid").into_response(),
|
||||
};
|
||||
let frame: u32 = params.get("frame").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
|
||||
// Get file path from Core API
|
||||
let url = format!("{}/api/v1/file/{}?api_key={}", CORE_API, file_uuid, API_KEY);
|
||||
let file_info = match state.client.get(&url).send().await {
|
||||
Ok(resp) => match resp.json::<serde_json::Value>().await {
|
||||
Ok(json) => json,
|
||||
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Parse failed: {}", e)).into_response(),
|
||||
},
|
||||
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let file_path = match file_info["file_path"].as_str() {
|
||||
Some(p) => p.to_string(),
|
||||
None => return (StatusCode::NOT_FOUND, "File path not found").into_response(),
|
||||
};
|
||||
|
||||
if !std::path::Path::new(&file_path).exists() {
|
||||
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
||||
}
|
||||
|
||||
let ext = file_path.rsplit('.').next().unwrap_or("").to_lowercase();
|
||||
let is_image = matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "gif" | "webp");
|
||||
|
||||
if is_image {
|
||||
match std::fs::read(&file_path) {
|
||||
Ok(bytes) => {
|
||||
let mime = match ext.as_str() {
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
_ => "image/jpeg",
|
||||
};
|
||||
([("content-type", mime)], axum::body::Body::from(bytes)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)).into_response(),
|
||||
}
|
||||
} else {
|
||||
let fps = file_info["fps"].as_f64().unwrap_or(24.0);
|
||||
let time_sec = frame as f64 / fps;
|
||||
let output = tokio::process::Command::new("ffmpeg")
|
||||
.args([
|
||||
"-ss", &time_sec.to_string(),
|
||||
"-i", &file_path,
|
||||
"-vframes", "1",
|
||||
"-q:v", "2",
|
||||
"-f", "mjpeg",
|
||||
"-"
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
let jpeg = out.stdout;
|
||||
([("content-type", "image/jpeg")], axum::body::Body::from(jpeg)).into_response()
|
||||
}
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, "Frame extraction failed").into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_pose_handler(
|
||||
axum::extract::Path(file_uuid): axum::extract::Path<String>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> Response {
|
||||
let frame = match params.get("frame").and_then(|v| v.parse::<u32>().ok()) {
|
||||
Some(f) => f,
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing frame").into_response(),
|
||||
};
|
||||
|
||||
let mut url = format!("{}/api/v1/file/{}/pose?frame={}", CORE_API, file_uuid, frame);
|
||||
if let Some(bbox_x) = params.get("bbox_x") {
|
||||
url.push_str(&format!("&bbox_x={}", bbox_x));
|
||||
}
|
||||
if let Some(bbox_y) = params.get("bbox_y") {
|
||||
url.push_str(&format!("&bbox_y={}", bbox_y));
|
||||
}
|
||||
if let Some(bbox_w) = params.get("bbox_w") {
|
||||
url.push_str(&format!("&bbox_w={}", bbox_w));
|
||||
}
|
||||
if let Some(bbox_h) = params.get("bbox_h") {
|
||||
url.push_str(&format!("&bbox_h={}", bbox_h));
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = match client
|
||||
.get(&url)
|
||||
.header("X-API-Key", API_KEY)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let body = match resp.text().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
|
||||
async fn get_appearance_handler(
|
||||
axum::extract::Path(file_uuid): axum::extract::Path<String>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> Response {
|
||||
let frame = match params.get("frame").and_then(|v| v.parse::<u32>().ok()) {
|
||||
Some(f) => f,
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing frame").into_response(),
|
||||
};
|
||||
|
||||
let mut url = format!("{}/api/v1/file/{}/appearance?frame={}", CORE_API, file_uuid, frame);
|
||||
if let Some(bbox_x) = params.get("bbox_x") {
|
||||
url.push_str(&format!("&bbox_x={}", bbox_x));
|
||||
}
|
||||
if let Some(bbox_y) = params.get("bbox_y") {
|
||||
url.push_str(&format!("&bbox_y={}", bbox_y));
|
||||
}
|
||||
if let Some(bbox_w) = params.get("bbox_w") {
|
||||
url.push_str(&format!("&bbox_w={}", bbox_w));
|
||||
}
|
||||
if let Some(bbox_h) = params.get("bbox_h") {
|
||||
url.push_str(&format!("&bbox_h={}", bbox_h));
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = match client
|
||||
.get(&url)
|
||||
.header("X-API-Key", API_KEY)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Request failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let body = match resp.text().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Body read failed: {}", e)).into_response(),
|
||||
};
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
Reference in New Issue
Block a user