From 36612444c554222caeb64edead45491a48ec8e97 Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 26 Feb 2026 10:42:20 -0500 Subject: [PATCH] Ensure we re-queue pending/failed records --- src/main.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index c9e7d8b..0f91af9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -750,6 +750,20 @@ async fn get_preview_status( for path in &body.paths { if let Some(clip) = clip_map.get(path) { + // Re-queue generation for stale pending/failed records + if clip.status == "pending" || clip.status == "failed" { + let full_path = format!( + "{}/{}", + app_state.base_path.trim_end_matches(['/', '\\']), + path.trim_start_matches(['/', '\\']) + ); + app_state + .preview_clip_generator + .do_send(GeneratePreviewClipMessage { + video_path: full_path, + }); + } + items.push(PreviewStatusItem { path: path.clone(), status: clip.status.clone(), @@ -1653,9 +1667,10 @@ fn process_new_files( for (full_path, relative_path) in &video_files { let status = existing_previews.get(relative_path).map(|s| s.as_str()); let needs_preview = match status { - None => true, // No record at all - Some("failed") => true, // Retry failed - _ => false, // pending, processing, or complete + None => true, // No record at all + Some("failed") => true, // Retry failed + Some("pending") => true, // Stale pending from previous run + _ => false, // processing or complete }; if needs_preview { @@ -1851,4 +1866,66 @@ mod tests { assert_eq!(previews[2]["path"], "c.mp4"); assert_eq!(previews[2]["status"], "pending"); } + + /// Verifies that the status endpoint re-queues generation for stale + /// "pending" and "failed" records (e.g., after a server restart or + /// when clip files were deleted). The do_send to the actor exercises + /// the re-queue code path; the actor runs against temp dirs so it + /// won't panic. + #[actix_rt::test] + async fn test_get_preview_status_requeues_pending_and_failed() { + let mut dao = TestPreviewDao::new(); + let ctx = opentelemetry::Context::new(); + + // Simulate stale records left from a previous server run + dao.insert_preview(&ctx, "stale/pending.mp4", "pending") + .unwrap(); + dao.insert_preview(&ctx, "stale/failed.mp4", "pending") + .unwrap(); + dao.update_status( + &ctx, + "stale/failed.mp4", + "failed", + None, + None, + Some("ffmpeg error"), + ) + .unwrap(); + + let preview_dao = make_preview_dao(dao); + let app_state = Data::new(AppState::test_state()); + let token = make_token(); + + let app = actix_web::test::init_service( + App::new() + .service(get_preview_status) + .app_data(app_state) + .app_data(preview_dao), + ) + .await; + + let req = actix_web::test::TestRequest::post() + .uri("/video/preview/status") + .insert_header(("Authorization", format!("Bearer {}", token))) + .set_json(serde_json::json!({ + "paths": ["stale/pending.mp4", "stale/failed.mp4"] + })) + .to_request(); + + let resp = actix_web::test::call_service(&app, req).await; + assert_eq!(resp.status(), 200); + + let body: serde_json::Value = actix_web::test::read_body_json(resp).await; + let previews = body["previews"].as_array().unwrap(); + assert_eq!(previews.len(), 2); + + // Both records are returned with their current status + assert_eq!(previews[0]["path"], "stale/pending.mp4"); + assert_eq!(previews[0]["status"], "pending"); + assert!(previews[0].get("preview_url").is_none()); + + assert_eq!(previews[1]["path"], "stale/failed.mp4"); + assert_eq!(previews[1]["status"], "failed"); + assert!(previews[1].get("preview_url").is_none()); + } }