LeyckerS/moondownloaderPublic

Bulk file downloader for datanodes.to and fuckingfast.co — real-Chrome extraction over CDP, pure-HTTP extraction with a Chrome TLS fingerprint, aiohttp streaming, WebView2 GUI and a headless CLI. Windows, Python 3.10+, MIT.

AI summary: High-performance bulk file downloader utilizing Playwright extraction and aiohttp streaming.

Stars
1.6K
+1 today
Forks
56
Watchers
1
Open issues
9
Open PRs
5
Contributors
~48
Commits
205
Branches
1

PythonMITCreated Oct 2, 2024Last push 1d agoLatest release v4.2+2 stars this week+5 this month

Quick answers

What is moondownloader?
High-performance bulk file downloader utilizing Playwright extraction and aiohttp streaming.
What does moondownloader do?
Moon Downloader is a Python-based utility engineered for extremely fast bulk file downloading. It combines Playwright for dynamic link extraction and browser flows with aiohttp for asynchronous streaming. Designed to maximize bandwidth utilization, it can reach peak speeds of 253 MB/s on gigabit fiber connections. It features pluggable support for various file-hosting providers like datanodes.to and fuckingfast.co via regex or automated browser interactions. The architecture meticulously separates extraction from the download pipeline, executing concurrent streams to fully saturate multi-gigabit connections while maintaining a lightweight edge WebView2 interface for monitoring.
Who is moondownloader for?
Power users and data engineers who need to download large volumes of files quickly from web-based hosting services. It requires familiarity with Python environments and basic command-line usage.
How do I get started with moondownloader?
git clone https://github.com/LeyckerS/moondownloader.git
How popular is moondownloader on GitHub?
LeyckerS/moondownloader has 1,589 stars and 56 forks on GitHub, and gained 2 stars in the last 7 days.
What license does moondownloader use?
LeyckerS/moondownloader is released under the MIT license.

Star history

since Jul 30, 2026
05001K1.5KJul 2026Aug 2026Sep 2026Oct 2026
1.6K stars as of Oct 3, 2026. Measured daily since Jul 30, 2026; GitHub no longer exposes earlier star timestamps.

Contribution activity

commits per day, last 52 weeks
OctNovDecJanFebMarAprMayJunJulAugSepMonWedFri2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-08: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 0 commits2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 0 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 0 commits2026-02-26: 0 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 0 commits2026-03-04: 0 commits2026-03-05: 0 commits2026-03-06: 0 commits2026-03-07: 0 commits2026-03-08: 0 commits2026-03-09: 0 commits2026-03-10: 0 commits2026-03-11: 0 commits2026-03-12: 0 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 0 commits2026-03-18: 0 commits2026-03-19: 0 commits2026-03-20: 0 commits2026-03-21: 0 commits2026-03-22: 0 commits2026-03-23: 0 commits2026-03-24: 0 commits2026-03-25: 0 commits2026-03-26: 0 commits2026-03-27: 0 commits2026-03-28: 0 commits2026-03-29: 0 commits2026-03-30: 0 commits2026-03-31: 0 commits2026-04-01: 0 commits2026-04-02: 0 commits2026-04-03: 0 commits2026-04-04: 0 commits2026-04-05: 0 commits2026-04-06: 0 commits2026-04-07: 0 commits2026-04-08: 0 commits2026-04-09: 0 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 0 commits2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 0 commits2026-04-19: 0 commits2026-04-20: 0 commits2026-04-21: 0 commits2026-04-22: 0 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 0 commits2026-05-07: 0 commits2026-05-08: 0 commits2026-05-09: 0 commits2026-05-10: 0 commits2026-05-11: 0 commits2026-05-12: 0 commits2026-05-13: 0 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 1 commit2026-05-18: 4 commits2026-05-19: 0 commits2026-05-20: 0 commits2026-05-21: 0 commits2026-05-22: 0 commits2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 0 commits2026-05-26: 0 commits2026-05-27: 0 commits2026-05-28: 0 commits2026-05-29: 0 commits2026-05-30: 0 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 0 commits2026-06-05: 0 commits2026-06-06: 0 commits2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 0 commits2026-06-10: 0 commits2026-06-11: 0 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 0 commits2026-06-16: 0 commits2026-06-17: 0 commits2026-06-18: 0 commits2026-06-19: 0 commits2026-06-20: 0 commits2026-06-21: 0 commits2026-06-22: 0 commits2026-06-23: 0 commits2026-06-24: 0 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 0 commits2026-06-30: 0 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 0 commits2026-07-08: 0 commits2026-07-09: 0 commits2026-07-10: 0 commits2026-07-11: 0 commits2026-07-12: 0 commits2026-07-13: 3 commits2026-07-14: 2 commits2026-07-15: 17 commits2026-07-16: 0 commits2026-07-17: 2 commits2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 0 commits2026-07-24: 1 commit2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 34 commits2026-07-31: 23 commits2026-08-01: 21 commits2026-08-02: 11 commits2026-08-03: 3 commits2026-08-04: 0 commits2026-08-05: 11 commits2026-08-06: 3 commits2026-08-07: 3 commits2026-08-08: 2 commits2026-08-09: 3 commits2026-08-10: 0 commits2026-08-11: 0 commits2026-08-12: 8 commits2026-08-13: 1 commit2026-08-14: 0 commits2026-08-15: 0 commits2026-08-16: 0 commits2026-08-17: 3 commits2026-08-18: 0 commits2026-08-19: 4 commits2026-08-20: 2 commits2026-08-21: 0 commits2026-08-22: 0 commits2026-08-23: 0 commits2026-08-24: 0 commits2026-08-25: 2 commits2026-08-26: 0 commits2026-08-27: 0 commits2026-08-28: 0 commits2026-08-29: 1 commit2026-08-30: 1 commit2026-08-31: 0 commits2026-09-01: 0 commits2026-09-02: 0 commits2026-09-03: 0 commits2026-09-04: 0 commits2026-09-05: 0 commits2026-09-06: 0 commits2026-09-07: 3 commits2026-09-08: 1 commit2026-09-09: 1 commit2026-09-10: 0 commits2026-09-11: 0 commits2026-09-12: 0 commits2026-09-13: 0 commits2026-09-14: 0 commits2026-09-15: 0 commits2026-09-16: 0 commits2026-09-17: 0 commits2026-09-18: 0 commits2026-09-19: 0 commits2026-09-20: 0 commits2026-09-21: 0 commits2026-09-22: 0 commits2026-09-23: 0 commits2026-09-24: 0 commits2026-09-25: 0 commits2026-09-26: 0 commits2026-09-27: 0 commits2026-09-28: 0 commits2026-09-29: 0 commits2026-09-30: 0 commits2026-10-01: 0 commits2026-10-02: 0 commits2026-10-03: 0 commits
171 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Actively maintained

    Pushed within 48 hours

  • Well documented

    High community health score

  • Permissive license

    MIT

  • Continuous integration

    Automated checks passing

What moondownloader does

Moon Downloader is a Python-based utility engineered for extremely fast bulk file downloading. It combines Playwright for dynamic link extraction and browser flows with aiohttp for asynchronous streaming. Designed to maximize bandwidth utilization, it can reach peak speeds of 253 MB/s on gigabit fiber connections. It features pluggable support for various file-hosting providers like datanodes.to and fuckingfast.co via regex or automated browser interactions. The architecture meticulously separates extraction from the download pipeline, executing concurrent streams to fully saturate multi-gigabit connections while maintaining a lightweight edge WebView2 interface for monitoring.

Power users and data engineers who need to download large volumes of files quickly from web-based hosting services. It requires familiarity with Python environments and basic command-line usage.

  • Asynchronous streaming: Leverages aiohttp for high-throughput, non-blocking downloads.
  • Browser automation: Uses Playwright to navigate complex extraction flows and captchas without user intervention.
  • High performance: Capable of saturating multi-gigabit connections, hitting up to 253 MB/s during peak bulk operations.
  • Pluggable architecture: Supports custom providers via simple regex or dedicated browser scripts.
  • Bulk management: Efficiently handles dozens of large files in a single session with visual progress tracking.
  • Decoupled UI: Maintains a responsive GUI by using a pull model where the frontend requests state updates rather than being flooded by backend events.

Where teams use it

Mass file retrieval

Download massive datasets or media collections rapidly from supported file hosts.

Automated extraction

Bypass manual link clicking on sites that require browser interaction before downloading.

Bandwidth saturation

Fully utilize multi-gigabit internet connections that standard downloaders fail to max out.

Custom provider scraping

Write custom regex or Playwright scripts to support new, unlisted file hosting platforms.

Getting started: git clone https://github.com/LeyckerS/moondownloader.git

README

main branch

🌙 Moon Downloader

V4.2

Bulk file downloader — real-Chrome extraction for datanodes.to, pure-HTTP extraction for fuckingfast.co, aiohttp streaming, and a GUI that runs on Edge WebView2.

Supported providers: datanodes.to · fuckingfast.co

Built with Python · Playwright · aiohttp · curl_cffi

Python Playwright WebView2 License CI CodeTriage



Best tested: ~250 MB/s on a 2.5 Gbps fiber — 23.5 GB across 47 files in ~3 minutes

Contributions welcome — the roadmap ranks everything open by size and by whether it needs Windows. Most of it doesn't. Three issues are held for anyone who has never had a pull request merged anywhere.



📸 The interface

The rate is the figure being watched, so it gets the left side and the plot underneath; completed, downloaded and ETA are reference and sit small. Every row shows which host it came from. Rendered from the page's own mock engine — the numbers are synthetic, which is what the demo chip in the corner says.



The cold open: the mark draws itself on, the wordmark resolves out of a blur, and the card cascade is held until it hands over. Under two seconds, and any key or click ends it.



Live transfers Engine log

Filter box and state chips over the list; what is moving gets room and a brighter name, what has finished tightens and recedes.


🔧 Engineering highlights

One browser decision, made once. BrowserGate in moon_extract.py owns the question of whether Chrome is needed at all. A batch of fuckingfast.co links never launches it — not even the Playwright driver's node process. A batch containing one datanodes.to link opens exactly one shared Chrome, on demand, no matter how many extractors are running. The GUI and the CLI import the same gate, so they cannot diverge, and tests/test_no_chrome.py asserts it for both in CI.

The UI pulls, Python never pushes. The page requests snapshot(cursor) about twelve times a second instead of the engine writing into it. Every DOM write stays on the page's own timeline, so a late snapshot costs a dropped frame instead of a stalled interface.

The log ring has a cursor. A bounded 6000-line ring plus a monotonic counter. The page asks for "everything after N"; if it fell behind further than the ring holds, it receives the oldest line still present rather than a silent gap it has no way to detect.

Untrusted input is clamped at one boundary. Everything the page sends passes through Engine.apply_cfg(), which coerces and clamps every number before it can reach a semaphore. There is one place to audit, not one per setting.

Measured, not asserted. 195.7 MB/s across 124 links on 8 download streams — 12.63 GB in 1m23s, 29 files done, 0 failed, no browser window opened. Method and instrumentation in docs/ENGINEERING_NOTES.md; the full design writeup is in docs/ARCHITECTURE.md.


🌗 What V4.2 is

A maintenance release. The change you will notice most is in the CLI: its progress line now comes from the same Engine.snapshot() the GUI reads, names its phase — extracting 12/48, then downloading 30/48 — and stops repeating itself while a slow extraction runs. The rest is accounting and CI, from outside contributors: a final write buffer that was counted twice in the byte totals, an Extractors slider that implied a concurrency datanodes never gets (it now shows the real limit), a syntax check for web/ where a pull request used to merge green with no checks at all, and two CI configuration files that CI itself did not watch — plus, from the maintainer, the last four places that still said 14.x or 4.0. Full detail and credits in CHANGELOG.md.


🌘 What V4.1 was

A maintenance release written entirely by other people — every entry in its changelog came from an outside contributor. Two were bugs you could hit in normal use:

  • A full destination disk stops the run. ENOSPC used to be handled like any other transfer error, so the queue kept going and the retry machinery kept re-fetching data that could never be written — 46 files and roughly 12 GB pulled and discarded on the run that reported it, with nothing on screen explaining why. It is now detected by errno, aborts the run, names the folder and the shortfall in the live log, and leaves every .tmp resumable.
  • Stop interrupts transfers already in flight. It used to set a flag and close Chrome while an in-progress download ran to completion, so on a large file the button appeared to do nothing for minutes.

The rest made the project harder to break by accident: structured CLI exit codes so a script can tell success from partial from total failure, a linter that runs once per pull request instead of five times, a test assertion that could never fail, and a test stub that left the engine able to reach the real network. Full detail and credits in CHANGELOG.md.


🌘 What V4 was

Both providers broke, and that release was the repair. The interface is unchanged since V3.

  • datanodes removed step 1. The share URL used to return a form whose submit carried name="method_free"; it now answers with step 2 directly, tokens already minted, and says "STEP 2 OF 2" on screen. The extractor waited 22s for markup that no longer exists and gave up before reaching the trigger chain that was already working.
  • fuckingfast added Turnstile to POST /f/<id>/go, so the pure-HTTP path returns 403 on its own.

Both are fixed and measured: datanodes 5/5 end to end, fuckingfast 3/3 consecutive at 6.8–9.7s with auto-solve only, both together 4/4. The full account — including three defects introduced while building the fuckingfast path and caught in testing — is in CHANGELOG.md.


🖥️ What V3 was

The interface, rebuilt. The engine is untouched — extraction, downloading and the CLI are the same code they were in 2.1, and that is deliberate: this release changes what you look at, not what it does.

