A cross-browser Manifest V3 extension that hands download links and detected media to the Downloader desktop app.
- Context menu — right-click a link, image, video or audio → “Download with Downloader”. Right-click a text selection → “Download selected links with Downloader”.
- Popup — paste a link, send the detected media for the current tab, or scan the page for downloadable links, then send one or all to the app.
- Media capture — watches network responses and surfaces video / audio / HLS (
.m3u8) streams, with a badge count on the toolbar icon. DASH (.mpd) is not listed: it can be neither size-probed nor read for a quality, so it could only ever be a row with nothing to say for itself. The app still downloads DASH — paste the.mpdlink into the popup's box, or add it in the app. - Size, resolution and quality picker — each detected item is probed for its file size, and an
HLS master playlist expands into a quality dropdown (resolution or bitrate per option) instead of
one opaque
.m3u8row. Probing never blocks the popup: it renders immediately, then upgrades rows in place as results arrive. - One list, best copy first — everything detected on the page is listed together: an HLS master
(
.m3u8) leads, then the highest quality (1080pabove720p), and where a link names no quality the largest file. A row without a quality picker shows the quality it was ranked on, so the order explains itself. There is no collapsed section and nothing to expand. Ordering by file type was tried first and dropped: type cannot say which copy of a video is the good one. (This replaced a "Main media vs. Other detected" split that promoted a group only when a visibility hint happened to be fresh at the moment the popup opened — on a feed page whose player had finished autoplaying, e.g. x.com, it usually was not, so the video you were looking at ended up hidden.) - Previews — each row carries a thumbnail so you can tell which video a link is without reading a
signed CDN URL: a frame captured from the page's own player when the browser allows it (a
cross-origin video blocks that), otherwise the player's poster image, otherwise the page's
og:image, otherwise a file-type placeholder. Previews are used only to draw the popup — they are never sent to the app or anywhere else. - Download folder — set one in Settings and every link the extension sends is saved there, so the app doesn't ask. The field is prefilled with the folder the app itself is configured to use (read from its local API), so you start from a correct absolute path; leave it empty to let the app decide as before.
- Known-unsupported-site message — on sites that stream via MSE/DRM with no fetchable file URL (YouTube, Netflix, …), the popup explains why nothing was found instead of showing a blank list that looks broken.
- Auto-send — every captured link is forwarded to the app's local listener. By default it is
added silently and starts downloading (the app's
/api/addendpoint); untick “Add silently (no dialog)” in the popup to review each link in the app's Add dialog (/add?url=…) instead. On an app version without the API the extension falls back to the dialog automatically. That choice governs every hand-off, including a download the extension intercepts from the browser: those go to/api/addwithconfirm: true, so the dialog opens carrying the link's cookies, referer, headers, mirrors and save folder rather than the bare URL. If you cancel that dialog, the browser's own download is left running and you keep the file. A link where you picked a specific quality is always sent silently, since the dialog would discard the pick. - Download interception (off by default) — take over downloads the browser starts and hand
them to the app instead. Turn it on in Settings (the popup's Settings & download interception
link, or your browser's extension options), where you also control what gets taken over: which file
types (an allow list of archive/installer types by default), an optional minimum size, and sites to
leave alone. If a download isn't being taken over, the file-type list is almost always why.
The type is judged from every source that names the file — the browser's own file name, the
server's
Content-Disposition, the URL path and the MIME type — so a download whose path ends in something that only looks like an extension (…/XAPK/com.instagram.android) is still matched on what the server actually called it. Both addresses the browser had are handed over — the one it was actually fetching the file from leads, and the link you clicked follows as a fallback — and the app tries them in turn, so a site that serves its file from a different address than the page still downloads. The browser's own download is cancelled only after the app has accepted the hand-off, so a closed app, an unreachable app or a refused add all just leave the browser downloading as usual — interception can never cost you a file. Each takeover shows a notification and counts on the badge, so a download disappearing from the browser is never unexplained. Requires thedownloadspermission, which is unused while the setting is off (PRIVACY.md). - Signed-in session hand-off — when you send a link, the extension also passes the cookies for that one URL to the app, so a site that needs you to be logged in (e.g. a YouTube video handled by the app's video downloader) can be fetched with your live session. Cookies are read only for the URL you send, go only to your local app, are never logged, and the app deletes them right after the download. An intercepted download carries the referring page as well, because the browser would have sent it — without it a site that checks the referer would refuse a file it was about to serve. See PRIVACY.md.
Running another intercepting download manager alongside this one is not supported. If two extensions both take over downloads, whichever reacts first wins and the outcome is not something either can arbitrate. Turn interception off in one of them.
Direct media capture doesn't work on DRM/encrypted streaming sites (Netflix, and YouTube's in-page player) — they don't expose a fetchable media URL. A YouTube video page link can still be sent to the app, which downloads it via its video-site plugin (using the session cookies above when the video requires sign-in).
- The Downloader desktop app must be running.
- Settings → Browser extension & local API must be on in the app (it opens the local listener
on port
15151, falling back to15152–15155if that port is taken by another program — the extension finds the right one automatically). It is enabled by default; the effective address is shown in the app's Settings. If the app answers on none of those ports, the popup says so and names the ports it probed, rather than showing an unexplained red dot.
The easiest route for a user is inside the desktop app: Settings → Browser extension & local API →
Get the files. The app detects installed browsers, downloads the build for each from the matching
GitHub Release, verifies its sha256 before unpacking, puts it in a stable folder under the app's own
data directory, and shows the per-browser steps. It never writes to a browser profile, never touches
browser policy, and never asks for elevation — see the install-browser-extension OpenSpec change for
why installing into a browser programmatically is off the table.
The manual route below is the same thing done by hand, and is what you want when developing.
- Go to
chrome://extensions(oredge://extensions). - Turn on Developer mode.
- Load unpacked → select this
browser-extensionfolder.
- Copy
manifest.firefox.jsonovermanifest.json(Firefox readsmanifest.json):(Keep a backup of the Chromecp manifest.firefox.json manifest.json
manifest.json— they differ only in thebackgroundshape and the Firefoxbrowser_specific_settingsblock.) - Go to
about:debugging#/runtime/this-firefox→ Load Temporary Add-on → pickmanifest.json.
The extension only needs the app's loopback endpoints:
GET http://127.0.0.1:<port>/api/add?url=<url-encoded link> # silent add (default)
GET http://127.0.0.1:<port>/add?url=<url-encoded link> # open the Add dialog instead
GET http://127.0.0.1:<port>/ping # reachability check
POST http://127.0.0.1:<port>/api/add # hand-off with cookies/referer/headers
# + "confirm": true to ask the user
GET http://127.0.0.1:<port>/api/add-status?ticket=<ticket> # follow a confirmation to added/cancelled
<port> is normally 15151; if another program holds it, the app falls back within the declared
range 15151–15155 and the extension probes /ping across that range (last-known-good port
first) to rediscover it. These are served by Services/LocalApiService.cs in the desktop app (see
docs/local-api.md in the main repo for the full API). No other ports, servers or accounts are
involved.
Every one of those requests also carries which extension is asking — extv (this extension's
version) and extb (a coarse browser label: chrome, edge or firefox), plus an
X-Downloader-Extension: <version>; <browser> header; the POST form of /api/add carries the same
two as JSON fields. That is what lets the app tell you your extension is out of date instead of
leaving you on a build that quietly stopped working. It needs no extra permission and adds no extra
request, the app keeps it in memory only (never in its config file, never in its log), and a request
without it is served exactly as before — so an older extension, the CLI and any other local tool are
unaffected.
| File | Role |
|---|---|
manifest.json |
Chrome/Edge MV3 manifest (service-worker background) |
manifest.firefox.json |
Firefox MV3 manifest (scripts background + gecko id) |
common.js |
Shared helpers: media detection, sendToApp(), size/HLS probing, grouping |
background.js |
Context menus, response sniffing, badge, message handler, probing coordinator, download interception |
popup.html / popup.css / popup.js |
Toolbar popup UI (one type-ordered list, previews, quality picker) |
options.html / options.css / options.js |
Settings page: download folder + download-interception rules |
icons/ |
Toolbar/store icons (16/48/128) |
Run the unit tests (pure helpers in common.js) with node --test src/browser-extension/common.test.js.
- Firefox (AMO) — automatic:
.github/workflows/extension.ymlsubmits the version for review on every push todevelop/mainthat touches this folder. SeePUBLISHING.mdfor the failure history and how the packaging bug that broke it for two months was fixed. - Chrome Web Store / Edge Add-ons — still a manual dashboard upload (needs the author's
developer account); zip this folder (
scripts/build-extension.sh) and upload it.
Bump version in both manifests for each release — a test enforces they agree.