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 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 `. * * 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) }