before V3 V3
Launch the window simply appeared a cold open under two seconds, skippable, removed from the DOM when done
Mark a 512px PNG an SVG whose crescent and orbit move independently
Headline four equal stat cards one hero band; the rate is a 49px figure, the rest is reference
Settings column five floating cards one surface, hairline sections, Start built in
Transfer rows all identical weight provider chip per row, weight follows state
Finding a file scrolling filter box, state chips, / to focus
Getting links in the Load button drag and drop anywhere, appended not replacing
After a run the list, as it was a summary that can copy every failed link

Keyboard: Ctrl+Enter start or stop · Ctrl+O load a .txt · / filter · ? the shortcut panel · Esc close or clear.

On motion. The GUI animates for everyone; there is no prefers-reduced-motion exception, and that is a stated choice rather than an oversight. Nothing flashes and every loop is slow. If you want a motion preference, open an issue — it is a small change.


⚡ How the two providers differ

The two providers stopped having anything in common, so the app stopped pretending they did.

datanodes.to fuckingfast.co
Extraction real Chrome over CDP + persistent profile plain HTTPS first, the shared Chrome when refused
Browser yes — pages on one window, one identity only for links /go refuses
Captcha Turnstile: auto-solve, manual fallback Turnstile since Aug 2026, same auto-solve
Cost per link seconds ~0.25 s over HTTPS, ~7 s through the browser
Settings Pages, Captcha wait, Chrome path, API key nothing to tune

This changed in V4. fuckingfast put Cloudflare Turnstile in front of POST /f/<id>/go in August 2026: without a cf-turnstile-response token the endpoint answers 403 captcha verification failed, and no TLS fingerprint can mint one. Links it refuses now go through the same Chrome datanodes uses. A batch fuckingfast still serves over plain HTTPS opens no window at all — the HTTP path is tried first, every time, and the browser is only reached for after a refusal.

A batch that contains one datanodes link opens exactly one shared Chrome, on demand, no matter how many extractors are running.

One BrowserGate in moon_extract.py owns that decision, so the GUI and the CLI behave the same way — tests/test_no_chrome.py asserts it for both.


⚖️ Scope and responsible use

Moon Downloader is a client for links you already have. It automates the retrieval step — it does not search, index, or discover content, and it has no catalogue of any kind.

  • What you download is your responsibility. Copyright, licensing and the terms of the hosts you point it at are yours to respect. This tool does not check any of that for you.
  • The datanodes.to path automates a challenge the site presents to visitors. That challenge is an anti-bot control, and automating it may be contrary to that host's terms of service. Decide whether that is acceptable for your use before you run it.
  • The defaults are deliberately conservative — 8 download streams, one shared browser, one identity. They can be raised. Do not use this to hammer a host.
  • No affiliation. This project is not affiliated with, endorsed by, or supported by datanodes.to, fuckingfast.co, Cloudflare, or Microsoft. All trademarks belong to their respective owners.
  • Provided as-is under the MIT licence, without warranty of any kind. See LICENSE.

Rightsholders and providers: if you want a path changed or removed, open an issue or use the contact in SECURITY.md.


🚀 Quick start

One-click (Windows)

  1. Install Python 3.10+ — check ✅ "Add Python to PATH"
  2. Double-click start.bat
  3. Done. The first run installs the dependencies and Chromium.

start.bat starts a loopback HTTP server and opens the GUI in Edge (or Chrome) with --app: a window with no tabs and no address bar. No native GUI dependency, nothing to guess.

Manual

pip install -r requirements.txt
playwright install chromium
python moon_bridge.py            # GUI
python moon_bridge.py --serve    # server only, prints the URL
python moon_cli.py --urls links.txt --output ./downloads   # headless CLI

Just want to look at the interface?

Open web/index.html in Chrome or Edge. It boots in demo mode with a synthetic engine.


🖥️ GUI features

  • Links — per-host colouring while you paste, live count, datanodes / fuckingfast / others split
  • Per-method panels — common knobs in one card, datanodes in its own, fuckingfast declaring it has nothing to tune
  • Live transfers — one row per file: progress ring, state, percentage, instantaneous speed. Active transfers sort above the finished tail
  • Stats — speed (3 s rolling window + sparkline), completed, downloaded, byte-based ETA
  • Pipeline — extraction and download tracked separately, because they run at the same time
  • Log — the engine's own lines, tagged and coloured, capped at 2000
  • English / Italian — switchable at runtime, English by default
  • Settings and pasted links persist in settings.json (atomic write)

⚙️ Settings

