Allow background processing (can close web page and reopen)
This commit is contained in:
parent
d52e334ba7
commit
eadd1b5d7b
6 changed files with 184 additions and 78 deletions
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import ConnectionStatus from './lib/components/ConnectionStatus.svelte';
|
||||
import OutputManager from './lib/components/OutputManager.svelte';
|
||||
import PeopleSelector from './lib/components/PeopleSelector.svelte';
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
let selectedPerson = $state(null);
|
||||
let jobStatus = $state('idle');
|
||||
let progress = $state({ completed: 0, total: 0, message: '' });
|
||||
let pollInterval = $state(null);
|
||||
|
||||
let isJobRunning = $derived(
|
||||
jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling'
|
||||
|
|
@ -18,6 +20,10 @@
|
|||
|
||||
function handleConnectionChange(data) {
|
||||
connectionOk = data.connected;
|
||||
// Check for running job when connection is established
|
||||
if (data.connected) {
|
||||
checkAndPollProgress();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePersonSelect(person) {
|
||||
|
|
@ -27,7 +33,66 @@
|
|||
function handleJobUpdate(data) {
|
||||
jobStatus = data.status;
|
||||
progress = data;
|
||||
|
||||
// Start polling if job just started
|
||||
if (data.status === 'running' || data.status === 'compiling_video') {
|
||||
startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndPollProgress() {
|
||||
try {
|
||||
const res = await fetch('/api/progress');
|
||||
const data = await res.json();
|
||||
|
||||
jobStatus = data.status;
|
||||
progress = data;
|
||||
|
||||
// If a job is running, start polling
|
||||
if (data.status === 'running' || data.status === 'compiling_video' || data.status === 'cancelling') {
|
||||
startPolling();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check progress:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollProgress() {
|
||||
try {
|
||||
const res = await fetch('/api/progress');
|
||||
const data = await res.json();
|
||||
|
||||
jobStatus = data.status;
|
||||
progress = data;
|
||||
|
||||
// Stop polling when job completes
|
||||
if (data.status === 'completed' || data.status === 'cancelled' || data.status === 'error' || data.status === 'idle') {
|
||||
stopPolling();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Poll failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollInterval) return; // Already polling
|
||||
pollInterval = setInterval(pollProgress, 500);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Initial check will happen when connection is established
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
|
@ -47,7 +112,7 @@
|
|||
disabled={isJobRunning}
|
||||
/>
|
||||
|
||||
{#if selectedPerson}
|
||||
{#if selectedPerson && !isJobRunning}
|
||||
<ProcessingControls
|
||||
personId={selectedPerson.id}
|
||||
personName={selectedPerson.name}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let { personId, personName, jobStatus, onupdate } = $props();
|
||||
|
||||
let dateFrom = $state('');
|
||||
let dateTo = $state('');
|
||||
let pollInterval = $state(null);
|
||||
let assetCount = $state(null);
|
||||
let loadingCount = $state(false);
|
||||
|
||||
|
|
@ -51,7 +48,13 @@
|
|||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.success) {
|
||||
startPolling();
|
||||
// Notify parent to start polling
|
||||
onupdate?.({
|
||||
status: 'running',
|
||||
completed: 0,
|
||||
total: 0,
|
||||
message: 'Starting...',
|
||||
});
|
||||
} else {
|
||||
onupdate?.({
|
||||
status: 'error',
|
||||
|
|
@ -77,43 +80,6 @@
|
|||
console.error('Cancel failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollProgress() {
|
||||
try {
|
||||
const res = await fetch('/api/progress');
|
||||
const data = await res.json();
|
||||
|
||||
onupdate?.(data);
|
||||
|
||||
if (data.status === 'completed' || data.status === 'cancelled' || data.status === 'error') {
|
||||
stopPolling();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Poll failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollProgress();
|
||||
pollInterval = setInterval(pollProgress, 500);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Check if there's already a running job
|
||||
pollProgress();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="processing-controls">
|
||||
|
|
@ -147,15 +113,9 @@
|
|||
</div>
|
||||
|
||||
<div class="actions">
|
||||
{#if isRunning}
|
||||
<button class="cancel-btn" onclick={cancelProcessing}>
|
||||
Cancel
|
||||
</button>
|
||||
{:else}
|
||||
<button class="start-btn" onclick={startProcessing}>
|
||||
Start Processing
|
||||
</button>
|
||||
{/if}
|
||||
<button class="start-btn" onclick={startProcessing}>
|
||||
Start Processing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -271,13 +231,4 @@
|
|||
.start-btn:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
error: 'error',
|
||||
}[jobStatus] || '');
|
||||
|
||||
let canCancel = $derived(jobStatus === 'running' || jobStatus === 'compiling_video');
|
||||
|
||||
// Person being processed
|
||||
let personDisplay = $derived(progress.person_name || progress.person_id || null);
|
||||
|
||||
// Skip statistics from the backend
|
||||
let skipStats = $derived(progress.skip_stats || {
|
||||
face_too_small: 0,
|
||||
|
|
@ -39,8 +44,6 @@
|
|||
total: 0,
|
||||
});
|
||||
|
||||
let hasSkips = $derived(skipStats.total > 0);
|
||||
|
||||
// Calculate kept (successful) count: completed - skipped
|
||||
let keptCount = $derived(Math.max(0, progress.completed - skipStats.total));
|
||||
|
||||
|
|
@ -58,14 +61,32 @@
|
|||
{ label: 'Crop failed', count: skipStats.crop_failed },
|
||||
].filter(r => r.count > 0)
|
||||
);
|
||||
|
||||
async function cancelProcessing() {
|
||||
try {
|
||||
await fetch('/api/cancel', { method: 'POST' });
|
||||
} catch (e) {
|
||||
console.error('Cancel failed:', e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="progress-display">
|
||||
<div class="header">
|
||||
<span class="status {statusClass}">{statusLabel}</span>
|
||||
{#if progress.total > 0}
|
||||
<span class="count">{progress.completed} / {progress.total}</span>
|
||||
{/if}
|
||||
<div class="header-left">
|
||||
<span class="status {statusClass}">{statusLabel}</span>
|
||||
{#if personDisplay}
|
||||
<span class="person-name">{personDisplay}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{#if progress.total > 0}
|
||||
<span class="count">{progress.completed} / {progress.total}</span>
|
||||
{/if}
|
||||
{#if canCancel}
|
||||
<button class="cancel-btn" onclick={cancelProcessing}>Cancel</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if jobStatus === 'running' || jobStatus === 'compiling_video' || jobStatus === 'cancelling'}
|
||||
|
|
@ -114,6 +135,18 @@
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
font-size: 0.875rem;
|
||||
color: #e0e0e0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
|
|
@ -142,11 +175,33 @@
|
|||
color: #f87171;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 0.875rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: #dc2626;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: #333;
|
||||
|
|
|
|||
|
|
@ -97,18 +97,20 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat
|
|||
// Clear the cancellation token when done
|
||||
state.clear_cancel_token().await;
|
||||
|
||||
// Get final skip stats from progress
|
||||
let final_skip_stats = state.progress.read().await.skip_stats.clone();
|
||||
// Get final state from progress (skip stats and person info)
|
||||
let final_progress = state.progress.read().await.clone();
|
||||
|
||||
match result {
|
||||
Ok(output_path) => {
|
||||
state
|
||||
.update_progress(Progress {
|
||||
status: JobStatus::Completed,
|
||||
completed: 0,
|
||||
total: 0,
|
||||
completed: final_progress.completed,
|
||||
total: final_progress.total,
|
||||
message: Some(format!("Video saved to: {}", output_path.display())),
|
||||
skip_stats: final_skip_stats,
|
||||
skip_stats: final_progress.skip_stats,
|
||||
person_id: final_progress.person_id,
|
||||
person_name: final_progress.person_name,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
@ -116,10 +118,12 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat
|
|||
state
|
||||
.update_progress(Progress {
|
||||
status: JobStatus::Cancelled,
|
||||
completed: 0,
|
||||
total: 0,
|
||||
completed: final_progress.completed,
|
||||
total: final_progress.total,
|
||||
message: Some("Job cancelled by user".to_string()),
|
||||
skip_stats: final_skip_stats,
|
||||
skip_stats: final_progress.skip_stats,
|
||||
person_id: final_progress.person_id,
|
||||
person_name: final_progress.person_name,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
@ -128,10 +132,12 @@ pub async fn run_job(state: AppState, params: JobParams, cancel_token: Cancellat
|
|||
state
|
||||
.update_progress(Progress {
|
||||
status: JobStatus::Error(e.to_string()),
|
||||
completed: 0,
|
||||
total: 0,
|
||||
completed: final_progress.completed,
|
||||
total: final_progress.total,
|
||||
message: Some(e.to_string()),
|
||||
skip_stats: final_skip_stats,
|
||||
skip_stats: final_progress.skip_stats,
|
||||
person_id: final_progress.person_id,
|
||||
person_name: final_progress.person_name,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
@ -193,6 +199,8 @@ async fn run_job_inner(
|
|||
total: 0,
|
||||
message: Some("Fetching assets from Immich...".to_string()),
|
||||
skip_stats: SkipStats::default(),
|
||||
person_id: Some(params.person_id.clone()),
|
||||
person_name: params.person_name.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -244,6 +252,8 @@ async fn run_job_inner(
|
|||
total,
|
||||
message: Some(format!("Processing {} images...", total)),
|
||||
skip_stats: SkipStats::default(),
|
||||
person_id: Some(params.person_id.clone()),
|
||||
person_name: params.person_name.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -267,6 +277,10 @@ async fn run_job_inner(
|
|||
|
||||
let mut handles = Vec::with_capacity(assets_with_faces.len());
|
||||
|
||||
// Clone person info for use in spawned tasks
|
||||
let task_person_id = params.person_id.clone();
|
||||
let task_person_name = params.person_name.clone();
|
||||
|
||||
for (asset, face_data) in assets_with_faces {
|
||||
// Check for cancellation before spawning more tasks
|
||||
if cancel_token.is_cancelled() {
|
||||
|
|
@ -295,6 +309,10 @@ async fn run_job_inner(
|
|||
let skip_decode_failed = skip_decode_failed.clone();
|
||||
let skip_crop_failed = skip_crop_failed.clone();
|
||||
|
||||
// Clone person info for this task
|
||||
let person_id = task_person_id.clone();
|
||||
let person_name = task_person_name.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Check cancellation at start of task
|
||||
if task_cancel_token.is_cancelled() {
|
||||
|
|
@ -353,6 +371,8 @@ async fn run_job_inner(
|
|||
total,
|
||||
message: Some(format!("Processing images... ({}/{})", done, total)),
|
||||
skip_stats: current_skip_stats,
|
||||
person_id: Some(person_id.clone()),
|
||||
person_name: person_name.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
@ -419,6 +439,8 @@ async fn run_job_inner(
|
|||
total,
|
||||
message: Some("Processing complete, preparing video...".to_string()),
|
||||
skip_stats: skip_stats.clone(),
|
||||
person_id: Some(params.person_id.clone()),
|
||||
person_name: params.person_name.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -442,6 +464,8 @@ async fn run_job_inner(
|
|||
total: successful as u32,
|
||||
message: Some("Compiling video...".to_string()),
|
||||
skip_stats: skip_stats.clone(),
|
||||
person_id: Some(params.person_id.clone()),
|
||||
person_name: params.person_name.clone(),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,8 @@ struct ProgressResponse {
|
|||
total: u32,
|
||||
message: Option<String>,
|
||||
skip_stats: SkipStatsResponse,
|
||||
person_id: Option<String>,
|
||||
person_name: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_progress(State(state): State<AppState>) -> Json<ProgressResponse> {
|
||||
|
|
@ -292,6 +294,8 @@ async fn get_progress(State(state): State<AppState>) -> Json<ProgressResponse> {
|
|||
crop_failed: skip_stats.crop_failed,
|
||||
total: skip_stats.total(),
|
||||
},
|
||||
person_id: progress.person_id.clone(),
|
||||
person_name: progress.person_name.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +326,7 @@ async fn start_processing(
|
|||
}
|
||||
}
|
||||
|
||||
// Reset progress
|
||||
// Reset progress with person info
|
||||
state
|
||||
.update_progress(Progress {
|
||||
status: JobStatus::Running,
|
||||
|
|
@ -330,6 +334,8 @@ async fn start_processing(
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ pub struct Progress {
|
|||
pub total: u32,
|
||||
pub message: Option<String>,
|
||||
pub skip_stats: SkipStats,
|
||||
/// The person being processed (for display when resuming)
|
||||
pub person_id: Option<String>,
|
||||
pub person_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Progress {
|
||||
|
|
@ -64,6 +67,8 @@ impl Default for Progress {
|
|||
total: 0,
|
||||
message: None,
|
||||
skip_stats: SkipStats::default(),
|
||||
person_id: None,
|
||||
person_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue