All notable changes to Moon Downloader will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Versioning. The public releases are V1 (tag
v14.1) and V2 (tagv2.0). V2 is where the numbering resets: the 14.x and 15.0 entries below keep the numbers they shipped with, andv14.1keeps its tag so its download link never breaks.
docs/CLI.md— a reference formoon_cli.py: every flag, its default, and what it actually controls (#37, @kushin25)moon_cli.py --version, and the version is now recorded in both report flavours: the CLI log header and JSON report gained it, the engine JSON report gained it for parity. OneVERSIONconstant, imported everywhere (#61, @Moferanoluwa)docs/CLI.mddocumented the pre-#48--proxiesbehaviour — that a missing proxy file is silent — which is the opposite of what the code does now. The row and a new prose block distinguish the four real cases, including that the zero-yield warning fires for the implicit default too (#63, @Moferanoluwa)- The verification suite is a pytest suite. The no-Chrome regression moved from a
standalone script into
tests/test_no_chrome.pywith a sharedtests/conftest.py, and CI runspytest tests/ -q(#38, @pollychen-lab)
- The download engine exists once.
download_file,TelemetryandProxyPoolmoved intomoon_download.py;moon_engine.pyandmoon_cli.pynow import them instead of each carrying their own copy (#41, @pollychen-lab). A fix in the download path is a one-file change from here on, and the two copies can no longer drift apart
- The documented verification commands pointed at
python test_no_chrome.py, which stopped existing when the tests moved. They now point atpytest tests/(#43, @NanoRisk6) native_dialogswallowed every exception, including a broken_DIALOG_SRC. It now catches onlysubprocess.TimeoutExpiredandOSError, so a real failure surfaces instead of returning an empty path. Four other deliberate swallows inmoon_bridge.py,moon_cli.pyandmoon_download.pynow say why they are there (#54, @AdvaitVarhade)--proxiesfailed silently. A misspelled path, or a file in a format 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 indication.ProxyPool.load()now returns(loaded, skipped)and warns when an explicitly passed file is missing, when a file parses to zero proxies, and how many lines it skipped. The implicitproxies.txtstays quiet when absent, which is the normal no-proxy state (#48, @AdvaitVarhade)
The GUI moved off tkinter. Both extraction methods were rebuilt in the 14.2–14.8 line and no longer share a mechanism, so the interface stopped pretending they do.
- New GUI on Edge WebView2 (
web/index.html,web/styles.css,web/app.js) — Chromium rendering: real anti-aliasing, real alpha, gradients, blur, GPU transitions moon_bridge.py— loopback HTTP host with a per-run token; launches Edge/Chrome with--app(a window with no tabs and no address bar), OS file dialogs, atomicsettings.jsonmoon_engine.py— the download engine with no GUI attached:start()/stop()/snapshot(cursor)/scan_tmp(), all JSON-ablebuild_engine.py— generator that producesmoon_engine.pyfrom a pristinemoon_tk.py, so there is one source of truth for the engine- Live transfer rows — progress ring, state, percentage and instantaneous speed per file,
fed by live
FileRecords (done_bytes/live_mbs, published ~4 Hz on their own window, kept separate from the stall detector's 60 s history) - English / Italian switch, English by default; the engine ships numbers and a stage name, the page writes the sentence
- Fluid type scale (
clamp()): the interface scales with the window instead of staying at an 8 px ink height on a 2560×1440 screen test_no_chrome.py,integration_http.py,integration_web.py,render_gui.py— the verification suite.test_no_chrome.pystubs Chrome and the network at themoon_extractboundary, so it needs no browser, no display and no Playwright install, and covers the engine, the CLI and (statically)moon_tk.pymoon_extract.BrowserGate— the deferred launch:get()opens Playwright and Chrome on first demand, collapses concurrent first calls onto one instance, and tears both down in order- Byte-based ETA, host split of the pasted links, per-host colouring in the link editor,
proxies.txtcount and.tmpresume count in the status bar
- Chrome is opened lazily — on the first datanodes link, never before. The decision lives in
moon_extract.BrowserGateand is shared by the WebView engine, the Tk GUI and the CLI Captchadefault 240 s → 30 s,Pagesdefault 3 → 8- Settings and pasted links persist across restarts in
settings.json - Every value the GUI sends is coerced and clamped in
Engine.apply_cfg()before it reaches a semaphore moon_tk.py(the tkinter GUI) still runs unchanged fromstart_tk.bat; the only edit it took is the lazy launch, so the two GUIs and the CLI cannot drift apart on itmoon_cli.py --browsersis documented as what it always was: parallel extraction workers, not one browser each- CI byte-compiles every module, runs
test_no_chrome.py, and regeneratesmoon_engine.pyfrommoon_tk.pyto prove the two have not drifted - Files renamed so every name is English and says what it is:
avvia.bat→start.bat,avvia_tk.bat→start_tk.bat,gen_1.py→moon_tk.py,gen_cli.py→moon_cli.py,apply_web_v16.py→build_engine.py - The repository is English throughout — launcher output, engine warnings, Tk labels, OS dialog titles, module docstrings and test assertions. The GUI's runtime EN/IT switch is unaffected
- Documentation restructured: the two Italian guides became
docs/ARCHITECTURE.md,docs/CONFIGURATION.mdanddocs/ENGINEERING_NOTES.md, and the README carries a documentation index
- fuckingfast batches launched Chrome. Every front-end called
open_browser()once per worker at the top of the run, before reading a single URL, so a pure-HTTP batch paid ~1.5 s of Playwright driver boot and put a Chrome window on screen — visible, because Turnstile issues no token to a headless build, so datanodes forcesheadless=Falseand every launch is therefore seen - On the fallback path (no real Chrome found) each worker got its own Playwright Chromium while the extraction layer only ever used one shared context — N browsers, one of them used
- Transfer count showed the row cap (40) instead of the transfers in flight — a 124-file session reported "40 active"
- The Log tab rendered the transfer list on top of the log:
.files { display: grid }outranks the user-agent[hidden]rule - Progress rings always rendered empty: a CSS declaration beats an SVG presentation attribute,
so
setAttribute("stroke-dasharray")lost to the stylesheet - Loopback API replied 403 without draining the request body, so the next keep-alive request on that connection was parsed as garbage and answered 501
- The tkinter GUI (
moon_tk.py,start_tk.bat) andbuild_engine.py, the generator that producedmoon_engine.pyfrom it. Keeping it meant maintaining a second interface for the same engine — and, worse, it made the legacy GUI the source of truth for the modern engine.moon_engine.pyis now a normal module.pillowdrops out of the requirements with it apply_patch.py— the v14.1 → v14.8 migration patcher. Against the current tree it half-applies instead of failing: in testing it silently revertedmoon_cli.py's imports to the pre-BrowserGateAPIprep_assets.py— one-shot asset builder whose inputs (the raw renders) were never in the repo; the assets it produced are committed- Three orphan v14 screenshots in the repo root that nothing linked
moon_ui.py— the tkinter layer rebuilt from scratch: canvas-drawn cards, sliders, progress lanes, sparkline, status pill and per-file rowsapply_ui_v15.py— exact-string patch that swaps the GUI layer and leaves the async engine byte-identical- Generated brand assets (
assets/mark.png,assets/backdrop.png) withprep_assets.py
- Tk's canvas has no anti-aliasing and no alpha channel: arcs, rounded corners and glows render as steps and bands
- Absolute type and geometry: on a large monitor the interface stays small and the layout does not redistribute
- GUI settings split per method. One "Browsers" slider described an architecture that no longer existed: fuckingfast opens no browser at all, datanodes is Chrome + Turnstile. Three panels instead: common, datanodes, fuckingfast
- datanodes knobs (
Pages, captcha wait, Chrome path, API key) moved from environment variables to the GUI and are pushed into the extraction layer on every run throughmoon_extract.configure()— no moresetxand restart
- Back to one shared Chrome window. Separate windows meant separate identities, and
Cloudflare re-challenged each of them; one window and one profile means one
cf_clearance
- The shared browser died after ~80 sequential extractions and every later extraction
stayed broken for the rest of the session, because nothing checked whether it was still
alive.
open_browser()now verifiesis_connected()on every call and respawns the instance transparently - Too many heavy tabs on one window slowed everything down: tabs are pooled per lane instead of opened per extraction
- fuckingfast.co over curl_cffi — Chrome TLS fingerprint plus the
hx-redirectheader, ~0.25 s per link, no browser and no captcha. Without it Cloudflare answers 403 on every link - datanodes.to on real Chrome driven over CDP with a persistent profile, instead of the Playwright Chromium: the profile is the point, because the Turnstile clearance survives
- Optional datanodes premium API key — a single JSON GET, no browser, no captcha
moon_extract.py— the extraction layer split out ofgen_1.py, shared by the GUI and the CLI
curl_cffiis now a hard requirement for fuckingfast.co
- Stall detection with automatic lane kills for genuinely slow downloads
- Per-URL retry with exponential backoff
- Live telemetry with
.logand.jsonoutput - CLI variant (
gen_cli.py) for headless / multi-IP deployment - Ad overlay bypass and popup dismissal on datanodes.to
- Default browser worker count tuned to 16 for typical 40+ file sessions
- Improved dead-link detection so failures fail fast instead of timing out
- Resource blocking widened to cover more analytics/ad domains
- Resume interrupted downloads via
.tmpfiles instead of restarting - Range-header edge case when server returns 200 instead of 206
- Initial public release
- datanodes.to and fuckingfast.co provider support
- Tkinter GUI with dual progress bars and color-coded log