Setting Range Default Applies to Description
Extractors 2 – 32 16 both Parallel extraction workers
DL streams 2 – 48 48 both Concurrent download connections
Retries 0 – 5 3 both Extraction retries per URL (network retries are separate)
Pages 1 – 8 8 datanodes Tabs on the shared Chrome window — not separate windows
Captcha 30 – 600 s 30 datanodes Manual Turnstile wait
Chrome path autodetect datanodes chrome.exe to drive over CDP
API key string — datanodes Premium key → direct JSON, no browser, no captcha

Fewer DL streams means more bandwidth per file; the pipe is still the ceiling.

Environment variables, the dedicated Chrome profile and the API-key limits are in docs/CONFIGURATION.md.


🏗️ Architecture

start.bat
   └── moon_bridge.py          loopback HTTP + token, launches Edge --app, OS dialogs
         ├── web/              index.html · styles.css · app.js   (the GUI)
         └── moon_engine.py    headless engine: start/stop/snapshot
               └── moon_extract.py   datanodes (Chrome+CDP) · fuckingfast (curl_cffi)
                                     BrowserGate: the launch, deferred

moon_cli.py       headless CLI, same engine, same extraction layer

Both front-ends import the same moon_extract, so extraction, the Chrome lifecycle and the launch decision exist once.

Pull model. The page asks for snapshot(cursor) ~12 times per second instead of Python pushing at it: every DOM write stays on the page's own timeline and a late snapshot is a dropped frame, not a stall.

The log has a cursor. A bounded 6000-line ring plus a monotonic counter; the page asks for "everything after N" and, if it fell behind further than the ring, gets the oldest line still held rather than a gap it cannot detect.

Rows read live FileRecords. download_file publishes rec.done_bytes and rec.live_mbs about four times a second on its own window, kept separate from the stall detector's 60 s history.

Untrusted input. Everything the page sends passes through Engine.apply_cfg(), which coerces and clamps every number before it reaches a semaphore.

Full write-up: docs/ARCHITECTURE.md.


📚 Documentation

Document What's in it
Quick start install, first run, what each setting does
CLI every flag, exit codes, scripting examples
Configuration every setting, every environment variable, the Chrome profile
Providers how each host is extracted, and how to add another
Architecture how the engine is built, feature by feature
Engineering notes the measurements behind the design decisions
Troubleshooting 403s, Turnstile failures, CDP conflicts, stalls
FAQ why curl_cffi is mandatory, what --extractors means
Contributing architecture, what counts as a contribution, the verification commands
Changelog every version since 14.0

🤝 Contributing

→ The roadmap — everything open, ranked by how hard it is and whether it needs Windows.

Most of the open work does not require a Windows machine. Documentation, CI, tests and dependency work all run on Linux and macOS, and the test suite stubs Chrome and the network at the moon_extract boundary, so it runs anywhere. Each issue says up front which it is.

good first issue scoped small, with the files to touch and the acceptance criteria already written out
help wanted everything open to outside contributors, including the larger items

Claim one by commenting on it in your own words — no need to ask permission first. What gets a pull request merged, and what gets one sent back, is written out in CONTRIBUTING.md.

How review works here, so you know what to expect:

  • Every claim in a pull request description is checked against the function it describes, not taken on trust. Where a review says something is wrong, it cites file.py:line and says whether it blocks or is merely untidy.
  • Issue bodies are written by the maintainer and have been wrong. If the code disagrees with the issue, the code wins — #63 was merged partly because it corrected the issue that opened it, and corrections to a specification are made publicly on the thread where the mistake happened.
  • Keeping a diff to its issue matters more than its size. Reformatting and "while I was in there" edits are the most common reason a pull request needs a second round.

Everyone who has shipped a change is named in AUTHORS.md with a line describing what they actually did, added in the same session as the merge.


🔒 The local server

Binds 127.0.0.1 only, on a kernel-chosen port, and every /api/ call must carry the token minted at startup — without it, 403. The API starts downloads and reads paths, so this is not a formality. The process exits by itself after 12 s with no requests: the page polls every 80 ms, so "no requests" means "window closed".


📂 Output files

File Description
moontech_*.log Human-readable performance report
moontech_*.json Per-file metrics (machine-readable)
output_links.txt Extracted direct links (Links-only mode)
failed_links.txt URLs that failed every retry
settings.json GUI settings, pasted links, language

📁 Optional files

Place next to the scripts:

File Purpose
proxies.txt Proxy list — ip:port:user:pass or http://user:pass@ip:port. Downloads only — see below

Proxies wrap the download session and nothing else. Link extraction always connects directly, from your own address: the shared Chrome instance datanodes needs for its Turnstile challenge, and the curl_cffi session fuckingfast uses, are never routed through the pool. An unreachable proxy list therefore stalls downloads while pages keep opening normally — that is the design, not a broken proxy.


