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 * /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 * /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 copy of yt-dlp.exe under userData, separate * from the read-only bundled seed in resources/bin. The managed copy is what * actually gets spawned and self-updated (`--update-to`), so an app reinstall or * portable re-extraction — which only ever replace the bundled seed — can never * roll a freshly-updated yt-dlp back to a stale version. See ensureManagedYtdlp * in ytdlp.ts, which seeds this from getBundledYtdlpPath() on first run. * * ffmpeg/ffprobe/aria2c stay in getBinDir(): they're not self-updating and * yt-dlp finds them via `--ffmpeg-location `. */ 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') } export function getFfmpegPath(): string { return join(getBinDir(), 'ffmpeg.exe') } /** * yt-dlp finds ffprobe via --ffmpeg-location (the bin dir), 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. This * accessor exists so startDownload can assert its presence up front. */ export function getFfprobePath(): 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) }