Compare commits
10 Commits
v0.7.0
...
614f56086f
| Author | SHA1 | Date | |
|---|---|---|---|
| 614f56086f | |||
| e8f0b83a7e | |||
| a7edde8a32 | |||
| 4204483792 | |||
| 94ef814fa7 | |||
| 78a168ca5c | |||
| 3b9dd6788d | |||
| ccee9b7d3b | |||
| af9c285864 | |||
| 87e16ca28e |
+2
-2
@@ -10,8 +10,8 @@ dist
|
||||
|
||||
# Secrets — never commit
|
||||
.gitea-token
|
||||
# Retired: no longer read by the build (baked update token removed), but the
|
||||
# local file still holds a live token until it's revoked — keep it ignored.
|
||||
# Retired: the baked update token was removed from the build and its service
|
||||
# account revoked; kept ignored so a stray local .update-token can never be committed.
|
||||
.update-token
|
||||
|
||||
# VS Code user settings (workspace settings/.vscode/launch.json are committed)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Devon Bontrager
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,91 @@
|
||||
<div align="center">
|
||||
|
||||
# AeroFetch
|
||||
|
||||
**A clean, native-feeling video & audio downloader for Windows — a friendly front-end for [yt-dlp](https://github.com/yt-dlp/yt-dlp).**
|
||||
|
||||
Paste a link, pick a quality, and go. No command line, no Python setup, no chasing down FFmpeg — everything is bundled.
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What it is
|
||||
|
||||
AeroFetch wraps the power of yt-dlp in a modern Windows desktop app. It handles single videos, whole playlists, and watched channels; it can grab audio-only, embed thumbnails and metadata, trim clips, skip sponsor segments, and keep a library of channels in sync — all from a point-and-click interface built with Fluent UI.
|
||||
|
||||
It ships as a **regular installer** *and* a **single portable `.exe`** (no admin rights required), with `yt-dlp`, `FFmpeg`, and `aria2c` bundled in — so it runs on a fresh machine with nothing else installed.
|
||||
|
||||
## Features
|
||||
|
||||
- **One-paste downloads** — drop in a video or playlist URL and press Enter. Clipboard detection can offer the link automatically.
|
||||
- **Format & quality control** — pick a resolution/codec (H.264 / VP9 / AV1), or extract audio to MP3, M4A, Opus, FLAC, WAV, or AAC.
|
||||
- **Playlists & channels** — select which videos to grab, or add a channel to your **Library** and let AeroFetch watch it for new uploads.
|
||||
- **Post-processing** — embed thumbnails, metadata, chapters, and subtitles; **SponsorBlock** to skip or mark sponsor segments; trim to a start/end timestamp.
|
||||
- **Queue that behaves** — concurrent downloads with a configurable cap, pause/resume, cancel, retry-all, and live speed/ETA.
|
||||
- **Library & sync** — organized per-source folders, scheduled daily sync, retention rules, and quality-upgrade selectors.
|
||||
- **History** — searchable, filterable record of everything you've downloaded, with quick open-file / show-in-folder / re-download.
|
||||
- **Fast & convenient** — `aria2c` acceleration, a `Ctrl+K` command palette, light / dark / auto themes, and native Windows notifications.
|
||||
- **Portable-friendly** — runs from a USB stick or Downloads folder without installing or needing admin rights.
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Downloads (light) | Downloads (dark) |
|
||||
| :---: | :---: |
|
||||
|  |  |
|
||||
|
||||
| Library — watched channels & playlists | History — searchable & filterable |
|
||||
| :---: | :---: |
|
||||
|  |  |
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Settings — grouped & searchable**
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Download & install
|
||||
|
||||
Grab the latest build from the [**Releases**](https://gitea.netbird.zimspace.uk:5938/debont80/AeroFetch/releases) page:
|
||||
|
||||
- **`AeroFetch Setup <version>.exe`** — standard installer. Installs per-user (no admin), adds a Start-menu shortcut, and auto-updates.
|
||||
- **`AeroFetch-<version>-portable.exe`** — single self-contained executable. No install, no admin; keeps its data in an `AeroFetch-data` folder beside the `.exe`. Ideal for locked-down or shared PCs.
|
||||
|
||||
Each release also publishes a `.sha256` next to every executable so you can verify the download.
|
||||
|
||||
> Because the executables aren't code-signed, Windows SmartScreen may warn on first launch — choose **More info → Run anyway**. No administrator rights are required either way.
|
||||
|
||||
## Build from source
|
||||
|
||||
Requires **Node.js 24.x**.
|
||||
|
||||
```bash
|
||||
git clone https://gitea.netbird.zimspace.uk:5938/debont80/AeroFetch.git
|
||||
cd AeroFetch
|
||||
npm install
|
||||
|
||||
npm run dev # launch the app in development
|
||||
npm run ui # preview just the renderer in a browser (http://localhost:5173)
|
||||
npm test # run the test suite
|
||||
npm run build:win # produce the installer + portable .exe in dist/
|
||||
```
|
||||
|
||||
The bundled `yt-dlp.exe`, `ffmpeg.exe`, `ffprobe.exe`, and `aria2c.exe` live in `resources/bin/` and are packaged automatically.
|
||||
|
||||
## Tech stack
|
||||
|
||||
Built with **Electron** + **electron-vite** + **electron-builder**, a **React 19** / **TypeScript** renderer, **Fluent UI**, **Zustand** for state, and **Zod** for boundary validation. yt-dlp runs only in the main process; the renderer talks to it over a typed IPC bridge with context isolation enabled.
|
||||
|
||||
## A note on responsible use
|
||||
|
||||
AeroFetch is a tool for downloading media you have the right to download — your own uploads, content offered for download, or material whose license permits it. Respect the terms of service of the sites you use and the rights of content creators.
|
||||
|
||||
## License
|
||||
|
||||
AeroFetch's own source code is released under the [**MIT License**](LICENSE) — free to use, modify, and redistribute.
|
||||
|
||||
It also bundles third-party binaries that remain under their own licenses: **yt-dlp** (Unlicense), **FFmpeg** (LGPL/GPL), and **aria2** (GPL). AeroFetch invokes these as separate executables rather than linking against them, so they are distributed as an aggregate; their licenses apply to the respective files in `resources/bin/`.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aerofetch",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aerofetch",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "aerofetch",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"description": "A yt-dlp frontend for Windows",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "AeroFetch",
|
||||
"license": "UNLICENSED",
|
||||
"license": "MIT",
|
||||
"homepage": "https://gitea.netbird.zimspace.uk:5938/debont80/AeroFetch",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
+7
-6
@@ -4,15 +4,16 @@
|
||||
* timeouts, caps, tickers) and from user settings (settings.ts). Retarget a
|
||||
* fork's update source by editing these three values.
|
||||
*
|
||||
* The update source is a PUBLIC, releases-only repo — it holds no source, just
|
||||
* the published installers — so anonymous reads work and the updater sends no
|
||||
* auth at all. Keeping releases separate from the (private) source repo is what
|
||||
* lets the client stay tokenless: there is no secret in the bundle to leak. A
|
||||
* private fork retargets its updater by editing these constants and rebuilding.
|
||||
* The update source is the main AeroFetch repo itself, made PUBLIC — so anonymous
|
||||
* reads work and the updater sends no auth at all; there is no secret in the bundle
|
||||
* to leak. (A prior design split releases into a separate releases-only repo so the
|
||||
* source could stay private; that's retired now that the source repo is public
|
||||
* itself — one fewer moving part.) A private fork retargets its updater by editing
|
||||
* these constants and rebuilding.
|
||||
*/
|
||||
export const UPDATE_HOST = 'https://gitea.netbird.zimspace.uk:5938'
|
||||
export const UPDATE_OWNER = 'debont80'
|
||||
export const UPDATE_REPO = 'AeroFetch-releases'
|
||||
export const UPDATE_REPO = 'AeroFetch'
|
||||
export const RELEASE_API = `${UPDATE_HOST}/api/v1/repos/${UPDATE_OWNER}/${UPDATE_REPO}/releases/latest`
|
||||
|
||||
// --- ffmpeg dependency source ------------------------------------------------
|
||||
|
||||
@@ -260,6 +260,15 @@ if (isPrimaryInstance) {
|
||||
// link it carried and hand it to the window we already have.
|
||||
app.on('second-instance', (_e, argv) => {
|
||||
if (!mainWindow) return
|
||||
// A scheduled `--sync` relaunch (Task Scheduler) hit the already-running instance:
|
||||
// run the watched-source sync in this window instead of no-opping. Without this the
|
||||
// daily auto-download never fires while AeroFetch is open — e.g. minimize-to-tray
|
||||
// users. Deliberately does NOT focus the window, so the daily sync stays unobtrusive
|
||||
// (mirrors the fresh-launch `--sync` showInactive path in createWindow).
|
||||
if (isSyncLaunch(argv)) {
|
||||
mainWindow.webContents.send(IpcChannels.runWatchedSync)
|
||||
return
|
||||
}
|
||||
focusWindow(mainWindow)
|
||||
const incomingUrl = extractIncomingUrl(argv)
|
||||
if (incomingUrl) mainWindow.webContents.send(IpcChannels.externalUrl, incomingUrl)
|
||||
|
||||
@@ -279,6 +279,14 @@ const api = {
|
||||
return () => ipcRenderer.removeListener(IpcChannels.indexProgress, listener)
|
||||
},
|
||||
|
||||
/** Subscribe to the "run watched-source sync now" nudge a scheduled `--sync` relaunch
|
||||
* sends when it hits the already-running instance. Returns an unsubscribe function. */
|
||||
onRunWatchedSync: (cb: () => void): (() => void) => {
|
||||
const listener = (): void => cb()
|
||||
ipcRenderer.on(IpcChannels.runWatchedSync, listener)
|
||||
return () => ipcRenderer.removeListener(IpcChannels.runWatchedSync, listener)
|
||||
},
|
||||
|
||||
// --- Built-in yt-dlp terminal (Phase N) ---
|
||||
|
||||
/** Run the bundled yt-dlp with raw args; output streams via onTerminalOutput. */
|
||||
|
||||
@@ -271,6 +271,7 @@ export const mockApi: Api = {
|
||||
onSystemThemeUpdate: () => () => {},
|
||||
openHighContrastSettings: async () => {},
|
||||
onExternalUrl: () => () => {},
|
||||
onRunWatchedSync: () => () => {},
|
||||
// Media-manager sources — a seeded channel so the (Phase H) Library view has
|
||||
// demo data in this browser-only preview.
|
||||
listSources: async () => [
|
||||
|
||||
@@ -485,13 +485,16 @@ if (!PREVIEW) {
|
||||
.listSources()
|
||||
.then((sources) => {
|
||||
useSources.setState({ sources })
|
||||
// If any source is watched, kick off a sync shortly after launch (covers the
|
||||
// scheduled `--sync` launch and a normal launch alike).
|
||||
// If any source is watched, kick off a sync shortly after launch (covers a fresh
|
||||
// `--sync` launch and a normal launch alike).
|
||||
if (sources.some((s) => s.watched)) {
|
||||
setTimeout(() => useSources.getState().syncWatched(), 1500)
|
||||
}
|
||||
})
|
||||
.catch(logError('startup listSources'))
|
||||
// A scheduled `--sync` relaunch that hit the already-running instance nudges us here,
|
||||
// so the daily sync fires even when AeroFetch was already open (e.g. tray mode).
|
||||
window.api.onRunWatchedSync?.(() => useSources.getState().syncWatched())
|
||||
window.api.onIndexProgress((p: IndexProgress) => {
|
||||
useSources.setState({
|
||||
indexing: {
|
||||
|
||||
@@ -105,6 +105,9 @@ export const IpcChannels = {
|
||||
scheduledSyncSet: 'sources:scheduled-sync-set',
|
||||
/** main → renderer push channel for live indexing progress */
|
||||
indexProgress: 'sources:index-progress',
|
||||
/** main → renderer: a scheduled `--sync` relaunch hit the running instance — run the
|
||||
* watched-source sync here instead of no-opping (Phase J) */
|
||||
runWatchedSync: 'sources:run-sync',
|
||||
// --- Built-in yt-dlp terminal (Phase N) ---
|
||||
terminalRun: 'terminal:run',
|
||||
terminalCancel: 'terminal:cancel',
|
||||
|
||||
Reference in New Issue
Block a user