DataNodes
DataNodes automates the click-through chain of datanodes.to download pages to extract the final direct download URL — driving up to eight browsers at once, blocking the popups, clicking the timers, and reporting progress in a Rich terminal UI.
Open source: github.com/Zephyr73/DataNodes
Executive Summary & Problem Statement
File hosts like datanodes.to don't hand you the file. They present a gauntlet:
- A 5-second countdown before the primary action unlocks.
- A button cascade — every intermediate "continue" regenerates the DOM and re-arms the timer.
- Ad popups, new tabs, and click-jack layers that block or hijack the real CTA.
- A protected, dynamically-obfuscated final download URL that changes per session.
Doing this by hand for a batch of files is soul-destroying and error-prone. The goal: turn one pasted link into a working direct URL a download manager (IDM, JDownloader) can consume — fully unattended, with observable progress.
Architecture Overview
A queue-processor over Playwright with heavy anti-interference infrastructure:
input links ──► (url, priority) queue ──► worker pool (8 contexts)
│
per-context: ad block → timer drive → cascade click
│
obfuscation decode → final direct URL
│
retry/backoff ──► result sink ──► Rich progress table
| Component | Role |
|---|---|
| Playwright pool | Up to 8 isolated browser contexts, one per link |
| Context isolation | Separate storage, routes, and user agents per worker |
| Ad/popup blocking | Request interception + popup event handler capture |
| Cascade driver | Timer + button click state machine per page |
| Decoder | Reverses the page's JS-level URL obfuscation |
| Retry layer | Per-link retry with exponential backoff |
| Rich UI | Live per-worker progress, status, and throughput |
Parallel Worker Pool
Requests are processed through a queue fed into a pool of up to eight fully isolated Playwright contexts. Each context carries:
- its own storage state (cookies, localStorage) — a page cannot leak session state between links,
- its own route overrides — deterministic interception instead of shared page routing,
- a dedicated user agent, so the host sees eight distinct visitors rather than one script.
This matters for two reasons: the host rate-limits per session, and its obfuscation encodes per-load state — sharing a context would corrupt unrelated requests. The pool makes the scraper at worst as fast as the slowest link, and failure in one context cannot cascade to the others.
Ad Blocking & Popup Control
The first thing every context does is take the site's weapons away:
async def block_ads(route: Route, request: Request) -> None:
if request.resource_type == "image" and any(
ad in request.url for ad in ADBLOCK_PATTERNS
):
await route.abort()
return
await route.continue_()
- Request interception — known ad/tracker host patterns are aborted at the network level before they render; the page gets a clean, fast DOM.
- Crowded popup events — every
page.on("popup")handler either closes the new window or logs it, so advertorial tabs can't steal focus or spawn a second timer state.
Between them, the cascade scripts only ever see their own buttons, never a popup layered on top.
Driving the Cascade
Each page is driven as a tiny state machine: wait → click → wait → click. The driver anchors on the page's own countdown text then triggers the primary button, re-running the routine each time the DOM regenerates until the final URL is reachable:
# Core wait-for-countdown routine
async def wait_for_timer(page, timeout=10_000):
deadline = page.locator("text=Download")
await deadline.wait_for(state="visible", timeout=timeout)
The cascade is resilient by design: every step waits on element visibility
rather than arbitrary sleeps, and a stock check (generic_button_ready) guards
against a partially-rendered page false-positive. When the final link is
available, the sequence terminates instead of blind-clicking onward.
URL Deobfuscation
The final URL is not in the HTML. It is assembled by the page's runtime JavaScript — concatenated, base64-encoded, or key-rotated fragments that only exist after the cascade completes. The scraper reads the post-DOM state left by the page's own script execution and reverse-assembles the link from the fragments it staged, producing a clean, resumable URL that a download manager can fetch directly.
Retries & Failure Handling
Every link gets bounded retries with exponential backoff (and jitter), so a transient 5-second-timer race or popup injection retries instead of failing:
- Per-link failure budget, isolated from the pool's other links.
- Backoff between attempts gives the host time to reset per-session state.
- A link that exhausts retries is marked
FAILEDin the result sink with the last error; the pool continues with the remaining queue.
Rich Progress Reporting
A Rich-rendered live table tracks the pool: per-context status, current action, retry count, and completed URLs. Because the table is a view over the result sink (not a per-context print), the UI stays the arbiter of truth while workers mutate only their own slots:
╭─ DataNodes ─────────────────────────────╮
│ link 0123 … direct URL │ SUCCESS │ 2.3s │
│ link 0456 … retry 2/3 │ · │ 1.1s │
│ link 0789 … popup bloc │ RUNNING │ 4.0s │
╰─────────────────────────────────────────╯
Design Decisions & Tradeoffs
- Playwright over requests — this site requires a live browser: timers, runtime-assembled URLs, and per-session state are all post-JS. Paying for browser overhead buys correctness that an HTTP client cannot reach.
- Context isolation over a shared session — 8 contexts × own state is slower to boot but immune to cross-link session poisoning, and effectively distributes rate limits.
- Interception-level ad blocking over cosmetic hiding — aborting requests keeps the DOM clean for selectors; hiding with CSS would leave broken layout to stumble through.
- Element-visibility waits over sleep() — deterministic timing that holds up under slow networks without racing the site's own timers.
- Explicit error classification —
FAILEDvsRETRYvsDONEin the sink gives the operator a decision surface rather than a silent script.
Engineering Discipline
- Bounded concurrency — the pool caps at 8 contexts, matching machine resource limits and host suspicion thresholds.
- Isolated failure domains — one bad page cannot evict or corrupt its siblings.
- Observable by default — the Rich table exposes every worker's state live, so operators trust rather than babysit.
- Reusable output — clean direct URLs drop straight into IDM / JDownloader without transformation.
Technical Competencies Demonstrated
- Headless browser orchestration — Playwright worker pools with fully isolated contexts, storage, and user agents.
- Anti-ad-unit engineering — network-level interception, popup capture, and DOM hygiene for reliable selector work.
- Coordinated state-machine driving — countdown timers + regenerating button cascades as deterministic waits and clicks.
- Runtime-level deobfuscation — reassembling URLs from post-execution page state rather than static markup.
- Resilient batch processing — retries, backoff, failure isolation, and a live progress terminal.