#!/usr/bin/env -S uv run # /// script # requires-python = ">=3.12" # dependencies = ["httpx"] # /// """ RapidGator (rapidgator.net) downloader — reverse-engineered from JDownloader's RapidGatorNet plugin. Usage: ./rg_download.py [--user EMAIL] [--pass PASSWORD] [--out DIR] [--2captcha KEY] Premium accounts use the clean v2 API path (no captcha, full speed). Free/anonymous downloads scrape the website; reCAPTCHA v2 or Cloudflare Turnstile is required — a 2captcha API key is strongly recommended (manual solving isn't feasible for these captcha types). """ import re import sys import os import time import argparse import getpass from pathlib import Path from urllib.parse import urlparse, unquote import httpx # ── constants ──────────────────────────────────────────────────────────────── API_BASE = "https://rapidgator.net/api/v2/" SITE_BASE = "https://rapidgator.net" SUPPORTED_DOMAINS = {"rapidgator.net", "rapidgator.asia", "rg.to"} # JD plugin regex: (?i)/file/([a-z0-9]{32}|\d+) FILE_ID_RE = re.compile(r"(?i)/file/([a-z0-9]{32}|\d+)") HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept-Language": "en-US,en;q=0.8", } # ── helpers ────────────────────────────────────────────────────────────────── def extract_file_id(url: str) -> str | None: m = FILE_ID_RE.search(url) return m.group(1) if m else None def check_domain(url: str): host = urlparse(url).hostname or "" host = host.removeprefix("www.") if host not in SUPPORTED_DOMAINS: sys.exit(f"Unsupported domain '{host}'. Supported: {', '.join(sorted(SUPPORTED_DOMAINS))}") def _api_get(session: httpx.Client, endpoint: str, params: dict) -> dict: """GET request to the v2 API; raises on HTTP error, returns parsed JSON.""" r = session.get(f"{API_BASE}{endpoint}", params=params, headers=HEADERS, timeout=30) try: data = r.json() except Exception: r.raise_for_status() raise return data def _api_check(data: dict, context: str = "") -> dict: """Raise a RuntimeError if the API returned an error status.""" status = data.get("status", "") if status != "success": err = data.get("details") or data.get("error") or data raise RuntimeError(f"API error{' (' + context + ')' if context else ''}: {err}") return data.get("details", {}) # ── API calls (mirroring JD's loginAPI / requestFileInformationAPI / handlePremium_api) ── def api_login(session: httpx.Client, email: str, password: str) -> str: """GET /user/login → returns session_id (used as ?token= in all other calls).""" data = _api_get(session, "user/login", {"login": email, "password": password}) details = _api_check(data, "login") # JD stores the field named "session_id" (PROPERTY_sessionid constant) sid = details.get("session_id") or details.get("token") if not sid: raise RuntimeError(f"Login succeeded but no session_id in response: {details}") return sid def api_user_info(session: httpx.Client, token: str) -> dict: """GET /user/info?token= → account details (is_premium, premium_end_time, …).""" data = _api_get(session, "user/info", {"token": token}) return _api_check(data, "user/info") def api_file_info(session: httpx.Client, token: str, file_id: str) -> dict: """GET /file/info?token=&file_id= → file metadata (name, size, hash, …).""" data = _api_get(session, "file/info", {"token": token, "file_id": file_id}) details = _api_check(data, "file/info") # Nested under "file" key return details.get("file") or details def api_file_download(session: httpx.Client, token: str, file_id: str) -> str: """GET /file/download?token=&file_id= → direct download_url.""" data = _api_get(session, "file/download", {"token": token, "file_id": file_id}) details = _api_check(data, "file/download") url = details.get("download_url") if not url: raise RuntimeError(f"No download_url in response: {details}") return url # ── 2captcha ───────────────────────────────────────────────────────────────── class TwoCaptcha: """ Thin wrapper around the 2captcha API v2. Supports reCAPTCHA v2 (RecaptchaV2TaskProxyless) and Cloudflare Turnstile (TurnstileTaskProxyless) — the two captcha types used by RapidGator's free download page. """ API = "https://api.2captcha.com" POLL_INTERVAL = 5 POLL_TIMEOUT = 180 def __init__(self, api_key: str, client: httpx.Client): self.api_key = api_key self.client = client def _call(self, endpoint: str, payload: dict) -> dict: r = self.client.post( f"{self.API}{endpoint}", json={"clientKey": self.api_key, **payload}, headers={"Content-Type": "application/json"}, timeout=30, ) r.raise_for_status() data = r.json() err_id = data.get("errorId") or 0 if err_id and err_id != 0: raise RuntimeError( f"2captcha error: {data.get('errorCode') or data.get('errorDescription') or data}" ) return data def _poll(self, task_id: int) -> str: """Poll until ready, return solution token.""" print(f" 2captcha task {task_id} submitted, polling…", flush=True) deadline = time.monotonic() + self.POLL_TIMEOUT while time.monotonic() < deadline: time.sleep(self.POLL_INTERVAL) result = self._call("/getTaskResult", {"taskId": task_id}) if result.get("status") == "ready": token = result["solution"]["token"] print(f" 2captcha solved (token length: {len(token)})") return token print(" …waiting for solution") raise RuntimeError(f"2captcha timed out after {self.POLL_TIMEOUT}s") def solve_recaptcha_v2(self, site_key: str, page_url: str) -> str: """Submit a RecaptchaV2TaskProxyless and return the g-recaptcha-response token.""" task_id = self._call("/createTask", { "task": { "type": "RecaptchaV2TaskProxyless", "websiteURL": page_url, "websiteKey": site_key, } })["taskId"] return self._poll(task_id) def solve_turnstile(self, site_key: str, page_url: str) -> str: """Submit a TurnstileTaskProxyless and return the cf-turnstile-response token.""" task_id = self._call("/createTask", { "task": { "type": "TurnstileTaskProxyless", "websiteURL": page_url, "websiteKey": site_key, } })["taskId"] return self._poll(task_id) # ── free download (website scrape) ──────────────────────────────────────────── _RECAPTCHA_RE = re.compile( r'class=["\']g-recaptcha["\'][^>]*data-sitekey=["\']([^"\']+)["\']' r'|data-sitekey=["\']([^"\']+)["\'][^>]*class=["\']g-recaptcha["\']' ) _TURNSTILE_RE = re.compile( r'class=["\']cf-turnstile["\'][^>]*data-sitekey=["\']([^"\']+)["\']' r'|data-sitekey=["\']([^"\']+)["\'][^>]*class=["\']cf-turnstile["\']' ) _DL_URL_RE = re.compile( r"'(https?://[A-Za-z0-9\-_]+\.[^/]+//\?r=download/index&session_id=[A-Za-z0-9]+)'" r"|\"(https?://[^/]+/download/[^<>\"']+)\"" ) # X-Requested-With is required by the AJAX endpoints — they return empty without it _XHR_HEADERS = {"X-Requested-With": "XMLHttpRequest"} def _first_group(m: re.Match | None) -> str | None: if not m: return None return next((g for g in m.groups() if g), None) def free_download( session: httpx.Client, file_id: str, file_name: str, solver: TwoCaptcha | None, out_dir: Path, ): """ Website-based free download flow (mirrors JD handleDownloadWebsite / free path). Actual page flow discovered by inspection: 1. GET /file/ → server sets PHPSESSID + file_id cookies; page JS has: var fid = ; var secs = 180; var startTimerUrl = '/download/AjaxStartTimer'; 2. GET /download/AjaxStartTimer?fid= (XHR) → {"state":"started", "sid":""} 3. Wait `secs` seconds (free-tier countdown) 4. GET /download/AjaxGetDownloadLink?sid= (XHR) → poll until {"state":"done"} 5. GET /download/captcha → HTML with reCAPTCHA v2 or Turnstile widget 6. POST /download/captcha with solved token → redirect to the actual download URL OR response body contains it """ file_url = f"{SITE_BASE}/file/{file_id}" # ── step 1: load file page ──────────────────────────────────────────────── print(f"Loading file page: {file_url}") r = session.get(file_url, headers=HEADERS, timeout=30) r.raise_for_status() page_html = r.text # Check for premium-only direct link already embedded (logged-in premium path) pm = re.search(r"var\s+premium_download_link\s*=\s*'(https?://[^']+)'", page_html) if pm and pm.group(1): stream_download(session, pm.group(1), out_dir, file_name) return # Extract numeric fid (set by server in page JS and cookie, distinct from hex URL ID) fid_m = re.search(r"var\s+fid\s*=\s*(\d+)", page_html) if not fid_m: sys.exit("Could not find numeric fid in page JS — page layout may have changed.") numeric_fid = fid_m.group(1) # Extract client-side wait timer (default 180 for free users) secs_m = re.search(r"var\s+secs\s*=\s*(\d+)", page_html) wait_secs = int(secs_m.group(1)) if secs_m else 180 print(f" Numeric fid={numeric_fid}, wait={wait_secs}s") if wait_secs > 600: sys.exit(f"Server wait is {wait_secs}s — IP appears rate-limited. Try again later.") # ── step 2: start the server-side timer ────────────────────────────────── print("Starting download timer…") r2 = session.get( f"{SITE_BASE}/download/AjaxStartTimer", params={"fid": numeric_fid}, headers={**HEADERS, **_XHR_HEADERS, "Referer": file_url}, timeout=30, ) r2.raise_for_status() try: timer_data = r2.json() except Exception: sys.exit(f"AjaxStartTimer returned non-JSON: {r2.text[:300]!r}") if timer_data.get("state") == "error": sys.exit(f"AjaxStartTimer error: {timer_data.get('code') or timer_data}") if timer_data.get("state") != "started": sys.exit(f"Unexpected AjaxStartTimer state: {timer_data}") sid = timer_data.get("sid") if not sid: sys.exit(f"AjaxStartTimer response missing sid: {timer_data}") print(f" sid={sid}") # ── step 3: wait for the server countdown ──────────────────────────────── print(f" Waiting {wait_secs}s…", flush=True) for remaining in range(wait_secs, 0, -5): time.sleep(min(5, remaining)) print(f"\r {remaining - min(5, remaining)}s remaining… ", end="", flush=True) print() # ── step 4: confirm server is ready ────────────────────────────────────── print("Confirming download ready…") for attempt in range(10): r3 = session.get( f"{SITE_BASE}/download/AjaxGetDownloadLink", params={"sid": sid}, headers={**HEADERS, **_XHR_HEADERS, "Referer": file_url}, timeout=30, ) r3.raise_for_status() try: link_data = r3.json() except Exception: sys.exit(f"AjaxGetDownloadLink returned non-JSON: {r3.text[:300]!r}") dl_state = link_data.get("state") if dl_state == "done": break if dl_state == "error": code = link_data.get("code", "") if "wait" in str(code).lower(): print(f" Server not ready yet, retrying in 5s… ({code})") time.sleep(5) continue sys.exit(f"AjaxGetDownloadLink error: {link_data}") print(f" Unexpected state '{dl_state}', retrying…") time.sleep(5) else: sys.exit(f"AjaxGetDownloadLink never returned done. Last response: {link_data}") # ── step 5: load captcha page ───────────────────────────────────────────── captcha_url = f"{SITE_BASE}/download/captcha" print("Loading captcha page…") r4 = session.get(captcha_url, headers={**HEADERS, "Referer": file_url}, timeout=30) r4.raise_for_status() captcha_html = r4.text if not captcha_html.strip(): sys.exit("Captcha page returned empty — session may have expired. Try again.") # Detect captcha type and extract site key recaptcha_key = _first_group(_RECAPTCHA_RE.search(captcha_html)) turnstile_key = _first_group(_TURNSTILE_RE.search(captcha_html)) # Also extract CSRF token from if present csrf_m = re.search(r'