Files
AeroFetch/src/main/binaries.ts
T
debont80 eb53de2ea5 feat(setup): fetch ffmpeg on first run instead of bundling it
Ship a smaller installer that no longer carries ffmpeg.exe/ffprobe.exe
(the bulk of its size). On first run they are downloaded from a pinned
upstream archive (gyan 8.1.2), verified against a pinned SHA-256, and
unpacked with the OS tar.exe into the managed userData/bin dir -- the
same place as the self-updating yt-dlp, so they survive app updates and
portable re-extraction. A hard onboarding gate blocks the app until they
are present, with a "locate existing ffmpeg" folder-picker fallback for
offline machines and a Repair action in Settings > About.

The updater's streaming/checksum/redirect/idle-timeout download loop is
extracted to lib/verifiedDownload.streamVerifiedFile and shared by both
the app-installer download and the ffmpeg fetch; the updater's public
behaviour and error strings are unchanged (its boundary tests still pass).
No new npm dependency: extraction uses the System32 bsdtar resolved by
absolute path (audit F3).

Note: this commit also carries pre-existing, in-progress aria2c/network
and updater-token work that was already uncommitted in the working tree
and is entangled with the above in shared files (updater.ts, ipc.ts,
preload/index.ts, mockApi.ts, shared/ipc.ts, plus the settings/network
files and aria2c.exe). It was not cleanly separable by path, so it is
included here rather than split out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:02:13 -04:00

131 lines
5.4 KiB
TypeScript

import { app, nativeImage, type NativeImage } from 'electron'
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
/**
* Resolves the directory holding the bundled binaries (yt-dlp.exe, ffmpeg.exe).
*
* In dev they live in the repo at resources/bin/.
* In a packaged build, electron-builder's extraResources copies them to
* <resources>/bin (see electron-builder.yml), reachable via process.resourcesPath.
*/
export function getBinDir(): string {
return is.dev ? join(app.getAppPath(), 'resources', 'bin') : join(process.resourcesPath, 'bin')
}
/**
* The app icon (.ico), for the system tray. In dev it's the repo's build/icon.ico;
* in a packaged build, electron-builder's extraResources copies it to
* <resources>/icon.ico (see electron-builder.yml).
*/
export function getAppIconPath(): string {
return is.dev
? join(app.getAppPath(), 'build', 'icon.ico')
: join(process.resourcesPath, 'icon.ico')
}
let appIconImage: NativeImage | null = null
/**
* The app icon as a cached NativeImage, for OS notifications (W13/L127). Loaded
* once from getAppIconPath(); a missing icon yields an empty image, which
* Notification treats as "no icon" (the OS default) rather than erroring. This
* gives completion/background toasts the real brand glyph — notably on the
* portable build, which has no installed AUMID shortcut icon for Windows to use.
*/
export function getAppIconImage(): NativeImage {
// Cache only a valid image so a transient read miss isn't latched forever; a
// genuinely-missing icon just re-reads (cheap — notifications are infrequent).
if (!appIconImage || appIconImage.isEmpty()) {
appIconImage = nativeImage.createFromPath(getAppIconPath())
}
return appIconImage
}
/**
* AeroFetch keeps its OWN writable copies of the tool binaries under userData/bin,
* separate from anything in the read-only bundle (resources/bin). This managed dir is
* what actually gets spawned, and it survives an app reinstall or portable re-extraction
* (which only ever touch the bundle) — so a self-updated yt-dlp is never rolled back to
* a stale bundled version, and the first-run-downloaded ffmpeg/ffprobe (which the
* installer no longer ships at all) persist across updates.
*
* - yt-dlp.exe — seeded from the bundled copy, then self-updates (`--update-to`).
* See ensureManagedYtdlp in ytdlp.ts.
* - ffmpeg.exe / ffprobe.exe — fetched from upstream on first run (ffmpegSetup.ts);
* in dev, ensureManagedFfmpeg seeds them from resources/bin so `npm run dev` needs
* no download. yt-dlp finds them here via `--ffmpeg-location <getFfmpegDir()>`.
*
* aria2c stays in getBinDir(): it's optional, bundled, and passed by absolute path.
*/
function getManagedBinDir(): string {
return join(app.getPath('userData'), 'bin')
}
/** The bundled, read-only yt-dlp.exe seeded into the managed copy when missing. */
export function getBundledYtdlpPath(): string {
return join(getBinDir(), 'yt-dlp.exe')
}
/** The managed (spawned + auto-updated) yt-dlp.exe under userData. */
export function getYtdlpPath(): string {
return join(getManagedBinDir(), 'yt-dlp.exe')
}
/**
* The managed ffmpeg.exe under userData/bin. The installer no longer bundles ffmpeg
* (it's the bulk of the download); ffmpegSetup.ts fetches it on first run, and in dev
* ensureManagedFfmpeg seeds it from getBundledFfmpegPath(). yt-dlp locates it (and
* ffprobe) via `--ffmpeg-location getFfmpegDir()`.
*/
export function getFfmpegPath(): string {
return join(getManagedBinDir(), 'ffmpeg.exe')
}
/**
* The managed ffprobe.exe under userData/bin. yt-dlp finds it via --ffmpeg-location
* (so the app never spawns it directly), but it must be present, or duration-aware
* post-processing (SponsorBlock-remove, --force-keyframes-at-cuts, --split-chapters)
* fails. startDownload / the setup gate assert its presence up front.
*/
export function getFfprobePath(): string {
return join(getManagedBinDir(), 'ffprobe.exe')
}
/** Directory holding the managed ffmpeg/ffprobe, handed to yt-dlp as --ffmpeg-location. */
export function getFfmpegDir(): string {
return getManagedBinDir()
}
/**
* Dev-only read seeds for ffmpeg/ffprobe in resources/bin, copied into the managed dir
* by ensureManagedFfmpeg when present. Absent in a shipped installer (excluded from the
* bundle in electron-builder.yml), where first-run download provides them instead.
*/
export function getBundledFfmpegPath(): string {
return join(getBinDir(), 'ffmpeg.exe')
}
export function getBundledFfprobePath(): string {
return join(getBinDir(), 'ffprobe.exe')
}
/** User-facing error shown by download/probe/index/update when yt-dlp.exe is missing. */
export const YTDLP_MISSING_MSG =
'yt-dlp.exe is missing. Open Settings → Software update to re-download it.'
/** Optional bundled external downloader; absent unless dropped into resources/bin. */
export function getAria2cPath(): string {
return join(getBinDir(), 'aria2c.exe')
}
/**
* Absolute path to a Windows system executable (e.g. taskkill.exe, schtasks.exe).
*
* SECURITY (audit F3): system tools are resolved by full path under System32
* rather than by bare name, so a same-named binary planted in the current
* working directory or earlier on PATH can't be invoked in their place — a real
* risk for the portable build, which runs from user-writable locations.
*/
export function getSystem32Path(exe: string): string {
return join(process.env.SystemRoot || 'C:\\Windows', 'System32', exe)
}