From de4d81ecd05bb670da281252f2abd32bb80a5d88 Mon Sep 17 00:00:00 2001 From: NimBold Date: Thu, 18 Jun 2026 20:20:10 +0330 Subject: [PATCH] feat(settings): stream engine check results --- src-tauri/src/lib.rs | 30 ++++++- src/components/SettingsView.tsx | 154 +++++++++++++++++++++++++++----- src/ipc.ts | 7 +- 3 files changed, 167 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db8e3ed..5939e6f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1754,6 +1754,29 @@ async fn get_engine_status( }) } +#[tauri::command] +async fn get_aria2_engine_status( + app_handle: tauri::AppHandle, + state: tauri::State<'_, AppState>, +) -> Result { + Ok(check_aria2(&app_handle, state.aria2_port, &state.aria2_secret).await) +} + +#[tauri::command] +async fn get_ytdlp_engine_status(app_handle: tauri::AppHandle) -> Result { + Ok(check_ytdlp(&app_handle).await) +} + +#[tauri::command] +async fn get_ffmpeg_engine_status(app_handle: tauri::AppHandle) -> Result { + Ok(check_ffmpeg(&app_handle).await) +} + +#[tauri::command] +async fn get_deno_engine_status(app_handle: tauri::AppHandle) -> Result { + Ok(check_deno(&app_handle).await) +} + fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str) -> Result { let full_name = format!( @@ -3419,9 +3442,10 @@ pub fn run() { } } }) - .invoke_handler(tauri::generate_handler![ - get_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder, - pause_download, resume_download, fetch_metadata, fetch_media_metadata, +.invoke_handler(tauri::generate_handler![ + get_engine_status, get_aria2_engine_status, get_ytdlp_engine_status, get_ffmpeg_engine_status, + get_deno_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder, + pause_download, resume_download, fetch_metadata, fetch_media_metadata, update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action, request_automation_permission, open_automation_settings, set_keychain_password, get_keychain_password, delete_keychain_password, diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index caf15bf..853435e 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useCallback, useRef, useState, useEffect } from 'react'; import { type AppFontSize, type ListRowDensity, @@ -29,15 +29,83 @@ const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[ { type: 'about', label: 'About', icon: Info }, ]; +const engineChecks = [ + { kind: 'aria2', name: 'Aria2', command: 'get_aria2_engine_status' }, + { kind: 'ytdlp', name: 'yt-dlp', command: 'get_ytdlp_engine_status' }, + { kind: 'ffmpeg', name: 'FFmpeg', command: 'get_ffmpeg_engine_status' }, + { kind: 'deno', name: 'Deno', command: 'get_deno_engine_status' }, +] as const; + +type EngineCheck = typeof engineChecks[number]; + +const engineStatusCache = new Map(); +const engineStatusInFlight = new Map>(); + +const upsertEngineStatus = (items: EngineStatusItem[], item: EngineStatusItem) => { + const next = items.filter(existing => existing.kind !== item.kind); + next.push(item); + return next; +}; + +const buildEngineStatusError = (check: EngineCheck, error: unknown): EngineStatusItem => ({ + name: check.name, + kind: check.kind, + expected_sidecar: '', + resolved_path: null, + version: null, + ready: false, + error: String(error), + stderr_tail: null, + remediation_hint: null, + rpc_port: null, + daemon_alive: null, + rpc_ready: null, + last_stderr_tail: null, + expects_internal_dir: null, + has_internal_dir: null, + has_python_framework: null, +}); + +const runEngineStatusCheck = (check: EngineCheck, force: boolean) => { + if (!force) { + const cached = engineStatusCache.get(check.kind); + if (cached) return Promise.resolve(cached); + } + + if (!force) { + const inFlight = engineStatusInFlight.get(check.kind); + if (inFlight) return inFlight; + } + + if (force) engineStatusCache.delete(check.kind); + + const promise = invoke(check.command) + .then(item => { + if (item.ready) engineStatusCache.set(item.kind, item); + return item; + }) + .catch(error => buildEngineStatusError(check, error)) + .finally(() => { + if (engineStatusInFlight.get(check.kind) === promise) { + engineStatusInFlight.delete(check.kind); + } + }); + + engineStatusInFlight.set(check.kind, promise); + return promise; +}; + export default function SettingsView() { const settings = useSettingsStore(); const { pickDirectory } = useDirectoryPicker(); const activeTab = settings.activeSettingsTab; // Local state for engine diagnostics - const [engineStatus, setEngineStatus] = useState(null); - const [expandedEngine, setExpandedEngine] = useState(null); - const [appVersion, setAppVersion] = useState('0.7.3'); +const [engineStatus, setEngineStatus] = useState(null); +const [expandedEngine, setExpandedEngine] = useState(null); +const [isRecheckingEngines, setIsRecheckingEngines] = useState(false); +const engineRunId = useRef(0); +const [appVersion, setAppVersion] = useState('0.7.3'); // Local state for adding site login const [loginPattern, setLoginPattern] = useState(''); @@ -56,20 +124,52 @@ export default function SettingsView() { } }, [toastMessage]); - useEffect(() => { - getVersion().then(setAppVersion).catch(() => undefined); - }, []); +useEffect(() => { +getVersion().then(setAppVersion).catch(() => undefined); +}, []); - // Fetch engine status when Engine tab is opened - useEffect(() => { - if (settings.activeView === 'settings' && activeTab === 'engine') { - setEngineStatus(null); - setExpandedEngine(null); - invoke('get_engine_status') - .then(r => setEngineStatus(r.engines)) - .catch(() => setEngineStatus([])); - } - }, [settings.activeView, activeTab]); +const runEngineChecks = useCallback((force = false) => { +const runId = ++engineRunId.current; +const cached = engineChecks +.map(check => engineStatusCache.get(check.kind)) +.filter((item): item is EngineStatusItem => Boolean(item)); +setEngineStatus(force ? [] : cached); +setExpandedEngine(null); + +const checksToRun = force +? engineChecks +: engineChecks.filter(check => !engineStatusCache.has(check.kind)); + +if (checksToRun.length === 0) { +setIsRecheckingEngines(false); +return; +} + +setIsRecheckingEngines(true); +let remaining = checksToRun.length; + +checksToRun.forEach(check => { +runEngineStatusCheck(check, force) +.then(item => { +if (engineRunId.current === runId) { +setEngineStatus(current => upsertEngineStatus(current ?? [], item)); +} +}) +.finally(() => { +remaining -= 1; +if (remaining === 0 && engineRunId.current === runId) { +setIsRecheckingEngines(false); +} +}); +}); +}, []); + +// Fetch engine status when Engine tab is opened +useEffect(() => { +if (settings.activeView === 'settings' && activeTab === 'engine') { +runEngineChecks(false); +} +}, [settings.activeView, activeTab, runEngineChecks]); const showToast = (msg: string) => { setToastMessage(msg); @@ -767,10 +867,24 @@ export default function SettingsView() { {/* Engine Pane */} {activeTab === 'engine' && ( -
-

