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), V2 (tagv2.0) and V3 (tagv3.0). V2 is where the numbering reset: the 14.x and 15.0 entries below keep the numbers they shipped with, andv14.1keeps its tag so its download link never breaks. V3 is a major because the interface was rebuilt, not because the engine changed — extraction, downloading and the CLI are the same code they were in 2.1.
- The
THEMEcolour palette at the top ofmoon_engine.py— fifteen constants with no references anywhere.moon_engine.pywas once generated from a tkinter GUI (moon_tk.pyviabuild_engine.py, both deleted in5f172d3), and the palette was that GUI's, compiled into the output; deleting the generator never touched what it had produced. The live interface takes its colours from CSS custom properties inweb/styles.css, which shares not one hex value with the block. Thanks to @AashishGupta2007 (#145, #159).
ruffran five times per pull request, once inside each Python version of the byte-compile matrix, checking identical work every time — the linter's result does not vary with the interpreter it is installed under, andruff.tomlsetstarget-version = "py310"regardless. It is now a standalone job that runs once. Theruff==0.16.1pin is unchanged, which is the point: an unpinned linter turns unrelated pull requests red the day upstream adds a rule. Thanks to @nightcityblade (#81, #158).- A test assertion that could not fail.
assert result["ok"] == len(urls)was shared by four tests intests/test_no_chrome.py. Through the tworun_clicallersokwas fabricated — the stubs write no files, sodone == 0and the helper substitutedlen(urls), comparing a number with itself. Through the tworun_enginecallers it is real, taken from the engine's own counter. It is now asserted inline in the engine tests only, so the vacuous half is gone and the half that works still fails when the engine drops a URL. Thanks to @XEDAB (#155, #157).
moon_cli.pynow reports the run's outcome in its exit code. It previously exited 0 whenever the run completed, whether one file failed or all of them did, and 0 on Ctrl-C — so a script had no way to tell success from total failure. The codes are now0all files succeeded,1some failed (also a run stopped by a full disk, and an interrupt),3every URL failed, and2a pre-flight problem that stopped the run from starting. All four are documented indocs/CLI.mdand covered bytests/test_cli_exit_codes.py. Thanks to @AdvaitVarhade (#32, #153).
- The two pre-flight errors in
moon_cli.pymoved from exit code 1 to 2 — a missing--urlsfile and a file containing no URLs. This matches argparse, which already exits 2 on a usage error, so the CLI is now consistent with its own parser. Scripts testing for a non-zero status are unaffected; one testing for1specifically will need updating (#153).
- A full destination disk no longer burns bandwidth it cannot write.
ENOSPCwas caught by the same catch-all that handles ordinary transfer errors, so the queue kept going and the retry machinery kept re-fetching data that could never land — measured on the run that opened the issue: 46 files, each downloading for ~236s before failing, ~12 GB pulled and discarded, with nothing on screen saying why. The firstENOSPCis now detected byerrnoand published as a run-level fatal state; new queue 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.tmpand stay resumable, and only the URL that triggered it is recorded as failed — files stopped as a consequence are reportedabortedrather than each being blamed individually. Writes now loop over amemoryview, because a short write on a nearly-full disk returns a count instead of raising and would otherwise truncate the file silently. Thanks to @shard872 (#116, #150). - Stop now interrupts downloads already in flight.
Engine.stop()set the stop flag and closed Chrome, but a transfer that had already started ran to completion — on a large file the button appeared to do nothing for minutes. The engine now keeps a registry of the kill event belonging to each active download and signals them withloop.call_soon_threadsafe(kill_evt.set), which is what setting anasyncio.Eventfrom the calling thread requires. Entries are discarded in afinally:so the set cannot grow over a long run. A stop is reported asstoppedrather than as a stall kill, so the retry path no longer re-queues the file and undoes the stop; the partial.tmpis kept and stays resumable, andfailed_links.txtis not written. Thanks to @Allen58562 (#65, #149).
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.
- 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#downloadRevealand#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 secondPOST /downloadre-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 appear once Cloudflare has been answered, and 22s is not a human
budget. It is
max(step-1 budget, manual captcha budget)— the GUI's Captcha slider, 30-600s. - fuckingfast.co put Turnstile in front of
POST /f/<id>/go. The endpoint answers403 captcha verification failedwithout acf-turnstile-responsetoken, which no TLS fingerprint can mint. Ruled out first: it is not the TLS impersonation (the page itself returns 200) and not a missing session (same cookies, and an empty token field, both still 403). There is no official alternative — no API, no docs, no premium tier, no registration, and the bundle exposes only upload endpoints. Links that are refused now fall through to the browser the app already opens for datanodes. A batch fuckingfast still serves over plain HTTPS never opens a window, so the rule thattests/test_no_chrome.pyexists to protect still holds. - The Turnstile auto-click had no target on fuckingfast. The widget locator
looked for
.cf-turnstileas a class and for an iframe whosesrccontainschallenges.cloudflare; fuckingfast uses it as an id and its challenge iframe carries nosrcat all, so both lookups missed and the box was never clicked — visible on screen, sitting there untouched. It now also matches#cf-turnstileand[data-sitekey]. - The fuckingfast page usually has nothing to solve. Its trigger is
click[!!window.turnstileToken || !!window.dlCleared]— note the OR — so waiting for a token waits for something that often never comes. The flow now clicks rather than waits, closes the ad tab the first click opens, and presses again, which is what a person does. Measured: the flag never arms and the click submits anyway. - Per-link browser contexts are gone from that path. The first version gave
each link its own
browser.new_context()— the v14.6 mistake already recorded aboveacquire_lane, where several identities from one IP read as a bot farm and Turnstile answers with a hard failure. It goes through the same pooled context datanodes uses: one profile, one cookie jar, onecf_clearance. - Popup and route handlers are page-scoped. Registering them on the shared context added one more of each per link; after the first extraction the next one stalled. Bound to the page, they die with it. This is what took the sequence from 1/3 to 3/3.
- datanodes: 5/5 through the CLI end to end, and 2/2 extraction
- fuckingfast: 3/3 consecutive, 6.8-9.7s per link, auto-solve only
(
MOON_DN_CAPTCHA_WAIT=0, so no manual tick was possible) - both providers together: 4/4
pytest tests/ -q: 24 passed
constraints.txt, the tested dependency set including transitives, for a reproducible install:pip install -r requirements.txt -c constraints.txt.docs/QUICKSTART.mddocuments both modes (#136, @PomPomSaturin)- A
dependency-contractCI job that fails when any runtime requirement lacks an upper bound, and installs both the constrained and unconstrained sets underpip check. The bounds were added once in #12 and silently lost in an unrelated engine rewrite; this is what stops that happening again (#136, @PomPomSaturin) - CI now runs on Python 3.13 and 3.14 as well as 3.10-3.12. The project advertises 3.10+ and two of the versions covered by that promise had never been exercised — including the one development happens on. All five pass (#129, @Vam-si-krish)
- 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 their state: what is moving gets room and a brighter name, what has finished tightens and recedes.
- Filter box and state chips over the transfer list. Presentation only — rows are hidden, and no count the engine owns is recomputed.
- Drag and drop of links or a
.txtanywhere on the window, appended rather than replacing what is already there. - Keyboard shortcuts —
Ctrl+Enter,Ctrl+O,/,?,Esc— with a shortcut panel. - Copy the source link per row, and an end-of-run summary that reads the last snapshot and can copy every failed link.
- Idle motion. The blooms drift, a glint travels the topbar hairline, the wordmark catches a sheen and Start shows one while it waits. All slow, and none of them claiming that anything is happening.
- One higher-contrast palette. The dimmest text tier moves from
#55677fto#97a9c1and the background photograph is pushed back — a real legibility gain in a bright room. - The speed plot. The head dot leaves the SVG: the plot is drawn with
preserveAspectRatio="none", which was rendering a<circle>as a flat ellipse. The area wash comes down, and a recessive hairline marks the window mean, since a plot scaled to its own peak cannot otherwise tell steady from spiky. It is drawn only when there is signal to average. - View Transitions on the Transfers/Log swap, behind a capability check.
- Scroll-driven edge fades on the scrollers, applied to the edge that actually has content past it; when a list does not overflow there is no mask at all.
- The
prefers-reduced-motionblock is gone, deliberately. Windows reports "reduce" whenever its own animation setting is off, and every version of honouring that here removed the thing the release is for — first the whole design, then the cold open, then the logo orbit and the wordmark sheen. The GUI now animates for everyone. Nothing flashes and every loop is slow, but this is a stated choice rather than an oversight, and a motion preference is the obvious thing to add if anyone asks for one. VERSIONwas stillv2.0after the 2.1 release, so the app under-reported itself for a whole version. It now tracks the tag again, atv3.0.
- The live speed read about nine times the real rate. It divided the bytes in a 3-second
window by the age of the oldest retained sample, which on bursty delivery collapses to a
fraction of a second and publishes the burst's peak. Measured: 38 MB/s on screen against a
median 4.2 MB/s in the same run's own report. It now divides by the observation window —
min(3.0, time since start)— in all three places that carried the expression, includingmoon_cli.py, which keeps its own copy (#142, @XEDAB) - The proxy chip said "no proxy" until a run started, even with a valid
proxies.txtpresent — wrong information rather than missing information, on the one control whose failure is silent. Three states are now distinguished: no file, a file yielding nothing usable, and N loaded. The per-line parser was extracted so the pre-run count andProxyPool.load()cannot disagree (#132, @8nt0n) - A failing browser worker aborted its siblings without saying which one failed. Both gathers now collect exceptions and report each with its worker id, matching the straggler gathers that already did (#143, @PomPomSaturin)
- Runtime dependencies have upper bounds again —
aiohttp>=3.9,<4,playwright>=1.40,<2,curl_cffi>=0.7,<1, with the lower bounds left where they were. Dependabot is set toincrease-if-necessaryso it stops proposing floor-only raises, which forbid old versions rather than permitting new ones (#136, @PomPomSaturin) - Every
except Exception:in the extraction and engine layers now names what it catches or says why it cannot. The last slice narrows the datanodes page flow toPlaywrightErrorand the CDP probe to(URLError, OSError, JSONDecodeError, KeyError), completing the 30-handler cleanup begun in #54 across four slices and three contributors (#130, @AdvaitVarhade) - Nothing said that proxies cover downloads only.
_PROXY_POOL.next()is consulted in one place —download_file— so the download session is proxied and nothing else is: the datanodes Chrome that answers the Turnstile challenge and the fuckingfastcurl_cffisession both connect directly, from the user's own address, on every run.README.md,docs/CLI.md,docs/CONFIGURATION.mdanddocs/FAQ.mdnow state the scope, and name the symptom it produces: pages opening normally while every download fails is an unusable proxy list, not a broken extractor (#128, @basisworks) - Closing the app mid-run left Chrome alive. Teardown only happened when a run ended
normally:
Engine.stop()set a flag and returned, the worker ran on a daemon thread, and the interpreter tore it down beforeBrowserGate.aclose()could execute — so the browser survived, still holding its profile lock.stop()now closes the gate from the caller's thread and waits briefly for the worker, and the gate refuses to reopen once closed, so a late worker cannot launch a second Chrome on the way out (#122, @Guflly) - The preview mock no longer ships identifiable sample content. Its filenames named a
specific title and a repack site, its link list carried them in readable URLs, and its
destination path was a real one — and every screenshot in the README is rendered from
that mock, so all of it was on display on the project front page. The sample data is now
neutral (
sample-archive.partNN.rar, opaque link ids,D:\downloads) and the screenshots have been regenerated from it. - The mock reported
v2.0 - preview, so the screenshots taken for the V3 release announced the previous version.
- CI runs on Python 3.10, 3.11 and 3.12, and lints with
ruff. The matrix usesfail-fast: falseso one version failing still reports the others.ruff.tomlselectsE/F/W/Iand parks the rules that fire on the codebase as it stands, each with its count and the reason — the goal is a baseline that catches regressions, not a reformat (#69, @Moferanoluwa) - Documentation-only pull requests are checked against the real CLI parser. A new
Docs CLI Checkworkflow triggers on**.mdand runstests/test_docs_cli_flags.py, which reads the true flag set frommoon_cli.py --helpat test time and fails on anymoon_cli.pyinvocation in a tracked Markdown file that uses a flag the parser does not accept. Reading--helprather than hardcoding means adding a flag never requires touching the test (#70, @Moferanoluwa) CONTRIBUTING.mdstates what counts as a contribution here, what gets labelledinvalidorspam, and that the bar does not move during Hacktoberfest (#72)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
- Two links resolving to the same filename shared one
.tmpand corrupted each other. The destination was derived from the filename alone, so two transfers opened the same partial file and raced to rename it — visible on Windows asWinError 32, silent on POSIX, whereos.replacedoes not refuse and the interleaved survivor was reportedok. Names are now reserved case-insensitively when each record is registered, before either transfer is scheduled, and the rename is reported in both front-ends (#119, @felix-windsor) - Mid-transfer failures never reached the screen. A dead proxy, a dropped connection or a
refused resume wrote a note that only appeared in
moontech_*.logafter the run, so a run sat at0 KB/swith no explanation.download_filenow takes an optionalon_eventcallback; the engine passes its log, the CLI prints, and the report keeps everything it had. Also fixes anUnboundLocalErrorwhen a connection failed before the first byte arrived (#120, @XEDAB) - The eight
except Exception:handlers in the Chrome-lifecycle slice ofmoon_extract.pyare narrowed or explained; the CDP attach now catchesPlaywrightErrorand the Chrome process teardown(subprocess.TimeoutExpired, OSError)(#118, @AashishGupta2007) - Datanodes extraction hung on every link and failed silently. The host now serves step 2
as a chain — Free Download / Standard Speed reveals Start Download / Your file is ready,
and only the second starts the transfer — while the trigger handler latched after one click.
The page was never unreadable:
DN_STEP2_JSmatched the new button on every poll and the latch discarded it, so each link burned the full 420s budget and returned nothing. The handler now tracks which labels it has clicked instead of whether it has clicked, so each new trigger in the chain gets exactly one press, capped byDN_STEP2_MAX_CLICKS(#111) - An unsupported host was retried like a network failure. A link from any host the
program does not handle matched neither dispatcher branch, so nothing ran, nothing was
logged, and the URL went back on the queue to be tried again with backoff — burning two
extra attempts and a worker slot on something that could never succeed, then reporting it
as an ordinary failure. It now fails on the first dispatch with a message naming the host
and the supported ones, and
moontech_*.logrecords it distinctly from an extraction failure. The host names now live once inmoon_extract.pyinstead of four times (#108, @Vam-si-krish) - The headless CLI example in
README.mddocumentedpython moon_cli.py <url> ... -o <folder>: positional URLs the parser never accepted, and a-oshort flag that does not exist. It now reads--urls/--output, which are the real flags and both required (#53, @tomatotomata) - The documentation guard could not see the line it was written to catch.
FLAG_REintests/test_docs_cli_flags.pymatched only--longflags, so the bogus-oabove passed every check. It now validates single-dash flags against the parser as well (#104, @kocaemre) - A partial download was discarded in silence. When a server answers
200instead of206it is refusing the resume request, and the code correctly restarts from zero — but said nothing, so a multi-gigabyte transfer appeared to begin again for no reason. The restart is now recorded on the file's record and appears inmoontech_*.log, with a regression test that fakes the200response without touching the network (#98, @XEDAB) - The CI byte-compile list was hand-maintained and had drifted. The job named
"Byte-compile every module" compiled an explicit list that stopped at
tests/test_no_chrome.py, somoon_download.py— the shared download engine both front-ends import — was never compiled, along with two newer test files. It now derives the list fromgit ls-files '*.py', which cannot drift and pulls in nothing untracked. The workflow also watches its own file now, so a change to it is actually checked (#77, @darlenepolek) - The eight
except Exception:handlers in the first slice ofmoon_extract.pyare narrowed or documented: thecurl_cffiimport guard catchesImportError, the Playwright probes catchPlaywrightError, and the ones that must stay broad say why. The deferred playwright import is preserved, so a fuckingfast-only batch still never imports it (#82, @AdvaitVarhade) - The elapsed-time clock kept counting after a run finished.
snapshot()computedelapsed_sfromnow - t0unconditionally, so the GUI carried on ticking once the last file had landed. The engine now records_t_endin_on_done()— the single exit point for both a normal finish and a crash — resets it instart(), and freezeselapsed_satt_end - t0once a run has ended (#73, @RubenSanosh) docs/ARCHITECTURE.mdstill saidmoon_engine.pyandmoon_cli.pyeach carry their own copy of the download engine. #41 made that untrue; the file table and the closing summary now describemoon_download.py(#68, @Moferanoluwa)- The three
except Exception:handlers inmoon_engine.pynow say why they are there. The report-save handler is narrowed toexcept (OSError, TypeError)— a write failure or a non-serializable field — instead of swallowing everything after a run that succeeded (#71, @Moferanoluwa) AUTHORS.mdlinked to a contributor profile that no longer exists, and cited an issue number in a column that otherwise lists merged pull requests. The contribution stays credited; only the dead link and the reference were corrected (#67)- 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