feat(settings): stream engine check results

This commit is contained in:
NimBold
2026-06-18 20:20:10 +03:30
parent 62945a5516
commit de4d81ecd0
3 changed files with 167 additions and 24 deletions
+27 -3
View File
@@ -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<EngineStatusItem, String> {
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<EngineStatusItem, String> {
Ok(check_ytdlp(&app_handle).await)
}
#[tauri::command]
async fn get_ffmpeg_engine_status(app_handle: tauri::AppHandle) -> Result<EngineStatusItem, String> {
Ok(check_ffmpeg(&app_handle).await)
}
#[tauri::command]
async fn get_deno_engine_status(app_handle: tauri::AppHandle) -> Result<EngineStatusItem, String> {
Ok(check_deno(&app_handle).await)
}
fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str) -> Result<std::path::PathBuf, String> {
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,
+134 -20
View File
@@ -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<string, EngineStatusItem>();
const engineStatusInFlight = new Map<string, Promise<EngineStatusItem>>();
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<EngineStatusItem[] | null>(null);
const [expandedEngine, setExpandedEngine] = useState<string | null>(null);
const [appVersion, setAppVersion] = useState('0.7.3');
const [engineStatus, setEngineStatus] = useState<EngineStatusItem[] | null>(null);
const [expandedEngine, setExpandedEngine] = useState<string | null>(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' && (
<div className="settings-pane space-y-6 max-w-[760px]">
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Media Downloader & Engines</h3>
<div className="settings-pane space-y-6 max-w-[760px]">
<div className="flex items-center justify-between gap-3 border-b border-border-color/30 pb-2">
<div>
<h3 className="text-base font-bold text-text-primary">Media Downloader & Engines</h3>
<p className="text-[11px] text-text-muted mt-0.5">Successful results are reused for this app session. Recheck runs real validation again.</p>
</div>
<button
type="button"
onClick={() => runEngineChecks(true)}
disabled={isRecheckingEngines}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-bg-modal hover:bg-item-hover disabled:opacity-60 text-[12px] text-text-primary"
>
<RefreshCw size={13} className={isRecheckingEngines ? 'animate-spin' : ''} />
{isRecheckingEngines ? 'Checking' : 'Recheck engines'}
</button>
</div>
{(() => {
{(() => {
const a2 = findEngine('aria2'); const yt = findEngine('ytdlp');
const ff = findEngine('ffmpeg'); const dn = findEngine('deno');
return (
+6 -1
View File
@@ -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 };