9 Commits

Author SHA1 Message Date
debont80 e8f0b83a7e chore: license the project under MIT
Replace the placeholder "UNLICENSED" with the MIT License so the now-public
repo is genuinely free and open source. Adds a LICENSE file and documents the
app-vs-bundled-binary licensing split in the README (yt-dlp/FFmpeg/aria2 are
invoked as separate executables, so they remain under their own licenses).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:09:52 -04:00
debont80 a7edde8a32 Merge pull request 'docs: add README with feature overview and screenshots' (#14) from docs/readme-screenshots into main 2026-07-10 13:18:22 -04:00
debont80 4204483792 docs: add README with feature overview and screenshots
Give the now-public repo a proper front page: pitch, grouped feature
list, light/dark screenshots of Downloads/Library/History/Settings,
download + build-from-source instructions, and a note on the bundled
yt-dlp/FFmpeg/aria2 licenses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:15:59 -04:00
debont80 94ef814fa7 chore: bump version to 0.7.2 2026-07-10 10:43:14 -04:00
debont80 78a168ca5c Merge pull request 'chore(config): point the updater at the now-public AeroFetch repo' (#13) from chore/consolidate-update-source into main
Reviewed-on: #13
2026-07-10 10:41:28 -04:00
debont80 3b9dd6788d chore(config): point the updater at the now-public AeroFetch repo
The source repo is public now, so the separate releases-only repo
(AeroFetch-releases) is no longer needed to keep the updater tokenless.
Retarget UPDATE_REPO from AeroFetch-releases back to AeroFetch and refresh the
config doc comment. The dedicated read-only baked-token service account
(aerofetch-updater) has been deleted and its token revoked, so nothing reads a
token any more; the .gitignore comment for the retired .update-token file is
updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:39:12 -04:00
debont80 ccee9b7d3b chore: bump version to 0.7.1 2026-07-09 15:50:14 -04:00
debont80 af9c285864 Merge pull request 'fix(sync): run watched sync when a scheduled --sync hits the running instance' (#12) from fix/scheduled-sync-running-instance into main
Reviewed-on: #12
2026-07-09 15:48:31 -04:00
debont80 87e16ca28e fix(sync): run watched sync when a scheduled --sync hits the running instance
The Windows daily-sync task launches `AeroFetch.exe --sync`, but the single-
instance lock routes that relaunch to the `second-instance` handler whenever
AeroFetch is already open. The handler only focused the window and looked for a
link -- it never checked `isSyncLaunch(argv)` -- so the sync never ran. The daily
auto-download of new watched-source uploads therefore did nothing for minimize-
to-tray / always-on users (and the relaunch stole window focus each day).

Add a `runWatchedSync` main->renderer push: on a `--sync` relaunch the handler
now nudges the renderer to run syncWatched() instead of focusing, so the daily
sync fires whether AeroFetch was closed or already running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:45:35 -04:00
16 changed files with 151 additions and 14 deletions
+2 -2
View File
@@ -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)
+21
View File
@@ -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.
+91
View File
@@ -0,0 +1,91 @@
<div align="center">
# AeroFetch
**A clean, native-feeling video &amp; 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.
![AeroFetch downloads screen](docs/screenshots/downloads-dark.png)
</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 &amp; quality control** — pick a resolution/codec (H.264 / VP9 / AV1), or extract audio to MP3, M4A, Opus, FLAC, WAV, or AAC.
- **Playlists &amp; 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 &amp; 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 &amp; 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) |
| :---: | :---: |
| ![Downloads, light theme](docs/screenshots/downloads-light.png) | ![Downloads, dark theme](docs/screenshots/downloads-dark.png) |
| Library — watched channels &amp; playlists | History — searchable &amp; filterable |
| :---: | :---: |
| ![Library](docs/screenshots/library-dark.png) | ![History](docs/screenshots/history-dark.png) |
<div align="center">
**Settings — grouped &amp; searchable**
![Settings](docs/screenshots/settings-light.png)
</div>
## Download &amp; 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

+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 ------------------------------------------------
+9
View File
@@ -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)
+8
View File
@@ -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. */
+1
View File
@@ -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 () => [
+5 -2
View File
@@ -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: {
+3
View File
@@ -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',