✅ Verification

pytest tests/ -q       # engine + CLI: no browser for fuckingfast, exactly one for datanodes
python integration_http.py     # browser → loopback HTTP → engine (the path start.bat takes)
python integration_web.py      # pywebview path
python render_gui.py out/           # renders at 2554x1400 and 1440x900 + overflow audit
python moon_engine.py          # headless engine: prints a snapshot and exits

68 tests, and they need no browser, no display and no Playwright install: Chrome and the network are stubbed at the moon_extract boundary, so the suite runs anywhere. CI byte-compiles every tracked Python file on 3.10 through 3.14, runs ruff against a pinned version, checks that every runtime dependency carries an upper bound, and runs the suite — on every push and every pull request (.github/workflows/lint.yml).


📋 Requirements

  • OS: Windows 10 / 11 (the GUI needs Edge or Chrome — both ship with Chromium)
  • Python: 3.10+
  • Disk: ~150 MB for the Playwright Chromium (datanodes only)
  • Packages: aiohttp, playwright, curl_cffi; pywebview optional

Made with 🖤 and cold coffee

View on GitHub

Recent activity

commits and pull requests

Recent open issues

view all

Releases and announcements

8 total
  1. **A maintenance release.** One change you will see, in the CLI; the rest is accounting and CI. Every entry except the version strings and the Dependabot pins came from an outside contributor. ## The one you will see **The CLI's progress line now comes from the engine's own snapshot.** `moon_cli.py` used to keep a second copy of the byte-rate arithmetic, the counters and the lock that guarded them, so a speed or ETA fix landed in one front-end and not the other. It now samples the same `Engine.snapshot()` the GUI reads. The line names its phase — `extracting 12/48` while provider pages are being resolved, then `downloading 30/48` while files transfer — and a snapshot identical to the last one is not printed again, so a slow extraction no longer fills the terminal with repeated zero-speed lines. A stall-killed URL that is re-extracted cannot advance the count twice, and the final summary and the exit codes introduced in 4.1 read the engine's `ok` and `fail`. Documented in [`docs/CLI.md`](https://github.com/LeyckerS/moondownloader/blob/main/docs/CLI.md#progress-output). Thanks to @breezeFur (#97, #154). ## The rest - **A completed file's final write buffer was counted twice** in `

  2. **Every entry in this release came from an outside contributor.** Two are bugs you can hit in normal use; the rest make the project harder to break by accident. ## Two you can hit **A full destination disk now stops the run.** `ENOSPC` was handled like any other transfer error, so the queue kept going and the retry machinery kept re-fetching data that could never be written — on the run that reported it, 46 files each downloading for ~236s before failing, roughly **12 GB pulled off the network and discarded**, with nothing on screen saying why. It is now detected by `errno` and treated as a run-level fatal state: intake and retries stop, active transfers unwind at their next write boundary, and the live log names the folder and the shortfall. Interrupted files keep their `.tmp` and stay resumable, and only the URL that triggered it is recorded as failed. Thanks to @shard872 (#116, #150). **Stop interrupts downloads already in flight.** It used to set a flag and close Chrome while a transfer that had already started ran to completion — on a large file the button appeared to do nothing for minutes. Each active download's kill event is now signalled directly, and a user-initiated s

  3. Both providers changed how they hand out links and every download stopped. This release is the repair. Both providers changed the way they hand out links and every download stopped. This release is the repair. The GUI is unchanged since 3.0. ### Fixed - **datanodes.to removed step 1.** The share URL used to return a form whose submit carried `name="method_free" value="Free Download >>"`; the page now answers with step 2 directly, tokens already minted, and says so on screen ("STEP 2 OF 2"). The extractor waited 22s for `#downloadReveal` and `#method_free`, timed out and gave up before ever reaching the trigger chain that was already working — the failing runs are 22.5s per link against a 22.0s budget. It now waits for whichever shape the server sends and branches on the page rather than on an assumption; the old two-step path is still taken when the old markup arrives. Skipping the obsolete POST also matters on its own: a second `POST /download` re-runs SecSave and invalidates the token step 2 is holding, so the run was destroying its own session. - **The wait now covers the operator's reaction time.** On the new shape the controls only app

  4. The interface was rebuilt. The engine was not: extraction, downloading and the CLI are the same code they were in 2.1. ### Added - **A cold open.** The mark draws itself on as a vector, the wordmark resolves out of a blur, a raking beam crosses the field, and the card cascade is *held* — not merely delayed — until it hands over. Under two seconds, ended instantly by any key or click, and removed from the DOM when it finishes. - **The mark is an SVG and it moves.** Crescent and orbit are separate shapes, so the ring tilts continuously: the minor radius is what animates, which reads as a rotation in depth. The same drawing now serves the topbar, the cold open and the empty state, and the PNG survives only as the favicon. - **A hero band replaces the four stat cards.** The rate is a 49px figure with the plot spanning the card underneath; completed, downloaded and ETA are reference and sit small. - **The settings column is one surface.** Five floating cards became four sections of a single panel divided by a hairline, with Start built into the same object. - **Transfer rows carry their provider**, read off the source URL, and their weight follows thei

  5. **Nine people outside the project shipped changes in this release.** Two days ago that number was zero. Everything below came from someone who read the code, disagreed with it, and sent a patch. ## Fixed **The elapsed-time clock kept counting after a run finished.** `snapshot()` computed the elapsed time from `now - t0` unconditionally, so the display carried on ticking after the last file landed. The engine now records the end time at the single point where a run terminates — covering both a normal finish and a crash — and freezes the value there. *(#73, @RubenSanosh)* **A discarded partial download said nothing.** When a server answers `200` instead of `206` it is refusing to resume, and the code correctly restarted from zero — silently. A multi-gigabyte transfer appeared to begin again for no reason. The restart is now recorded and appears in `moontech_*.log`, with a regression test that fakes the response without touching the network. *(#98, @XEDAB)* **`--proxies` failed silently.** A misspelled path, or a file the parser did not recognise, loaded zero proxies and printed nothing — so a run started specifically to avoid direct connections made them anyway, with no indicatio

