Overview
EasyDownload runs entirely on the user's computer. No cloud, no account. Three parts work together: the interface you see, a small bridge, and a privileged core that does the real work.
Simple rule
The interface never touches the file system or runs programs. Every risky action goes through the bridge.
The Three Layers
| Layer | What it does |
|---|---|
| Interface | Shows progress, collects clicks, displays history. Built with React. |
| Bridge | A tiny typed API (window.mediaDownloader). The only door to the core. |
| Core | Owns downloads, inspection, conversions, and files. Talks to yt-dlp and FFmpeg. |
Each step is a single IPC call with a validated payload. The UI only ever sees clean progress — percent, speed, ETA — never raw tool output.
Inside the Core
The main process is split into eleven single-purpose service modules, composed in one factory with dependency injection — which is what makes the whole core unit-testable with mocked dependencies:
| Service | One job |
|---|---|
| Download Manager | Queue, state machine, concurrency (1–10), duplicate prevention |
| Media Service | URL inspection via yt-dlp, metadata normalization |
| Conversion Manager | FFmpeg conversions and audio extraction, with persisted history |
| Process Manager | Spawning and supervising child processes with UTF-8 stream decoding |
| File / Dependency / Settings / History / Inspection History / Notification Managers | Persistence and OS integration |
Every download lives in an explicit state machine — enforced by guards in the Download Manager and covered by 50+ dedicated unit tests:
How pause actually works
Resuming doesn't resurrect the old process — pausing kills it and keeps the partial file; resuming starts a fresh yt-dlp with --continue, picking up where the bytes left off.
The contract between UI and core
- Explicit channels — each operation has its own channel (
download:pause,conversion:start, …). No generic "run anything" command. - Validated payloads — zod
.strict()schemas reject bad input before any service runs. - Structured results — handlers return
{ ok } | { error }instead of throwing across processes; errors map to a typed taxonomy (ValidationError,DependencyError,NetworkError, …) so the UI shows a useful message, never a stack trace. - Contained paths — renderer-supplied file paths must stay inside the configured download directory; anything else is rejected.
- Event-driven progress — long jobs broadcast normalized state; raw tool output never leaves the main process.
Download Flow
- Inspect first — users see what they will get before spending bandwidth.
- Queue — multiple downloads wait their turn. The limit (1–10) is a setting, re-read on every queue drain.
- Single videos by design — yt-dlp runs with
--no-playlist; playlist support is deliberately out of scope today.
State in one sentence
A download is queued → inspecting → downloading → completed/failed/paused. The manager enforces it.
Data That Persists
No database. Four small JSON files in the OS user-data folder:
| File | Keeps |
|---|---|
history.json | Terminal downloads only — id, URL, status, file path |
inspection-history.json | Inspected URLs — one per normalized URL, 30-day retention |
settings.json | Folder, notifications, concurrency (sanitized against defaults on load) |
conversions.json | Completed audio extractions |
Deleting a history entry removes the record, never the file on disk. Completed entries whose file vanished are pruned on next load; lost destinations are backfilled by matching the file on disk — self-healing.
Known gap
JSON writes are not yet atomic (no temp-file + rename), so a crash mid-write could corrupt a store — tracked as an open task in the repository's audit docs.
Why This Shape
- Local-first — works offline except for fetching the media itself. No accounts, no servers.
- Bundled tools — yt-dlp and FFmpeg ship inside the installer (
resources/bin/). No setup for the user. - Single integration point — all tool calls are
spawn(executable, args[])in one service. Easy to test, easy to secure.
Result
A modern desktop look on top of proven command-line power, with the boundaries small enough to keep secure and testable.
Hard Problems Solved
Real issues found in testing and daily use — each fixed at the root, not patched over:
| Problem | Root cause | Fix |
|---|---|---|
| Arabic titles garbled on Windows | yt-dlp writes output in the legacy Windows code page; multi-byte characters split across stream chunks | Force --encoding utf-8 and decode child output with a StringDecoder |
| Concurrent downloads failed spuriously | Starting a second download mid-schedule let the queue pick up a job that was still inspecting | Only start/resume/retry enqueue work; status, re-entry, and active-execution guards added — with a regression test |
| Same video at two qualities collided into one file | Output template didn't include the format ID | Template now embeds the selected format; downloads keep UUID identity end to end |
| Completed downloads lost their file path, breaking "Open file" | Final path wasn't captured reliably | Ask yt-dlp to print the authoritative post-move path (--print after_move:filepath), verify against disk, and repair missing paths from history on load |
Why this section exists
These are the stories that show how the system behaves under real conditions — encoding quirks, race conditions, flaky path capture — and that each fix came with tests.
Honest Gaps
Documented in the repository's own audit (open task specs), not hidden:
- Chromium sandbox is disabled (
sandbox: false) — enabling it plus awill-navigateguard are tracked hardening work. - No automatic retry for transient network failures yet — retry today is manual (and works from history after restarts); auto-retry with backoff is a planned enhancement.
- JSON persistence is not crash-atomic.
- A renderer test suite exists, but end-to-end tests are not configured.
On the roadmap
A Chrome extension companion is recorded as a proposed ADR — scoped to discovery/control only, with the integration mechanism deliberately left open pending a future decision. Nothing is implemented.