Ahmed Abdelaziz

EasyDownloadDesign Docs

Architecture

How the app is organized — UI, bridge, core, state machine, and hard problems solved.

Last updated August 2026

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

LayerWhat it does
InterfaceShows progress, collects clicks, displays history. Built with React.
BridgeA tiny typed API (window.mediaDownloader). The only door to the core.
CoreOwns 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:

ServiceOne job
Download ManagerQueue, state machine, concurrency (1–10), duplicate prevention
Media ServiceURL inspection via yt-dlp, metadata normalization
Conversion ManagerFFmpeg conversions and audio extraction, with persisted history
Process ManagerSpawning and supervising child processes with UTF-8 stream decoding
File / Dependency / Settings / History / Inspection History / Notification ManagersPersistence 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:

FileKeeps
history.jsonTerminal downloads only — id, URL, status, file path
inspection-history.jsonInspected URLs — one per normalized URL, 30-day retention
settings.jsonFolder, notifications, concurrency (sanitized against defaults on load)
conversions.jsonCompleted 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:

ProblemRoot causeFix
Arabic titles garbled on Windowsyt-dlp writes output in the legacy Windows code page; multi-byte characters split across stream chunksForce --encoding utf-8 and decode child output with a StringDecoder
Concurrent downloads failed spuriouslyStarting a second download mid-schedule let the queue pick up a job that was still inspectingOnly 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 fileOutput template didn't include the format IDTemplate 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 reliablyAsk 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 a will-navigate guard 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.