immich-automated-selfie-tim.../src/web/handlers/processing.rs
2026-02-05 21:55:10 +01:00

149 lines
4.4 KiB
Rust

//! Processing job control endpoints (progress, start, cancel).
use crate::job::{run_job, JobParams};
use crate::web::state::{AppState, JobStatus, Progress, SkipStats};
use axum::{extract::State, http::StatusCode, response::Json};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::StartResponse;
/// Skip statistics for API response.
///
/// Uses a dynamic HashMap to support arbitrary skip reasons from pipeline steps.
#[derive(Serialize)]
pub struct SkipStatsResponse {
/// Map of skip reason ID to count.
#[serde(flatten)]
pub counts: HashMap<String, u32>,
/// Total number of skipped images.
pub total: u32,
}
impl From<&SkipStats> for SkipStatsResponse {
fn from(stats: &SkipStats) -> Self {
Self {
counts: stats.counts().clone(),
total: stats.total(),
}
}
}
/// Progress response.
#[derive(Serialize)]
pub struct ProgressResponse {
pub status: String,
pub completed: u32,
pub total: u32,
pub message: Option<String>,
pub skip_stats: SkipStatsResponse,
pub person_id: Option<String>,
pub person_name: Option<String>,
}
/// Get current progress.
pub async fn get_progress(State(state): State<AppState>) -> Json<ProgressResponse> {
let progress = state.progress.read().await;
let status_str = match &progress.status {
JobStatus::Idle => "idle",
JobStatus::Running => "running",
JobStatus::Cancelling => "cancelling",
JobStatus::CompilingVideo => "compiling_video",
JobStatus::Completed => "completed",
JobStatus::Cancelled => "cancelled",
JobStatus::Error(_) => "error",
};
Json(ProgressResponse {
status: status_str.to_string(),
completed: progress.completed,
total: progress.total,
message: progress.message.clone(),
skip_stats: SkipStatsResponse::from(&progress.skip_stats),
person_id: progress.person_id.clone(),
person_name: progress.person_name.clone(),
})
}
/// Start processing request.
#[derive(Deserialize)]
pub struct StartRequest {
pub person_id: String,
pub person_name: Option<String>,
pub date_from: Option<String>,
pub date_to: Option<String>,
}
/// Start processing for a person.
pub async fn start_processing(
State(state): State<AppState>,
Json(request): Json<StartRequest>,
) -> Result<Json<StartResponse>, (StatusCode, String)> {
// Check if already running
{
let progress = state.progress.read().await;
if progress.status == JobStatus::Running || progress.status == JobStatus::CompilingVideo {
return Err((StatusCode::CONFLICT, "A job is already running".to_string()));
}
}
// Reset progress with person info (bypasses terminal state check)
state
.reset_progress(Progress {
status: JobStatus::Running,
completed: 0,
total: 0,
message: Some("Starting...".to_string()),
skip_stats: SkipStats::default(),
person_id: Some(request.person_id.clone()),
person_name: request.person_name.clone(),
})
.await;
// Create cancellation token
let cancel_token = state.create_cancel_token().await;
tracing::info!(
"Starting processing for person {} (date range: {:?} - {:?})",
request.person_id,
request.date_from,
request.date_to
);
// Spawn the processing job in the background
let job_params = JobParams {
person_id: request.person_id,
person_name: request.person_name,
date_from: request.date_from,
date_to: request.date_to,
};
let job_state = state.clone();
tokio::spawn(async move {
run_job(job_state, job_params, cancel_token).await;
});
Ok(Json(StartResponse {
success: true,
message: "Processing started".to_string(),
}))
}
/// Cancel the current processing job.
pub async fn cancel_processing(State(state): State<AppState>) -> Json<StartResponse> {
let cancelled = state.request_cancel().await;
if cancelled {
// The job will update its own status when it detects cancellation
Json(StartResponse {
success: true,
message: "Cancellation requested".to_string(),
})
} else {
Json(StartResponse {
success: false,
message: "No job running to cancel".to_string(),
})
}
}