video_name missing in export

This commit is contained in:
2026-07-06 19:47:31 +05:30
parent 4d71e08aa4
commit 05ce1f2cdb

View File

@@ -566,6 +566,31 @@ async fn list_videos(State(s): State<AppState>, user: AuthUser, AxPath(id): AxPa
Ok(Json(rows)) Ok(Json(rows))
} }
/// Ensure an annotations export doc carries a `video` identity block. Some ingested /
/// pushed docs omit it, which makes a bulk export impossible to tie back to a file.
/// Injects `{ file_name, rel_path }` when absent; leaves an existing block untouched.
fn ensure_video_block(mut doc: serde_json::Value, file_name: &str, rel_path: &str) -> serde_json::Value {
if let Some(obj) = doc.as_object_mut() {
let has_video = obj.get("video").map(|v| !v.is_null()).unwrap_or(false);
if !has_video {
obj.insert("video".into(), json!({ "file_name": file_name, "rel_path": rel_path }));
}
}
doc
}
/// One entry in the bulk project export. Serialized as a struct (NOT the `json!` macro)
/// so the video identity fields come FIRST — serde_json orders `json!` object keys
/// alphabetically, which otherwise buries `file_name` far below the huge `annotations`
/// array and makes it look missing.
#[derive(serde::Serialize)]
struct ExportedVideo {
file_name: String,
rel_path: String,
video_id: i32,
annotations: serde_json::Value,
}
/// `GET /api/videos/:id/export` — download one video's annotations as the canonical /// `GET /api/videos/:id/export` — download one video's annotations as the canonical
/// export JSON (`videos.raw_json`, the doc pushed by the worker / ingested from NAS). /// export JSON (`videos.raw_json`, the doc pushed by the worker / ingested from NAS).
/// Auth required (via `?token=`); served as a file attachment. /// Auth required (via `?token=`); served as a file attachment.
@@ -573,14 +598,19 @@ async fn export_video(State(s): State<AppState>, Query(q): Query<TokenQuery>, Ax
if auth::validate_token(&s, q.token.as_deref().unwrap_or("")).await.is_none() { if auth::validate_token(&s, q.token.as_deref().unwrap_or("")).await.is_none() {
return Err((StatusCode::UNAUTHORIZED, "missing or invalid token".into())); return Err((StatusCode::UNAUTHORIZED, "missing or invalid token".into()));
} }
let row: Option<(String, Option<serde_json::Value>)> = let row: Option<(String, String, Option<serde_json::Value>)> =
sqlx::query_as("SELECT file_name, raw_json FROM videos WHERE id=$1") sqlx::query_as("SELECT file_name, rel_path, raw_json FROM videos WHERE id=$1")
.bind(id) .bind(id)
.fetch_optional(&s.db) .fetch_optional(&s.db)
.await .await
.map_err(err)?; .map_err(err)?;
let (file_name, raw_json) = row.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?; let (file_name, rel_path, raw_json) = row.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
let doc = raw_json.unwrap_or_else(|| json!({ "fixed_annotations": [], "range_annotations": [] })); // Embed the video identity inside the doc so the file's name/path travel with it.
let doc = ensure_video_block(
raw_json.unwrap_or_else(|| json!({ "fixed_annotations": [], "range_annotations": [] })),
&file_name,
&rel_path,
);
let body = serde_json::to_vec_pretty(&doc).map_err(err)?; let body = serde_json::to_vec_pretty(&doc).map_err(err)?;
let stem = file_name.rsplit_once('.').map(|(a, _)| a).unwrap_or(&file_name); let stem = file_name.rsplit_once('.').map(|(a, _)| a).unwrap_or(&file_name);
let fname = format!("{stem}_annotations.json"); let fname = format!("{stem}_annotations.json");
@@ -604,9 +634,18 @@ async fn export_project(State(s): State<AppState>, Query(q): Query<TokenQuery>,
.fetch_all(&s.db) .fetch_all(&s.db)
.await .await
.map_err(err)?; .map_err(err)?;
let videos: Vec<serde_json::Value> = rows let videos: Vec<ExportedVideo> = rows
.into_iter() .into_iter()
.map(|(vid, fname, rel, raw)| json!({ "video_id": vid, "file_name": fname, "rel_path": rel, "annotations": raw })) .map(|(vid, fname, rel, raw)| {
// file_name first (struct order) AND embedded in the annotations doc, so the
// name is both immediately visible per entry and self-contained with the data.
let annotations = ensure_video_block(
raw.unwrap_or_else(|| json!({ "fixed_annotations": [], "range_annotations": [] })),
&fname,
&rel,
);
ExportedVideo { file_name: fname, rel_path: rel, video_id: vid, annotations }
})
.collect(); .collect();
let doc = json!({ "project_id": id, "video_count": videos.len(), "videos": videos }); let doc = json!({ "project_id": id, "video_count": videos.len(), "videos": videos });
let body = serde_json::to_vec_pretty(&doc).map_err(err)?; let body = serde_json::to_vec_pretty(&doc).map_err(err)?;