Initial commit: AeroFetch — yt-dlp/YouTube downloader for Windows

Electron + React (Fluent UI) desktop frontend for yt-dlp:
- Download queue with live progress, concurrency cap, cancel/retry
- Format/quality picker via yt-dlp probe; audio extraction to MP3
- Settings + history persistence (electron-store / JSON)
- Clipboard link auto-detect; persisted light/dark theme
- NSIS + portable packaging (binaries bundled at build time, not in git)

UI: "Studio" sidebar layout with a toffee-brown theme (light + neutral dark).
Dropdowns use a native <select> and tooltips a CSS-only Hint, to avoid a
dev-machine GPU overlay flicker/blank issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 18:17:41 -04:00
commit 1a2270c95e
39 changed files with 11351 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { Api } from './index'
declare global {
interface Window {
electron: ElectronAPI
api: Api
}
}
+75
View File
@@ -0,0 +1,75 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import {
IpcChannels,
type YtdlpVersionResult,
type ProbeResult,
type StartDownloadOptions,
type StartDownloadResult,
type DownloadEvent,
type Settings,
type HistoryEntry
} from '@shared/ipc'
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
const api = {
getYtdlpVersion: (): Promise<YtdlpVersionResult> =>
ipcRenderer.invoke(IpcChannels.ytdlpVersion),
probe: (url: string): Promise<ProbeResult> => ipcRenderer.invoke(IpcChannels.probe, url),
startDownload: (opts: StartDownloadOptions): Promise<StartDownloadResult> =>
ipcRenderer.invoke(IpcChannels.downloadStart, opts),
cancelDownload: (id: string): Promise<void> =>
ipcRenderer.invoke(IpcChannels.downloadCancel, id),
getDefaultFolder: (): Promise<string> => ipcRenderer.invoke(IpcChannels.defaultFolder),
chooseFolder: (): Promise<string | null> => ipcRenderer.invoke(IpcChannels.chooseFolder),
openPath: (path: string): Promise<string> => ipcRenderer.invoke(IpcChannels.openPath, path),
showInFolder: (path: string): Promise<void> =>
ipcRenderer.invoke(IpcChannels.showInFolder, path),
readClipboard: (): Promise<string> => ipcRenderer.invoke(IpcChannels.clipboardRead),
getSettings: (): Promise<Settings> => ipcRenderer.invoke(IpcChannels.settingsGet),
setSettings: (partial: Partial<Settings>): Promise<Settings> =>
ipcRenderer.invoke(IpcChannels.settingsSet, partial),
listHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IpcChannels.historyList),
addHistory: (entry: HistoryEntry): Promise<HistoryEntry[]> =>
ipcRenderer.invoke(IpcChannels.historyAdd, entry),
removeHistory: (id: string): Promise<HistoryEntry[]> =>
ipcRenderer.invoke(IpcChannels.historyRemove, id),
clearHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IpcChannels.historyClear),
/** Subscribe to live download events. Returns an unsubscribe function. */
onDownloadEvent: (cb: (ev: DownloadEvent) => void): (() => void) => {
const listener = (_e: IpcRendererEvent, ev: DownloadEvent): void => cb(ev)
ipcRenderer.on(IpcChannels.downloadEvent, listener)
return () => ipcRenderer.removeListener(IpcChannels.downloadEvent, listener)
}
}
export type Api = typeof api
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (defined in index.d.ts)
window.electron = electronAPI
// @ts-ignore (defined in index.d.ts)
window.api = api
}