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:
@@ -85,6 +85,15 @@ describe('main runtime safety production paths', () => {
|
||||
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', () => {
|
||||
const source = mainSource();
|
||||
const cut = source.slice(source.indexOf('async function cutVideo('), source.indexOf('async function mergeVideos('));
|
||||
|
||||
+2
-2
@@ -6471,12 +6471,12 @@ async function processDownloadMergeGroup(
|
||||
if (mg.mergePhase === 'downloading') {
|
||||
const streamlinkReady = await ensureStreamlinkInstalled();
|
||||
if (!streamlinkReady) {
|
||||
return { success: false, error: tBackend('streamlinkMissing') };
|
||||
return { success: false, error: tBackend('streamlinkAutoInstallFailed') };
|
||||
}
|
||||
|
||||
const ffmpegReady = await ensureFfmpegInstalled();
|
||||
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, '');
|
||||
|
||||
@@ -7,10 +7,9 @@ export const BACKEND_MESSAGES = {
|
||||
invalidClipUrl: 'Ungültige Clip-URL',
|
||||
clipNotFound: 'Clip nicht gefunden',
|
||||
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).',
|
||||
streamlinkExitCode: 'Streamlink Fehlercode {code}',
|
||||
ffmpegMissing: 'FFmpeg fehlt.',
|
||||
ffmpegAutoInstallFailed: 'FFmpeg fehlt und konnte nicht automatisch installiert werden. Siehe debug.log.',
|
||||
ffmpegMergeFailed: 'FFmpeg Merge fehlgeschlagen.',
|
||||
ffmpegSplitFailed: 'FFmpeg Split fehlgeschlagen.',
|
||||
fileTooSmall: 'Datei zu klein ({bytes} Bytes)',
|
||||
@@ -49,10 +48,9 @@ export const BACKEND_MESSAGES = {
|
||||
invalidClipUrl: 'Invalid clip URL',
|
||||
clipNotFound: 'Clip not found',
|
||||
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).',
|
||||
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.',
|
||||
ffmpegSplitFailed: 'FFmpeg split failed.',
|
||||
fileTooSmall: 'File too small ({bytes} bytes)',
|
||||
|
||||
Vendored
+1
@@ -305,6 +305,7 @@ interface ManagedToolStatus {
|
||||
archiveName: string;
|
||||
state: 'missing' | 'installing' | 'verified' | 'unverified' | 'corrupt';
|
||||
verified: boolean;
|
||||
fallbackRunnable: boolean;
|
||||
}
|
||||
|
||||
interface ManagedToolStatuses {
|
||||
|
||||
@@ -342,6 +342,7 @@ const UI_TEXT_DE = {
|
||||
managedToolsVerified: 'Verifiziert',
|
||||
managedToolsUnverified: 'Nicht verifiziert',
|
||||
managedToolsCorrupt: 'Beschädigt',
|
||||
managedToolsFallbackActive: 'Downloads weiterhin möglich',
|
||||
debugLogTitle: 'Live Debug-Log',
|
||||
refreshLog: 'Aktualisieren',
|
||||
autoRefresh: 'Auto-Refresh',
|
||||
|
||||
@@ -342,6 +342,7 @@ const UI_TEXT_EN = {
|
||||
managedToolsVerified: 'Verified',
|
||||
managedToolsUnverified: 'Unverified',
|
||||
managedToolsCorrupt: 'Corrupt',
|
||||
managedToolsFallbackActive: 'Downloads still available',
|
||||
debugLogTitle: 'Live Debug Log',
|
||||
refreshLog: 'Refresh',
|
||||
autoRefresh: 'Auto refresh',
|
||||
|
||||
@@ -77,6 +77,43 @@ function createElements(...ids: string[]): Map<string, FakeElement> {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const elements = createElements('runtimeMetricsOutput');
|
||||
const context = {
|
||||
|
||||
@@ -471,7 +471,11 @@ function getManagedToolStateLabel(state: ManagedToolStatus['state']): string {
|
||||
function renderManagedToolStatus(statuses: ManagedToolStatuses): void {
|
||||
const lines = [statuses.streamlink, statuses.ffmpeg].map((status) => {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -338,6 +338,10 @@ async function downloadFile(url: string, destinationPath: string): Promise<boole
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writer = fs.createWriteStream(destinationPath);
|
||||
response.data.on('error', (err: Error) => {
|
||||
writer.destroy();
|
||||
reject(err);
|
||||
});
|
||||
response.data.pipe(writer);
|
||||
writer.on('finish', () => resolve());
|
||||
writer.on('error', (err) => reject(err));
|
||||
@@ -412,14 +416,27 @@ function getManagedToolInstaller(toolId: 'streamlink' | 'ffmpeg'): ManagedToolIn
|
||||
}
|
||||
|
||||
export interface ManagedToolStatuses {
|
||||
streamlink: ManagedToolStatus;
|
||||
ffmpeg: ManagedToolStatus;
|
||||
streamlink: ManagedToolStatus & { fallbackRunnable: boolean };
|
||||
ffmpeg: ManagedToolStatus & { fallbackRunnable: boolean };
|
||||
}
|
||||
|
||||
export async function getManagedToolStatuses(): Promise<ManagedToolStatuses> {
|
||||
refreshBundledToolPaths();
|
||||
const streamlinkCommand = getStreamlinkCommand();
|
||||
const streamlinkVersionArgs = [...streamlinkCommand.prefixArgs, '--version'];
|
||||
const ffmpegPath = getFFmpegPath();
|
||||
const ffprobePath = getFFprobePath();
|
||||
return {
|
||||
streamlink: await getManagedToolInstaller('streamlink').status(APPLICATION_TOOL_MANIFEST.streamlink),
|
||||
ffmpeg: await getManagedToolInstaller('ffmpeg').status(APPLICATION_TOOL_MANIFEST.ffmpeg)
|
||||
streamlink: {
|
||||
...(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
|
||||
// ==========================================
|
||||
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> {
|
||||
refreshBundledToolPaths();
|
||||
|
||||
@@ -468,7 +501,7 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
return false;
|
||||
return fallbackToRunnableStreamlink(current.command, versionArgs);
|
||||
}
|
||||
|
||||
_appendDebugLog('streamlink-install-start');
|
||||
@@ -476,7 +509,7 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
|
||||
const result = await getManagedToolInstaller('streamlink').install(manifest);
|
||||
if (!result.success) {
|
||||
_appendDebugLog('streamlink-install-failed', { error: result.error, status: result.status });
|
||||
return false;
|
||||
return fallbackToRunnableStreamlink(current.command, versionArgs);
|
||||
}
|
||||
|
||||
refreshBundledToolPaths(true);
|
||||
@@ -489,10 +522,10 @@ export async function ensureStreamlinkInstalled(): Promise<boolean> {
|
||||
cacheVerifiedStreamlinkCommand(cmd.command, installedVersionArgs);
|
||||
}
|
||||
_appendDebugLog('streamlink-install-finished', { works, command: cmd.command, prefixArgs: cmd.prefixArgs });
|
||||
return works;
|
||||
return works || fallbackToRunnableStreamlink(current.command, versionArgs);
|
||||
} catch (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') {
|
||||
return false;
|
||||
return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
|
||||
}
|
||||
|
||||
_appendDebugLog('ffmpeg-install-start');
|
||||
@@ -522,7 +555,7 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
|
||||
const result = await getManagedToolInstaller('ffmpeg').install(manifest);
|
||||
if (!result.success) {
|
||||
_appendDebugLog('ffmpeg-install-failed', { error: result.error, status: result.status });
|
||||
return false;
|
||||
return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
|
||||
}
|
||||
|
||||
refreshBundledToolPaths(true);
|
||||
@@ -534,9 +567,9 @@ export async function ensureFfmpegInstalled(): Promise<boolean> {
|
||||
cacheVerifiedFfmpegCommands(newFfmpegPath, newFfprobePath);
|
||||
}
|
||||
_appendDebugLog('ffmpeg-install-finished', { works, ffmpeg: newFfmpegPath, ffprobe: newFfprobePath });
|
||||
return works;
|
||||
return works || fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
|
||||
} catch (e) {
|
||||
_appendDebugLog('ffmpeg-install-failed', String(e));
|
||||
return false;
|
||||
return fallbackToRunnableFfmpeg(ffmpegPath, ffprobePath);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user