From 45a7a889a2dd19cdcd82128ff1f020ce6f2e1882 Mon Sep 17 00:00:00 2001 From: BurntToasters <61037367+BurntToasters@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:08:03 -0700 Subject: [PATCH 1/7] b1 --- CHANGELOG.md | 17 +-- src/main/downloader.ts | 10 +- src/main/platform.ts | 132 +++++++++++++++++------ src/tests/platform.bundledFfmpeg.test.ts | 14 ++- src/tests/platform.env.test.ts | 45 ++++---- 5 files changed, 151 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7827e96..5c04051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,13 @@ - +> 🅱️ This is a Beta build. # ⬇️ Downloads -| Windows | macOS | Linux | -| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-arm64.AppImage) | -|
| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-arm64.deb) | -| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-aarch64.rpm) | +| Windows | macOS | Linux | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-arm64.AppImage) | +|
| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-arm64.deb) | +| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.3-beta.1/ROSI-Linux-aarch64.rpm) | > [!IMPORTANT] > The `.sig` files in this repo are NOT normal GPG signatures — they are for ROSI's built-in updater to verify the integrity of updates before downloading and installing. @@ -30,6 +29,10 @@ --- +## Changes in `v4.1.3-beta.1:` + +- **FFMPEG:** Ensure bundled ffprobe is executable and always pass the bundled helper directory to yt-dlp so metadata extraction can find it. + ## Changes in `v4.1.2:` - **macOS:** Addressed a codesigning issue with yt-dlp/ffmpeg on macOS builds of ROSI. diff --git a/src/main/downloader.ts b/src/main/downloader.ts index fb33807..8dee011 100644 --- a/src/main/downloader.ts +++ b/src/main/downloader.ts @@ -3,7 +3,13 @@ import * as fs from 'fs'; import sanitize from 'sanitize-filename'; import { dialog } from 'electron'; import log from 'electron-log/main.js'; -import { spawnWithEnv, getEffectiveFfmpegPath, ytdlpBinary, isWindows } from './platform'; +import { + spawnWithEnv, + getEffectiveFfmpegPath, + resolveFfmpegLocationForYtdlp, + ytdlpBinary, + isWindows, +} from './platform'; import { killChildProcess } from './processKill'; import { loadSettings, recordDownload } from './settings'; import { @@ -471,7 +477,7 @@ export function startDownload( const settings = loadSettings(); const effectiveSettings: Settings = { ...settings }; const ffmpegCommand = getEffectiveFfmpegPath(options.ffmpegPath || settings.ffmpegPath); - const ffmpegLocation = ffmpegCommand !== 'ffmpeg' ? path.dirname(ffmpegCommand) : null; + const ffmpegLocation = resolveFfmpegLocationForYtdlp(options.ffmpegPath || settings.ffmpegPath); if (options.convertFormat !== undefined) { if (typeof options.convertFormat === 'string' && options.convertFormat.trim() !== '') { diff --git a/src/main/platform.ts b/src/main/platform.ts index 4028a45..931bc37 100644 --- a/src/main/platform.ts +++ b/src/main/platform.ts @@ -142,6 +142,42 @@ export function resolveFfmpegPath(customPath: unknown): string | null { return candidate; } +function copyHelperBinaryToTemp(sourcePath: string): string { + const binaryName = path.basename(sourcePath); + const isFlatpak = Boolean(process.env.FLATPAK_ID); + const binDir = isFlatpak + ? path.join(app.getPath('userData'), '.bin') + : path.join(app.getPath('temp'), 'rosi-bin'); + if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true }); + const tmpBin = path.join(binDir, binaryName); + fs.copyFileSync(sourcePath, tmpBin); + fs.chmodSync(tmpBin, 0o755); + + const probeName = isWindows ? 'ffprobe.exe' : 'ffprobe'; + const probeSrc = path.join(path.dirname(sourcePath), probeName); + if (fs.existsSync(probeSrc)) { + const tmpProbe = path.join(binDir, probeName); + fs.copyFileSync(probeSrc, tmpProbe); + fs.chmodSync(tmpProbe, 0o755); + } + + return tmpBin; +} + +function ensureFfmpegHelperBinaries(dir: string): string | null { + const ext = isWindows ? '.exe' : ''; + const ffmpegPath = path.join(dir, `ffmpeg${ext}`); + const ffprobePath = path.join(dir, `ffprobe${ext}`); + let effectiveFfmpeg: string | null = null; + if (fs.existsSync(ffmpegPath)) { + effectiveFfmpeg = ensureExecutable(ffmpegPath); + } + if (fs.existsSync(ffprobePath)) { + ensureExecutable(ffprobePath); + } + return effectiveFfmpeg; +} + function ensureExecutable(filePath: string): string { if (isWindows) return filePath; @@ -159,16 +195,7 @@ function ensureExecutable(filePath: string): string { fs.accessSync(filePath, fs.constants.X_OK); } catch { try { - const binaryName = path.basename(filePath); - const isFlatpak = Boolean(process.env.FLATPAK_ID); - const binDir = isFlatpak - ? path.join(app.getPath('userData'), '.bin') - : path.join(app.getPath('temp'), 'rosi-bin'); - if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true }); - const tmpBin = path.join(binDir, binaryName); - fs.copyFileSync(filePath, tmpBin); - fs.chmodSync(tmpBin, 0o755); - return tmpBin; + return copyHelperBinaryToTemp(filePath); } catch (copyErr) { log.error( `Failed to copy ffmpeg to temp for execution: ${(copyErr as Error).message}` @@ -218,10 +245,12 @@ export function resolveBundledFfmpegPath(): string | null { const ext = isWindows ? '.exe' : ''; const bundledPath = path.join(bundledDir, `ffmpeg${ext}`); if (fs.existsSync(bundledPath)) { - const effectivePath = ensureExecutable(bundledPath); - cachedBundledFfmpegPath = effectivePath; - log.info(`Resolved bundled ffmpeg at: ${effectivePath}`); - return effectivePath; + const effectivePath = ensureFfmpegHelperBinaries(bundledDir); + if (effectivePath) { + cachedBundledFfmpegPath = effectivePath; + log.info(`Resolved bundled ffmpeg at: ${effectivePath}`); + return effectivePath; + } } } @@ -256,36 +285,69 @@ export function getEffectiveFfmpegPath(customPath?: string | null): string { return 'ffmpeg'; } +export function resolveFfmpegLocationForYtdlp(customPath?: string | null): string | null { + const resolved = resolveFfmpegPath(customPath); + if (resolved) { + const dir = path.dirname(resolved); + ensureFfmpegHelperBinaries(dir); + return dir; + } + + const bundledDir = getBundledFfmpegDir(); + if (!bundledDir) return null; + + const ext = isWindows ? '.exe' : ''; + if (!fs.existsSync(path.join(bundledDir, `ffmpeg${ext}`))) return null; + + ensureFfmpegHelperBinaries(bundledDir); + return bundledDir; +} + export function hasBundledFfmpeg(): boolean { return resolveBundledFfmpegPath() !== null; } export function verifyBundledFfmpeg(): void { - const ffmpegPath = resolveBundledFfmpegPath(); - if (!ffmpegPath) { + const bundledDir = getBundledFfmpegDir(); + if (!bundledDir) { log.info('No bundled ffmpeg found; will rely on system ffmpeg.'); return; } - try { - const proc = spawnWithEnv(ffmpegPath, ['-version'], { shell: false }); - let output = ''; - proc.stdout?.on('data', (data: Buffer) => { - if (output.length < 512) output += data.toString(); - }); - proc.on('close', (code: number | null) => { - if (code === 0 && output) { - const firstLine = output.split('\n')[0]?.trim() ?? ''; - log.info(`Bundled ffmpeg verified: ${firstLine}`); - } else { - log.warn(`Bundled ffmpeg at ${ffmpegPath} exited with code ${code}`); - } - }); - proc.on('error', (err: Error) => { - log.warn(`Bundled ffmpeg at ${ffmpegPath} failed to execute: ${err.message}`); - }); - } catch (err) { - log.warn(`Failed to verify bundled ffmpeg: ${(err as Error).message}`); + ensureFfmpegHelperBinaries(bundledDir); + + const ext = isWindows ? '.exe' : ''; + const helpers = [ + { label: 'ffmpeg', filePath: path.join(bundledDir, `ffmpeg${ext}`) }, + { label: 'ffprobe', filePath: path.join(bundledDir, `ffprobe${ext}`) }, + ]; + + for (const helper of helpers) { + if (!fs.existsSync(helper.filePath)) { + log.warn(`Bundled ${helper.label} not found at ${helper.filePath}`); + continue; + } + + try { + const proc = spawnWithEnv(helper.filePath, ['-version'], { shell: false }); + let output = ''; + proc.stdout?.on('data', (data: Buffer) => { + if (output.length < 512) output += data.toString(); + }); + proc.on('close', (code: number | null) => { + if (code === 0 && output) { + const firstLine = output.split('\n')[0]?.trim() ?? ''; + log.info(`Bundled ${helper.label} verified: ${firstLine}`); + } else { + log.warn(`Bundled ${helper.label} at ${helper.filePath} exited with code ${code}`); + } + }); + proc.on('error', (err: Error) => { + log.warn(`Bundled ${helper.label} at ${helper.filePath} failed to execute: ${err.message}`); + }); + } catch (err) { + log.warn(`Failed to verify bundled ${helper.label}: ${(err as Error).message}`); + } } } diff --git a/src/tests/platform.bundledFfmpeg.test.ts b/src/tests/platform.bundledFfmpeg.test.ts index a8c687e..c8b71b8 100644 --- a/src/tests/platform.bundledFfmpeg.test.ts +++ b/src/tests/platform.bundledFfmpeg.test.ts @@ -85,11 +85,23 @@ describe('platform bundled ffmpeg resolution', () => { expect(platform.getEffectiveFfmpegPath('')).toBe(expectedPath); }); - it('falls back to system ffmpeg when no bundled or custom path is available', async () => { + it('resolveFfmpegLocationForYtdlp returns bundled directory for yt-dlp', async () => { + const resourcesPath = createTempResourcesPath(); + const ffmpegName = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'; + const ffprobeName = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'; + createBinary(resourcesPath, path.join('ffmpeg', ffmpegName)); + createBinary(resourcesPath, path.join('ffmpeg', ffprobeName)); + const platform = await loadPlatformModule(resourcesPath); + + expect(platform.resolveFfmpegLocationForYtdlp('')).toBe(path.join(resourcesPath, 'ffmpeg')); + }); + + it('resolveFfmpegLocationForYtdlp returns null when no bundled or custom path exists', async () => { const resourcesPath = createTempResourcesPath(); fs.mkdirSync(path.join(resourcesPath, 'ffmpeg'), { recursive: true }); const platform = await loadPlatformModule(resourcesPath); + expect(platform.resolveFfmpegLocationForYtdlp('')).toBeNull(); expect(platform.resolveBundledFfmpegPath()).toBeNull(); expect(platform.hasBundledFfmpeg()).toBe(false); expect(platform.getEffectiveFfmpegPath('')).toBe('ffmpeg'); diff --git a/src/tests/platform.env.test.ts b/src/tests/platform.env.test.ts index 9ec3e3c..c6ba81e 100644 --- a/src/tests/platform.env.test.ts +++ b/src/tests/platform.env.test.ts @@ -6,6 +6,15 @@ const initialResourcesPath = (process as NodeJS.Process & { resourcesPath?: stri const initialPlatform = process.platform; const initialArch = process.arch; const ffmpegBinaryName = process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg'; +const ffprobeBinaryName = process.platform === 'win32' ? 'ffprobe.exe' : 'ffprobe'; + +function bundledHelperExists(target: string): boolean { + const normalized = target.replace(/\\/g, '/'); + return ( + normalized.endsWith(`/ffmpeg/${ffmpegBinaryName}`) || + normalized.endsWith(`/ffmpeg/${ffprobeBinaryName}`) + ); +} interface PlatformEnvMocks { existsSyncMock: ReturnType; @@ -204,33 +213,34 @@ describe('platform env and ffmpeg verification', () => { it('verifyBundledFfmpeg logs first version line on success', async () => { const proc = createProc(); const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => - target.replace(/\\/g, '/').endsWith(`/ffmpeg/${ffmpegBinaryName}`) - ); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.spawnMock.mockReturnValue(proc); }); mod.verifyBundledFfmpeg(); proc.stdout.emit('data', Buffer.from('ffmpeg version 7.1\nmore')); proc.emit('close', 0); + proc.stdout.emit('data', Buffer.from('ffprobe version 7.1\nmore')); + proc.emit('close', 0); expect(mocks.logInfoMock).toHaveBeenCalledWith( expect.stringContaining('Bundled ffmpeg verified: ffmpeg version 7.1') ); + expect(mocks.spawnMock).toHaveBeenCalledTimes(2); }); it('verifyBundledFfmpeg logs non-zero close and process errors', async () => { const proc = createProc(); const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => - target.replace(/\\/g, '/').endsWith(`/ffmpeg/${ffmpegBinaryName}`) - ); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.spawnMock.mockReturnValue(proc); }); mod.verifyBundledFfmpeg(); proc.emit('close', 2); proc.emit('error', new Error('blocked')); + proc.emit('close', 2); + proc.emit('error', new Error('blocked')); expect(mocks.logWarnMock).toHaveBeenCalledWith(expect.stringContaining('exited with code 2')); expect(mocks.logWarnMock).toHaveBeenCalledWith( @@ -240,9 +250,7 @@ describe('platform env and ffmpeg verification', () => { it('verifyBundledFfmpeg catches spawn exceptions', async () => { const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => - target.replace(/\\/g, '/').endsWith(`/ffmpeg/${ffmpegBinaryName}`) - ); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.spawnMock.mockImplementation(() => { throw new Error('spawn blocked'); }); @@ -298,9 +306,7 @@ describe('platform env and ffmpeg verification', () => { it('chmods bundled ffmpeg on Linux when not executable', async () => { const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => - target.replace(/\\/g, '/').endsWith('/ffmpeg/ffmpeg') - ); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.statSyncMock.mockReturnValue({ isDirectory: () => false, mode: 0o644 }); }, 'linux'); @@ -308,14 +314,12 @@ describe('platform env and ffmpeg verification', () => { expect(resolved?.replace(/\\/g, '/')).toBe('/app/resources/ffmpeg/ffmpeg'); expect(mocks.chmodSyncMock).toHaveBeenCalledWith(expect.stringContaining('ffmpeg'), 0o755); + expect(mocks.chmodSyncMock).toHaveBeenCalledWith(expect.stringContaining('ffprobe'), 0o755); }); it('copies bundled ffmpeg to temp bin when chmod and access fail on Linux', async () => { const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => { - const normalized = target.replace(/\\/g, '/'); - return normalized.endsWith('/ffmpeg/ffmpeg'); - }); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.statSyncMock.mockReturnValue({ isDirectory: () => false, mode: 0o644 }); m.chmodSyncMock.mockImplementationOnce(() => { const err = new Error('readonly') as NodeJS.ErrnoException; @@ -333,17 +337,14 @@ describe('platform env and ffmpeg verification', () => { expect(mocks.mkdirSyncMock).toHaveBeenCalledWith(expect.stringContaining('rosi-bin'), { recursive: true, }); - expect(mocks.copyFileSyncMock).toHaveBeenCalled(); - expect(mocks.chmodSyncMock).toHaveBeenLastCalledWith(expect.stringContaining('ffmpeg'), 0o755); + expect(mocks.copyFileSyncMock).toHaveBeenCalledTimes(2); + expect(mocks.chmodSyncMock).toHaveBeenCalledWith(expect.stringContaining('ffprobe'), 0o755); }); it('copies bundled ffmpeg to app data bin inside Flatpak', async () => { vi.stubEnv('FLATPAK_ID', 'com.burnttoasters.rosi'); const { mod, mocks } = await loadPlatform((m) => { - m.existsSyncMock.mockImplementation((target: string) => { - const normalized = target.replace(/\\/g, '/'); - return normalized.endsWith('/ffmpeg/ffmpeg'); - }); + m.existsSyncMock.mockImplementation((target: string) => bundledHelperExists(target)); m.statSyncMock.mockReturnValue({ isDirectory: () => false, mode: 0o644 }); m.chmodSyncMock.mockImplementationOnce(() => { const err = new Error('readonly') as NodeJS.ErrnoException; From 7b9cf4a5bab445fc06ea3fe397b69c28a3268413 Mon Sep 17 00:00:00 2001 From: BurntToasters <61037367+BurntToasters@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:09:58 -0700 Subject: [PATCH 2/7] b1 --- com.burnttoasters.rosi.metainfo.xml | 2 +- package-lock.json | 12 ++++++------ package.json | 2 +- src/renderer/splash.html | 2 +- src/tests/downloader.errorPaths.test.ts | 3 +++ src/tests/downloader.fetchFormats.test.ts | 1 + src/tests/downloader.session.test.ts | 1 + 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/com.burnttoasters.rosi.metainfo.xml b/com.burnttoasters.rosi.metainfo.xml index 898c94e..ad5187e 100644 --- a/com.burnttoasters.rosi.metainfo.xml +++ b/com.burnttoasters.rosi.metainfo.xml @@ -33,7 +33,7 @@ - + diff --git a/package-lock.json b/package-lock.json index 2778ef0..3591efd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rosi", - "version": "4.1.2", + "version": "4.1.3-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rosi", - "version": "4.1.2", + "version": "4.1.3-beta.1", "license": "MPL-2.0", "dependencies": { "electron-log": "^5.3.4", @@ -954,14 +954,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", diff --git a/package.json b/package.json index 93cd983..6ce0448 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rosi", - "version": "4.1.2", + "version": "4.1.3-beta.1", "private": true, "description": "Electron GUI for yt-dlp", "desktopName": "com.burnttoasters.rosi.desktop", diff --git a/src/renderer/splash.html b/src/renderer/splash.html index b331a85..24c2df3 100644 --- a/src/renderer/splash.html +++ b/src/renderer/splash.html @@ -327,7 +327,7 @@

ROSI

Loading
-
v4.1.2
+
v4.1.3-beta.1
+ diff --git a/src/renderer/licenses-iframe.html b/src/renderer/licenses-iframe.html index 5f5a1e6..7747c0e 100644 --- a/src/renderer/licenses-iframe.html +++ b/src/renderer/licenses-iframe.html @@ -4,7 +4,7 @@ ROSI License & 3rd Party Licenses/Credits