fix(tools): never block downloads while a runnable tool exists

ensureStreamlinkInstalled and ensureFfmpegInstalled forced a managed
reinstall whenever a bundled tool directory existed without passing
manifest verification. When that reinstall failed (offline, blocked
download, locked executable), both functions returned false without ever
probing the tool that was already present and runnable, so every
download path aborted with an auto-install error while the System Check
kept reporting the same tool as available. Both functions now fall back
to the same executability probe the System Check uses after a failed
managed repair, cache the verified command on success, and only report
failure when nothing runnable remains. The repair attempt itself is
preserved and retried on the next call.

The tool archive download also registered no error handler on the HTTP
response stream. A stream that failed mid-transfer settled neither the
writer finish nor the writer error path, leaving the install promise
pending forever and the download start hanging indefinitely. The
response stream now rejects the download on error and destroys the
writer.

Merge group downloads reported the bare "Streamlink is missing." text
while the VOD and live paths already explained the failed auto-install.
All three gates now share streamlinkAutoInstallFailed, ffmpeg gained the
matching ffmpegAutoInstallFailed message, and the now unused
streamlinkMissing and ffmpegMissing keys were removed.

The Managed tools panel showed "Missing / Unverified" with no hint that
downloads still work through a system-provided installation, which reads
like a hard failure on machines that never installed the managed copies.
getManagedToolStatuses now reports per tool whether a runnable
installation answers the version probe, and the panel appends a
localized "Downloads still available" note whenever an unverified state
is covered by a working tool.
This commit is contained in:
Sucukdeluxe
2026-08-14 12:46:02 +02:00
parent 9e8a3a0f4d
commit 477dfd5750
10 changed files with 281 additions and 19 deletions
+9
View File
@@ -85,6 +85,15 @@ describe('main runtime safety production paths', () => {
expect(closeHandler.indexOf('finish({ success: true, filename })')).toBeGreaterThan(closeHandler.indexOf('partialDownloadRegistry.commit')); expect(closeHandler.indexOf('finish({ success: true, filename })')).toBeGreaterThan(closeHandler.indexOf('partialDownloadRegistry.commit'));
}); });
it('reports the auto-install failure guidance on every download tool gate', () => {
const source = mainSource();
const mergeGroup = source.slice(source.indexOf('async function processDownloadMergeGroup'), source.indexOf('// ---- PHASE 2: MERGING ----'));
expect(mergeGroup).toContain("tBackend('streamlinkAutoInstallFailed')");
expect(mergeGroup).toContain("tBackend('ffmpegAutoInstallFailed')");
expect(source).not.toContain("tBackend('streamlinkMissing')");
expect(source).not.toContain("tBackend('ffmpegMissing')");
});
it('guards and tracks every standalone cut and merge process across shutdown', () => { it('guards and tracks every standalone cut and merge process across shutdown', () => {
const source = mainSource(); const source = mainSource();
const cut = source.slice(source.indexOf('async function cutVideo('), source.indexOf('async function mergeVideos(')); const cut = source.slice(source.indexOf('async function cutVideo('), source.indexOf('async function mergeVideos('));
+2 -2
View File
@@ -6471,12 +6471,12 @@ async function processDownloadMergeGroup(
if (mg.mergePhase === 'downloading') { if (mg.mergePhase === 'downloading') {
const streamlinkReady = await ensureStreamlinkInstalled(); const streamlinkReady = await ensureStreamlinkInstalled();
if (!streamlinkReady) { if (!streamlinkReady) {
return { success: false, error: tBackend('streamlinkMissing') }; return { success: false, error: tBackend('streamlinkAutoInstallFailed') };
} }
const ffmpegReady = await ensureFfmpegInstalled(); const ffmpegReady = await ensureFfmpegInstalled();
if (!ffmpegReady) { if (!ffmpegReady) {
return { success: false, error: tBackend('ffmpegMissing') }; return { success: false, error: tBackend('ffmpegAutoInstallFailed') };
} }
const streamer = mg.items[0].streamer.replace(/[^a-zA-Z0-9_-]/g, ''); const streamer = mg.items[0].streamer.replace(/[^a-zA-Z0-9_-]/g, '');
+2 -4
View File
@@ -7,10 +7,9 @@ export const BACKEND_MESSAGES = {
invalidClipUrl: 'Ungültige Clip-URL', invalidClipUrl: 'Ungültige Clip-URL',
clipNotFound: 'Clip nicht gefunden', clipNotFound: 'Clip nicht gefunden',
streamlinkAutoInstallFailed: 'Streamlink fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.', streamlinkAutoInstallFailed: 'Streamlink fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.',
streamlinkMissing: 'Streamlink fehlt.',
streamlinkNotFound: 'Streamlink nicht gefunden. Installiere Streamlink oder Python+streamlink (py -3 -m pip install streamlink).', streamlinkNotFound: 'Streamlink nicht gefunden. Installiere Streamlink oder Python+streamlink (py -3 -m pip install streamlink).',
streamlinkExitCode: 'Streamlink Fehlercode {code}', streamlinkExitCode: 'Streamlink Fehlercode {code}',
ffmpegMissing: 'FFmpeg fehlt.', ffmpegAutoInstallFailed: 'FFmpeg fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.',
ffmpegMergeFailed: 'FFmpeg Merge fehlgeschlagen.', ffmpegMergeFailed: 'FFmpeg Merge fehlgeschlagen.',
ffmpegSplitFailed: 'FFmpeg Split fehlgeschlagen.', ffmpegSplitFailed: 'FFmpeg Split fehlgeschlagen.',
fileTooSmall: 'Datei zu klein ({bytes} Bytes)', fileTooSmall: 'Datei zu klein ({bytes} Bytes)',
@@ -49,10 +48,9 @@ export const BACKEND_MESSAGES = {
invalidClipUrl: 'Invalid clip URL', invalidClipUrl: 'Invalid clip URL',
clipNotFound: 'Clip not found', clipNotFound: 'Clip not found',
streamlinkAutoInstallFailed: 'Streamlink is missing and could not be auto-installed. See debug.log.', streamlinkAutoInstallFailed: 'Streamlink is missing and could not be auto-installed. See debug.log.',
streamlinkMissing: 'Streamlink is missing.',
streamlinkNotFound: 'Streamlink not found. Install streamlink or Python+streamlink (py -3 -m pip install streamlink).', streamlinkNotFound: 'Streamlink not found. Install streamlink or Python+streamlink (py -3 -m pip install streamlink).',
streamlinkExitCode: 'Streamlink exit code {code}', streamlinkExitCode: 'Streamlink exit code {code}',
ffmpegMissing: 'FFmpeg is missing.', ffmpegAutoInstallFailed: 'FFmpeg is missing and could not be auto-installed. See debug.log.',
ffmpegMergeFailed: 'FFmpeg merge failed.', ffmpegMergeFailed: 'FFmpeg merge failed.',
ffmpegSplitFailed: 'FFmpeg split failed.', ffmpegSplitFailed: 'FFmpeg split failed.',
fileTooSmall: 'File too small ({bytes} bytes)', fileTooSmall: 'File too small ({bytes} bytes)',
+1
View File
@@ -305,6 +305,7 @@ interface ManagedToolStatus {
archiveName: string; archiveName: string;
state: 'missing' | 'installing' | 'verified' | 'unverified' | 'corrupt'; state: 'missing' | 'installing' | 'verified' | 'unverified' | 'corrupt';
verified: boolean; verified: boolean;
fallbackRunnable: boolean;
} }
interface ManagedToolStatuses { interface ManagedToolStatuses {
+1
View File
@@ -342,6 +342,7 @@ const UI_TEXT_DE = {
managedToolsVerified: 'Verifiziert', managedToolsVerified: 'Verifiziert',
managedToolsUnverified: 'Nicht verifiziert', managedToolsUnverified: 'Nicht verifiziert',
managedToolsCorrupt: 'Beschädigt', managedToolsCorrupt: 'Beschädigt',
managedToolsFallbackActive: 'Downloads weiterhin möglich',
debugLogTitle: 'Live Debug-Log', debugLogTitle: 'Live Debug-Log',
refreshLog: 'Aktualisieren', refreshLog: 'Aktualisieren',
autoRefresh: 'Auto-Refresh', autoRefresh: 'Auto-Refresh',
+1
View File
@@ -342,6 +342,7 @@ const UI_TEXT_EN = {
managedToolsVerified: 'Verified', managedToolsVerified: 'Verified',
managedToolsUnverified: 'Unverified', managedToolsUnverified: 'Unverified',
managedToolsCorrupt: 'Corrupt', managedToolsCorrupt: 'Corrupt',
managedToolsFallbackActive: 'Downloads still available',
debugLogTitle: 'Live Debug Log', debugLogTitle: 'Live Debug Log',
refreshLog: 'Refresh', refreshLog: 'Refresh',
autoRefresh: 'Auto refresh', autoRefresh: 'Auto refresh',
@@ -77,6 +77,43 @@ function createElements(...ids: string[]): Map<string, FakeElement> {
} }
describe('renderer settings production diagnostics paths', () => { describe('renderer settings production diagnostics paths', () => {
it('marks unverified managed tools as still downloadable when a runnable installation exists', () => {
const elements = createElements('managedToolStatus');
const context = {
UI_TEXT: {
static: {
managedToolsMissing: 'Missing',
managedToolsInstalling: 'Installing',
managedToolsVerified: 'Verified',
managedToolsUnverified: 'Unverified',
managedToolsCorrupt: 'Corrupt',
managedToolsFallbackActive: 'Downloads still available',
},
},
byId: (id: string) => elements.get(id),
};
const api = evaluate(
sourceFragment('function getManagedToolStateLabel', 'async function refreshManagedToolStatus'),
context,
'renderManagedToolStatus'
);
api.renderManagedToolStatus({
streamlink: { id: 'streamlink', version: '8.4.0', state: 'missing', verified: false, fallbackRunnable: true },
ffmpeg: { id: 'ffmpeg', version: '8.1.2', state: 'missing', verified: false, fallbackRunnable: false },
});
const lines = (elements.get('managedToolStatus')?.textContent ?? '').split('\n');
expect(lines[0]).toBe('streamlink 8.4.0: Missing · Unverified · Downloads still available');
expect(lines[1]).toBe('ffmpeg 8.1.2: Missing · Unverified');
api.renderManagedToolStatus({
streamlink: { id: 'streamlink', version: '8.4.0', state: 'verified', verified: true, fallbackRunnable: true },
ffmpeg: { id: 'ffmpeg', version: '8.1.2', state: 'verified', verified: true, fallbackRunnable: true },
});
expect(elements.get('managedToolStatus')?.textContent).not.toContain('Downloads still available');
});
it('ends every consecutive runtime metrics rejection in the localized error state', async () => { it('ends every consecutive runtime metrics rejection in the localized error state', async () => {
const elements = createElements('runtimeMetricsOutput'); const elements = createElements('runtimeMetricsOutput');
const context = { const context = {
+5 -1
View File
@@ -471,7 +471,11 @@ function getManagedToolStateLabel(state: ManagedToolStatus['state']): string {
function renderManagedToolStatus(statuses: ManagedToolStatuses): void { function renderManagedToolStatus(statuses: ManagedToolStatuses): void {
const lines = [statuses.streamlink, statuses.ffmpeg].map((status) => { const lines = [statuses.streamlink, statuses.ffmpeg].map((status) => {
const verification = status.verified ? UI_TEXT.static.managedToolsVerified : UI_TEXT.static.managedToolsUnverified; const verification = status.verified ? UI_TEXT.static.managedToolsVerified : UI_TEXT.static.managedToolsUnverified;
return `${status.id} ${status.version}: ${getManagedToolStateLabel(status.state)} · ${verification}`; const parts = [`${status.id} ${status.version}: ${getManagedToolStateLabel(status.state)} · ${verification}`];
if (!status.verified && status.fallbackRunnable) {
parts.push(UI_TEXT.static.managedToolsFallbackActive);
}
return parts.join(' · ');
}); });
byId('managedToolStatus').textContent = lines.join('\n'); byId('managedToolStatus').textContent = lines.join('\n');
} }
+178
View File
@@ -0,0 +1,178 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { PassThrough } from 'node:stream';
vi.mock('axios', () => ({
default: {
get: vi.fn()
}
}));
vi.mock('child_process', () => ({
spawn: vi.fn(),
execSync: vi.fn(() => {
throw new Error('not on PATH');
}),
spawnSync: vi.fn(() => ({ status: 1 }))
}));
interface ToolsModule {
initToolDirs(streamlinkDir: string, ffmpegDir: string, getTempPath: () => string): void;
setDebugLogFn(fn: (message: string, details?: unknown) => void): void;
ensureStreamlinkInstalled(): Promise<boolean>;
ensureFfmpegInstalled(): Promise<boolean>;
getManagedToolStatuses(): Promise<{
streamlink: { state: string; verified: boolean; fallbackRunnable: boolean };
ffmpeg: { state: string; verified: boolean; fallbackRunnable: boolean };
}>;
}
const originalPlatform = process.platform;
let tempRoot: string;
let streamlinkDir: string;
let ffmpegDir: string;
let debugMessages: string[];
function forcePlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
}
function writeUnverifiedStreamlinkInstall(): string {
const binDir = path.join(streamlinkDir, 'bin');
fs.mkdirSync(binDir, { recursive: true });
const exePath = path.join(binDir, 'streamlink.exe');
fs.writeFileSync(exePath, 'dummy-streamlink-binary');
return exePath;
}
function writeUnverifiedFfmpegInstall(): { ffmpegPath: string; ffprobePath: string } {
const binDir = path.join(ffmpegDir, 'bin');
fs.mkdirSync(binDir, { recursive: true });
const ffmpegPath = path.join(binDir, 'ffmpeg.exe');
const ffprobePath = path.join(binDir, 'ffprobe.exe');
fs.writeFileSync(ffmpegPath, 'dummy-ffmpeg-binary');
fs.writeFileSync(ffprobePath, 'dummy-ffprobe-binary');
return { ffmpegPath, ffprobePath };
}
async function loadTools(): Promise<{ tools: ToolsModule; axiosGet: ReturnType<typeof vi.fn>; spawnSync: ReturnType<typeof vi.fn> }> {
vi.resetModules();
const axios = (await import('axios')).default as unknown as { get: ReturnType<typeof vi.fn> };
const childProcess = await import('child_process') as unknown as { spawnSync: ReturnType<typeof vi.fn> };
const tools = await import('./tools') as unknown as ToolsModule;
tools.initToolDirs(streamlinkDir, ffmpegDir, () => path.join(tempRoot, 'tmp'));
tools.setDebugLogFn((message) => {
debugMessages.push(message);
});
return { tools, axiosGet: axios.get, spawnSync: childProcess.spawnSync };
}
function allowExecutionOf(spawnSync: ReturnType<typeof vi.fn>, runnablePaths: string[]): void {
spawnSync.mockImplementation((command: string) => (
runnablePaths.includes(command) ? { status: 0 } : { status: 1 }
));
}
beforeEach(() => {
forcePlatform('win32');
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'tvm-tools-test-'));
streamlinkDir = path.join(tempRoot, 'tools', 'streamlink');
ffmpegDir = path.join(tempRoot, 'tools', 'ffmpeg');
fs.mkdirSync(streamlinkDir, { recursive: true });
fs.mkdirSync(ffmpegDir, { recursive: true });
debugMessages = [];
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
fs.rmSync(tempRoot, { recursive: true, force: true });
});
describe('ensureStreamlinkInstalled with an unverified managed install', () => {
it('returns true when the forced repair download fails but the existing streamlink runs', async () => {
const exePath = writeUnverifiedStreamlinkInstall();
const { tools, axiosGet, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, [exePath]);
axiosGet.mockRejectedValue(new Error('offline'));
await expect(tools.ensureStreamlinkInstalled()).resolves.toBe(true);
expect(debugMessages).toContain('streamlink-install-start');
expect(debugMessages).toContain('streamlink-install-failed');
});
it('returns false when the forced repair fails and the existing streamlink does not run', async () => {
writeUnverifiedStreamlinkInstall();
const { tools, axiosGet, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, []);
axiosGet.mockRejectedValue(new Error('offline'));
await expect(tools.ensureStreamlinkInstalled()).resolves.toBe(false);
expect(debugMessages).toContain('streamlink-install-failed');
});
it('resolves instead of hanging when the download response stream errors mid-transfer', async () => {
const exePath = writeUnverifiedStreamlinkInstall();
const { tools, axiosGet, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, [exePath]);
const brokenStream = new PassThrough();
let errorScheduled = false;
brokenStream.on('newListener', (event) => {
if (event === 'error' && !errorScheduled) {
errorScheduled = true;
setImmediate(() => brokenStream.emit('error', new Error('connection reset')));
}
});
axiosGet.mockResolvedValue({ data: brokenStream });
await expect(tools.ensureStreamlinkInstalled()).resolves.toBe(true);
expect(debugMessages).toContain('streamlink-install-failed');
}, 8000);
});
describe('getManagedToolStatuses fallback availability', () => {
it('reports a runnable unverified install as still downloadable', async () => {
const exePath = writeUnverifiedStreamlinkInstall();
const { tools, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, [exePath]);
const statuses = await tools.getManagedToolStatuses();
expect(statuses.streamlink.verified).toBe(false);
expect(statuses.streamlink.fallbackRunnable).toBe(true);
expect(statuses.ffmpeg.fallbackRunnable).toBe(false);
});
it('reports nothing runnable when no installation responds', async () => {
writeUnverifiedStreamlinkInstall();
const { tools, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, []);
const statuses = await tools.getManagedToolStatuses();
expect(statuses.streamlink.fallbackRunnable).toBe(false);
expect(statuses.ffmpeg.fallbackRunnable).toBe(false);
});
});
describe('ensureFfmpegInstalled with an unverified managed install', () => {
it('returns true when the forced repair download fails but the existing ffmpeg and ffprobe run', async () => {
const { ffmpegPath, ffprobePath } = writeUnverifiedFfmpegInstall();
const { tools, axiosGet, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, [ffmpegPath, ffprobePath]);
axiosGet.mockRejectedValue(new Error('offline'));
await expect(tools.ensureFfmpegInstalled()).resolves.toBe(true);
expect(debugMessages).toContain('ffmpeg-install-start');
expect(debugMessages).toContain('ffmpeg-install-failed');
});
it('returns false when the forced repair fails and the existing ffmpeg does not run', async () => {
writeUnverifiedFfmpegInstall();
const { tools, axiosGet, spawnSync } = await loadTools();
allowExecutionOf(spawnSync, []);
axiosGet.mockRejectedValue(new Error('offline'));
await expect(tools.ensureFfmpegInstalled()).resolves.toBe(false);
expect(debugMessages).toContain('ffmpeg-install-failed');
});
});
+45 -12
View File
@@ -338,6 +338,10 @@ async function downloadFile(url: string, destinationPath: string): Promise<boole
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const writer = fs.createWriteStream(destinationPath); const writer = fs.createWriteStream(destinationPath);
response.data.on('error', (err: Error) => {
writer.destroy();
reject(err);
});
response.data.pipe(writer); response.data.pipe(writer);
writer.on('finish', () => resolve()); writer.on('finish', () => resolve());
writer.on('error', (err) => reject(err)); writer.on('error', (err) => reject(err));
@@ -412,14 +416,27 @@ function getManagedToolInstaller(toolId: 'streamlink' | 'ffmpeg'): ManagedToolIn
} }
export interface ManagedToolStatuses { export interface ManagedToolStatuses {
streamlink: ManagedToolStatus; streamlink: ManagedToolStatus & { fallbackRunnable: boolean };
ffmpeg: ManagedToolStatus; ffmpeg: ManagedToolStatus & { fallbackRunnable: boolean };
} }
export async function getManagedToolStatuses(): Promise<ManagedToolStatuses> { export async function getManagedToolStatuses(): Promise<ManagedToolStatuses> {
refreshBundledToolPaths();
const streamlinkCommand = getStreamlinkCommand();
const streamlinkVersionArgs = [...streamlinkCommand.prefixArgs, '--version'];
const ffmpegPath = getFFmpegPath();
const ffprobePath = getFFprobePath();
return { return {
streamlink: await getManagedToolInstaller('streamlink').status(APPLICATION_TOOL_MANIFEST.streamlink), streamlink: {
ffmpeg: await getManagedToolInstaller('ffmpeg').status(APPLICATION_TOOL_MANIFEST.ffmpeg) ...(await getManagedToolInstaller('streamlink').status(APPLICATION_TOOL_MANIFEST.streamlink)),
fallbackRunnable: isVerifiedStreamlinkCommand(streamlinkCommand.command, streamlinkVersionArgs)
|| fallbackToRunnableStreamlink(streamlinkCommand.command, streamlinkVersionArgs)
},
ffmpeg: {
...(await getManagedToolInstaller('ffmpeg').status(APPLICATION_TOOL_MANIFEST.ffmpeg)),
fallbackRunnable: isVerifiedFfmpegCommands(ffmpegPath, ffprobePath)
|| fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath)
}
}; };
} }
@@ -450,6 +467,22 @@ export async function resetManagedTools(): Promise<{ success: boolean; statuses:
// ========================================== // ==========================================
// AUTO-INSTALL TOOLS // AUTO-INSTALL TOOLS
// ========================================== // ==========================================
function fallbackToRunnableStreamlink(command: string, versionArgs: string[]): boolean {
if (!canExecuteCommand(command, versionArgs)) {
return false;
}
cacheVerifiedStreamlinkCommand(command, versionArgs);
return true;
}
function fallbackToRunnableFfmpeg(ffmpegPath: string, ffprobePath: string): boolean {
if (!canExecuteCommand(ffmpegPath, ['-version']) || !canExecuteCommand(ffprobePath, ['-version'])) {
return false;
}
cacheVerifiedFfmpegCommands(ffmpegPath, ffprobePath);
return true;
}
export async function ensureStreamlinkInstalled(): Promise<boolean> { export async function ensureStreamlinkInstalled(): Promise<boolean> {
refreshBundledToolPaths(); refreshBundledToolPaths();
@@ -468,7 +501,7 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
} }
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
return false; return fallbackToRunnableStreamlink(current.command, versionArgs);
} }
_appendDebugLog('streamlink-install-start'); _appendDebugLog('streamlink-install-start');
@@ -476,7 +509,7 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
const result = await getManagedToolInstaller('streamlink').install(manifest); const result = await getManagedToolInstaller('streamlink').install(manifest);
if (!result.success) { if (!result.success) {
_appendDebugLog('streamlink-install-failed', { error: result.error, status: result.status }); _appendDebugLog('streamlink-install-failed', { error: result.error, status: result.status });
return false; return fallbackToRunnableStreamlink(current.command, versionArgs);
} }
refreshBundledToolPaths(true); refreshBundledToolPaths(true);
@@ -489,10 +522,10 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
cacheVerifiedStreamlinkCommand(cmd.command, installedVersionArgs); cacheVerifiedStreamlinkCommand(cmd.command, installedVersionArgs);
} }
_appendDebugLog('streamlink-install-finished', { works, command: cmd.command, prefixArgs: cmd.prefixArgs }); _appendDebugLog('streamlink-install-finished', { works, command: cmd.command, prefixArgs: cmd.prefixArgs });
return works; return works || fallbackToRunnableStreamlink(current.command, versionArgs);
} catch (e) { } catch (e) {
_appendDebugLog('streamlink-install-failed', String(e)); _appendDebugLog('streamlink-install-failed', String(e));
return false; return fallbackToRunnableStreamlink(current.command, versionArgs);
} }
} }
@@ -514,7 +547,7 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
} }
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
return false; return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
} }
_appendDebugLog('ffmpeg-install-start'); _appendDebugLog('ffmpeg-install-start');
@@ -522,7 +555,7 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
const result = await getManagedToolInstaller('ffmpeg').install(manifest); const result = await getManagedToolInstaller('ffmpeg').install(manifest);
if (!result.success) { if (!result.success) {
_appendDebugLog('ffmpeg-install-failed', { error: result.error, status: result.status }); _appendDebugLog('ffmpeg-install-failed', { error: result.error, status: result.status });
return false; return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
} }
refreshBundledToolPaths(true); refreshBundledToolPaths(true);
@@ -534,9 +567,9 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
cacheVerifiedFfmpegCommands(newFfmpegPath, newFfprobePath); cacheVerifiedFfmpegCommands(newFfmpegPath, newFfprobePath);
} }
_appendDebugLog('ffmpeg-install-finished', { works, ffmpeg: newFfmpegPath, ffprobe: newFfprobePath }); _appendDebugLog('ffmpeg-install-finished', { works, ffmpeg: newFfmpegPath, ffprobe: newFfprobePath });
return works; return works || fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
} catch (e) { } catch (e) {
_appendDebugLog('ffmpeg-install-failed', String(e)); _appendDebugLog('ffmpeg-install-failed', String(e));
return false; return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
} }
} }