Media Downloader & Engines

+
+
+
+

Media Downloader & Engines

+

Successful results are reused for this app session. Recheck runs real validation again.

+
+ +
- {(() => { +{(() => { const a2 = findEngine('aria2'); const yt = findEngine('ytdlp'); const ff = findEngine('ffmpeg'); const dn = findEngine('deno'); return ( diff --git a/src/ipc.ts b/src/ipc.ts index 146b831..ab2eff2 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -10,6 +10,7 @@ import type { ExtensionDownload } from './bindings/ExtensionDownload'; import type { MediaCookieSource } from './bindings/MediaCookieSource'; import type { MediaMetadata } from './bindings/MediaMetadata'; import type { MetadataResponse } from './bindings/MetadataResponse'; +import type { EngineStatusItem } from './bindings/EngineStatusItem'; import type { EngineStatusResult } from './bindings/EngineStatusResult'; import type { PostQueueAction } from './bindings/PostQueueAction'; import type { ReleaseCheckOutcome } from './bindings/ReleaseCheckOutcome'; @@ -57,7 +58,11 @@ type CommandMap = { args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null }; result: MediaMetadata; }; - get_engine_status: { args: undefined; result: EngineStatusResult }; + get_engine_status: { args: undefined; result: EngineStatusResult }; + get_aria2_engine_status: { args: undefined; result: EngineStatusItem }; + get_ytdlp_engine_status: { args: undefined; result: EngineStatusItem }; + get_ffmpeg_engine_status: { args: undefined; result: EngineStatusItem }; + get_deno_engine_status: { args: undefined; result: EngineStatusItem }; open_file: { args: { path: string }; result: void }; show_in_folder: { args: { path: string }; result: void }; reveal_in_file_manager: { args: { path: string }; result: void };