A Chrome extension that resolves ad-gated shortener links in place on the page you're viewing, and forwards downloads to JDownloader 2 without you copying anything.
shortened links on the page --> resolved in place --> JDownloader 2
It runs on dlpsgame.com and nswgf.com only. Everywhere else it is inert: no badges, no right-click entries, nothing reading the page.
No build step, no dependencies, no bundler, no telemetry — the files Chrome loads are the files in this repo.
- Download the
.zipfrom Releases - Unzip it somewhere permanent — not Downloads. Chrome reloads it from that exact folder every time it starts, so moving it later breaks the extension.
- Open
chrome://extensions - Turn on Developer mode — toggle, top right
- Click Load unpacked and pick the unzipped
link-decryptfolder
Done. Then set up JDownloader below.
Notes
Chrome only installs from the Web Store, which is why this is an "unpacked" extension. Chrome may mention developer-mode extensions on startup — harmless, and dismissing it doesn't disable anything.
After changing the code, click ↻ on the extension's card in
chrome://extensions and reload any open tabs. The version shows in the
popup header, so you can confirm the reload took.
In JDownloader 2: Settings → Advanced Settings → search
externinterface → tick Remote Control / External Interface.
Then in the extension: click its icon → Settings → Test
connection. You want jdownloader=true.
The first time you send links, JD asks "An external application tries to add links" — click Allow it! once and it remembers.
Sites commonly label a row of links by host — Mediafire · Akia · Viki ·
1File — while every one of them wraps the same landing page. They
look like alternative mirrors and aren't; the real mirrors live on that
page. One click follows it and sends everything it finds, so a second
click has nothing left to add. The badge says already sent rather than
appearing to do nothing.
JDownloader then groups the results by filename and size, so if one host
fails it falls back to the others by itself. A host it can't reach shows
up as its own entry — Blocked by! Cloudflare Site-Protection, for
instance — which is JD reporting that mirror unavailable, not stray junk.
JDownloader's Dupe Manager silently drops links it has seen before,
and it answers success either way — so the extension has no way to tell
the difference. Two settings control it, both on by default
(Settings → Advanced Settings, search dupe):
GeneralSettings: Dupe Manager— checks against the download listLinkCollector: Dupe Manager— checks against the Linkgrabber
The first one is the surprising one: removing a link from the Linkgrabber doesn't forget it if it ever reached the download list, so re-sending that link does nothing, quietly and forever. Test with a link you've never added, or untick these to allow duplicates.
Many URL shorteners and ad-gateways don't actually hide their destination — they carry it base64-encoded in a query parameter:
https://short.example/full?api=<key>&url=aHR0cHM6Ly9leGFtcGxlLmNvbS9wYWdl&type=2
^ base64 of https://example.com/page
Those are resolved with no network request at all, which makes them instant and completely reliable. Handles urlsafe base64, stripped padding, percent-encoded plain URLs, ~16 common parameter names, and wrappers nested inside other wrappers.
Resolved links get their href rewritten to the real target and a badge
added reading → hostname:
- click the badge to send that link straight to JDownloader
- alt-click to copy the URL instead
A MutationObserver catches links added after page load, so lazy-loaded and infinite-scroll pages are covered. Both behaviours are toggleable in Settings.
If the page you're on is itself a wrapped link, the popup offers a one-click jump to the real destination.
The popup lists every downloadable link found, tagged one of three ways:
archive— the URL ends in an archive extension. All the multi-part naming schemes are covered, since missing one silently drops half a set:.part1.rar….partN.rar,.rar+.r00….r99,.7z.001/.001…, and.zip+.z01….z99.host— a known file host.?— a resolved link on a host that isn't in the list.
That third category matters: no hardcoded host list stays current, and without it an unrecognised link would vanish from the UI entirely. Anything you find yourself unticking often is worth adding to the host list in Settings.
The same reasoning applies when following: if a link we didn't recognise turns out to have no links on it, that usually means the page is the download and we simply didn't know the host. Rather than give up, the original link goes to JDownloader — which knows hundreds of hosts, far more than any list kept here. Worst case it reports the link offline, which costs nothing.
Each row shows the link's text from the page rather than a bare hostname, since when one file is offered across several hosts every row would otherwise read the same. Above the list, per-host chips toggle all links from one host at once — so choosing which host to pull from is a single click rather than unticking a dozen boxes.
Links whose text reads like Guide, Tool Download or How to install
are skipped: they sit right beside the real links and are
indistinguishable by URL. The match is word-bounded, so something
genuinely called Toolbox Simulator survives.
Everything is ticked by default. Handing a folder or host link to JDownloader and letting its host plugins expand it into the individual files is more reliable than scraping for them ourselves — JD knows how to authenticate against hosts, follow their folder APIs, and enumerate a multi-part set. Anything it can't handle simply shows as offline, which costs nothing.
Links are packaged under the page's title rather than its hostname, so a multi-part set arrives as one recognisable package. Nothing starts downloading until you say so.
There are also right-click options: Send link to JDownloader on any link, and Send all download links on this page.
Sometimes the page you're on doesn't hold the files — its links point at
other pages. When the popup sees resolved links leading to further
pages, it offers "Follow links & find downloads": the service worker
fetches those pages in the background, harvests the downloadable links,
and merges them into the list tagged via <site>. Nothing opens a tab
and nothing is downloaded — it only reads HTML.
Two hops by default, because that's the shape these sites actually take: a wrapped link lands on a mirror list, and each mirror on it is a further page holding the real host link.
page you're on wrapped link, labelled "Rootz"
└─ /archives/34685 mirror list: Rootz · Mediafire · Akia · Viki · 1File
└─ /archives/34681 the actual rootz.so link
Both wrapped links and sibling pages — same site, same folder — are followed, since those inner mirror links are usually plain hrefs rather than wrapped ones. Restricting to the same folder is what stops this becoming a whole-site crawl: it picks up the mirror list and skips the site root, the nav, and off-site guides. There's a page cap (40 by default), a 20s per-page timeout, a concurrency limit of 4, and at most 15 siblings per page.
The upshot is that one click collects every mirror, so JDownloader can fall back on its own when one host fails. To pull from a specific host, use the per-host chips in the popup.
Note that this reads raw HTML, so a host that renders its file list in JavaScript won't yield individual files this way. Sending the host link straight to JD is the more reliable path there — which is why it's the default.
- Bump
versioninmanifest.json - Add a short entry to
CHANGELOG.mdsaying what changed and why — one paragraph is plenty, but don't skip it - Tag it:
git tag v0.7.0 && git push origin v0.7.0CI runs the tests, checks the tag matches manifest.json, builds the zip
and publishes it to Releases. Put the same changelog entry in the release
notes, so anyone downloading it can see what changed without reading
commits.
To build one locally:
./package.sh # -> dist/link-decrypt-v<version>.zipThat copies only manifest.json, src/ and icons/ into a clean zip,
plus an INSTALL.txt. The repo root doubles as the unpacked extension
folder, so packaging copies what's needed rather than zipping the folder
— otherwise .git, the tests and CI config would go along for the ride.
Worth knowing: Chrome refuses to install extensions from outside the
Web Store, so a .crx file sent to someone will not install — Chrome
blocks it silently. The realistic options are a GitHub Release (what this
repo does) or a Web Store listing, which can be set to Unlisted so it
doesn't appear in search and only people with the link can install it.
That costs a one-time $5 developer fee and a review, in exchange for
one-click installs and auto-updates.
A content script inherits the page's origin, so on an https:// page a
POST to http://127.0.0.1:9666 is blocked as mixed content. The
service worker runs on the extension's own origin, and with
host_permissions for 127.0.0.1/localhost its requests bypass CORS.
http://localhost also counts as a trustworthy origin, so there's no
mixed-content issue either. That's why src/background.js owns all
JDownloader traffic.
It also has no DOM — there is no DOMParser in a MV3 background context
— so src/harvest.js extracts links from fetched pages with regex rather
than a tree walk. Only href attributes and bare URLs are needed, not
structure.
manifest.json extension manifest (MV3)
src/ the extension itself
unwrap.js resolves links with an embedded destination
links.js classifies archive / file-host links
harvest.js pulls links out of fetched page HTML
content.js rewrites and badges links on the page
background.js service worker: JDownloader + crawling
popup.* the popup UI
options.* settings page
icons/ toolbar icons
test/ node test suites (not shipped)
package.sh builds dist/link-decrypt-v<version>.zip
Because this folder is loaded directly by Chrome, avoid adding anything
named with a leading _ — Chrome treats those as reserved and refuses to
load the extension. test/test_layout.mjs enforces this.
No build step and no dependencies:
node test/test_unwrap.mjs # resolving wrapped links
node test/test_links.mjs # archive / file-host classification
node test/test_harvest.mjs # pulling links out of page HTML
node test/test_crawl.mjs # depth, dedup, page caps, failures
node test/test_layout.mjs # loads as an extension, and only on the one sitetest_crawl.mjs stubs fetch with a small fake site graph, so the
depth/dedup/cap logic is verified without touching the network.
test_layout.mjs earns its place because the repo root is the folder
Chrome loads: it fails on reserved _ names, checks the manifest's
references and the service worker's importScripts targets all resolve,
and fails if a content-script match pattern would ever reach beyond
dlpsgame.com and nswgf.com.
package.sh runs it before building, so a broken layout can't be
packaged.
CI runs all five on every push.
| Permission | Why |
|---|---|
storage |
remembers your JD host/port and the file-host list |
contextMenus |
the two right-click menu entries |
notifications |
reports success/failure of right-click sends |
activeTab |
lets the popup read links from the tab you're looking at |
http://127.0.0.1/*, http://localhost/* |
talking to JDownloader |
http://*/*, https://*/* |
fetching followed pages to harvest their links |
content script on dlpsgame.com, nswgf.com |
resolving links on the page you're viewing |
The content script — the part that reads a page and puts badges on it —
is limited to dlpsgame.com and nswgf.com.
The host permission is separate and stays broad, because the mirror pages a download hides behind live on whatever domain the site sends you to, and reading another page's HTML cross-origin requires permission for it. It is only ever used for pages you explicitly ask it to follow, one click at a time, and never to read a page you are merely visiting.
Installing an unpacked extension means trusting it with every page you
visit, so here is exactly what this one does and doesn't do. All of it is
checkable against the source in a couple of minutes — there's no build
step, so what's in src/ is what runs.
-
No remote code. Nothing is fetched and executed. No
eval, nonew Function, no remotely-loaded scripts. MV3's default policy forbids it and nothing here tries to work around that. -
No
innerHTMLanywhere. Link text and hostnames come from pages you visit, i.e. from untrusted input. Every one of them is rendered withtextContent, so a page can't inject markup into the badge or the popup. -
No analytics, no telemetry, no phoning home. The only outbound requests are to your own JDownloader on
127.0.0.1:9666, and to pages you explicitly ask it to follow. -
Nothing exposed to web pages. No
web_accessible_resourcesand noexternally_connectable, so a website can't reach into the extension or talk to its service worker. -
Credentials are never sent when following pages (
credentials: "omit"), so your cookies for those sites aren't attached. -
Settings stay local, in
chrome.storage. -
Two sites only. The content script is declared for
dlpsgame.comandnswgf.comand nothing else, so on every other page you visit the extension never runs at all — seecontent_scripts.matchesinmanifest.json, which a test enforces.
The one thing worth weighing before you install: the host permission
still covers all sites, because the service worker has to fetch the
mirror pages a download hides behind and those sit on other domains.
Chrome's install prompt reports that as access to every site, which is
accurate about what it could do. What it actually does is in
src/content.js and src/background.js — about 400 lines between them,
and worth the read if that matters to you.
This resolves links and forwards URLs to your own JDownloader instance. It hosts, mirrors and distributes nothing. You are responsible for what you download and for complying with the terms of any site you point it at, and with applicable copyright law.
MIT — see LICENSE.