Commits per week

last 52 weeks
780Week of 2025-10-05: 0 commitsWeek of 2025-10-12: 0 commitsWeek of 2025-10-19: 0 commitsWeek of 2025-10-26: 0 commitsWeek of 2025-11-02: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 0 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 0 commitsWeek of 2026-02-22: 0 commitsWeek of 2026-03-01: 0 commitsWeek of 2026-03-08: 0 commitsWeek of 2026-03-15: 0 commitsWeek of 2026-03-22: 0 commitsWeek of 2026-03-29: 0 commitsWeek of 2026-04-05: 0 commitsWeek of 2026-04-12: 0 commitsWeek of 2026-04-19: 0 commitsWeek of 2026-04-26: 0 commitsWeek of 2026-05-03: 0 commitsWeek of 2026-05-10: 0 commitsWeek of 2026-05-17: 5 commitsWeek of 2026-05-24: 0 commitsWeek of 2026-05-31: 0 commitsWeek of 2026-06-07: 0 commitsWeek of 2026-06-14: 0 commitsWeek of 2026-06-21: 0 commitsWeek of 2026-06-28: 0 commitsWeek of 2026-07-05: 0 commitsWeek of 2026-07-12: 24 commitsWeek of 2026-07-19: 1 commitsWeek of 2026-07-26: 78 commitsWeek of 2026-08-02: 33 commitsWeek of 2026-08-09: 12 commitsWeek of 2026-08-16: 9 commitsWeek of 2026-08-23: 3 commitsWeek of 2026-08-30: 1 commitsWeek of 2026-09-06: 5 commitsWeek of 2026-09-13: 0 commitsWeek of 2026-09-20: 0 commitsWeek of 2026-09-27: 0 commitsOct 5, 2025Sep 27, 2026
171 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 2 commitsSun 1:00 — 0 commitsSun 2:00 — 4 commitsSun 3:00 — 4 commitsSun 4:00 — 0 commitsSun 5:00 — 2 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 0 commitsSun 10:00 — 0 commitsSun 11:00 — 0 commitsSun 12:00 — 0 commitsSun 13:00 — 0 commitsSun 14:00 — 2 commitsSun 15:00 — 0 commitsSun 16:00 — 0 commitsSun 17:00 — 0 commitsSun 18:00 — 0 commitsSun 19:00 — 0 commitsSun 20:00 — 1 commitsSun 21:00 — 0 commitsSun 22:00 — 0 commitsSun 23:00 — 1 commitsMon 0:00 — 4 commitsMon 1:00 — 2 commitsMon 2:00 — 0 commitsMon 3:00 — 0 commitsMon 4:00 — 1 commitsMon 5:00 — 0 commitsMon 6:00 — 0 commitsMon 7:00 — 0 commitsMon 8:00 — 0 commitsMon 9:00 — 0 commitsMon 10:00 — 0 commitsMon 11:00 — 0 commitsMon 12:00 — 2 commitsMon 13:00 — 0 commitsMon 14:00 — 0 commitsMon 15:00 — 0 commitsMon 16:00 — 3 commitsMon 17:00 — 1 commitsMon 18:00 — 1 commitsMon 19:00 — 2 commitsMon 20:00 — 0 commitsMon 21:00 — 0 commitsMon 22:00 — 0 commitsMon 23:00 — 0 commitsTue 0:00 — 0 commitsTue 1:00 — 1 commitsTue 2:00 — 0 commitsTue 3:00 — 0 commitsTue 4:00 — 0 commitsTue 5:00 — 0 commitsTue 6:00 — 0 commitsTue 7:00 — 1 commitsTue 8:00 — 1 commitsTue 9:00 — 0 commitsTue 10:00 — 0 commitsTue 11:00 — 0 commitsTue 12:00 — 0 commitsTue 13:00 — 0 commitsTue 14:00 — 0 commitsTue 15:00 — 0 commitsTue 16:00 — 0 commitsTue 17:00 — 0 commitsTue 18:00 — 0 commitsTue 19:00 — 0 commitsTue 20:00 — 0 commitsTue 21:00 — 0 commitsTue 22:00 — 0 commitsTue 23:00 — 2 commitsWed 0:00 — 2 commitsWed 1:00 — 2 commitsWed 2:00 — 5 commitsWed 3:00 — 10 commitsWed 4:00 — 1 commitsWed 5:00 — 1 commitsWed 6:00 — 2 commitsWed 7:00 — 3 commitsWed 8:00 — 1 commitsWed 9:00 — 2 commitsWed 10:00 — 3 commitsWed 11:00 — 0 commitsWed 12:00 — 0 commitsWed 13:00 — 1 commitsWed 14:00 — 0 commitsWed 15:00 — 1 commitsWed 16:00 — 1 commitsWed 17:00 — 1 commitsWed 18:00 — 0 commitsWed 19:00 — 0 commitsWed 20:00 — 1 commitsWed 21:00 — 0 commitsWed 22:00 — 3 commitsWed 23:00 — 2 commitsThu 0:00 — 0 commitsThu 1:00 — 0 commitsThu 2:00 — 0 commitsThu 3:00 — 0 commitsThu 4:00 — 0 commitsThu 5:00 — 1 commitsThu 6:00 — 0 commitsThu 7:00 — 0 commitsThu 8:00 — 0 commitsThu 9:00 — 0 commitsThu 10:00 — 0 commitsThu 11:00 — 0 commitsThu 12:00 — 0 commitsThu 13:00 — 0 commitsThu 14:00 — 1 commitsThu 15:00 — 0 commitsThu 16:00 — 0 commitsThu 17:00 — 10 commitsThu 18:00 — 12 commitsThu 19:00 — 10 commitsThu 20:00 — 4 commitsThu 21:00 — 0 commitsThu 22:00 — 2 commitsThu 23:00 — 0 commitsFri 0:00 — 4 commitsFri 1:00 — 1 commitsFri 2:00 — 4 commitsFri 3:00 — 1 commitsFri 4:00 — 1 commitsFri 5:00 — 0 commitsFri 6:00 — 0 commitsFri 7:00 — 0 commitsFri 8:00 — 0 commitsFri 9:00 — 1 commitsFri 10:00 — 0 commitsFri 11:00 — 0 commitsFri 12:00 — 0 commitsFri 13:00 — 1 commitsFri 14:00 — 1 commitsFri 15:00 — 0 commitsFri 16:00 — 0 commitsFri 17:00 — 0 commitsFri 18:00 — 0 commitsFri 19:00 — 7 commitsFri 20:00 — 2 commitsFri 21:00 — 1 commitsFri 22:00 — 2 commitsFri 23:00 — 4 commitsSat 0:00 — 0 commitsSat 1:00 — 1 commitsSat 2:00 — 1 commitsSat 3:00 — 0 commitsSat 4:00 — 3 commitsSat 5:00 — 1 commitsSat 6:00 — 1 commitsSat 7:00 — 1 commitsSat 8:00 — 3 commitsSat 9:00 — 0 commitsSat 10:00 — 1 commitsSat 11:00 — 3 commitsSat 12:00 — 3 commitsSat 13:00 — 1 commitsSat 14:00 — 1 commitsSat 15:00 — 0 commitsSat 16:00 — 0 commitsSat 17:00 — 0 commitsSat 18:00 — 1 commitsSat 19:00 — 1 commitsSat 20:00 — 0 commitsSat 21:00 — 1 commitsSat 22:00 — 0 commitsSat 23:00 — 1 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.

Who is committing

last 52 weeks
Maintainer commits127 (62%)
Community commits77 (38%)

204 commits in total over the last year.

DateListRankStars gained
Jul 8, 2026daily#21+4