Download configs/mega_folder_dl.py from AdwolfCzar/minih33-smoothloopv3: direct link, hf CLI and curl.
- Browser
- Download file 6.78 kB
-
https://huggingface.co/AdwolfCzar/minih33-smoothloopv3/resolve/main/configs/mega_folder_dl.py
- Command line
-
hf download hf://AdwolfCzar/minih33-smoothloopv3/configs/mega_folder_dl.py
-
curl -L -o mega_folder_dl.py https://huggingface.co/AdwolfCzar/minih33-smoothloopv3/resolve/main/configs/mega_folder_dl.py
6.78 kB
| #!/usr/bin/env python3 | |
| """Paralelo: baixa um link publico de PASTA do mega.nz. | |
| megadl e sequencial (~0,6 MB/s aqui). Varias conexoes contornam o throttle | |
| por-conexao do Mega. Pula arquivos ja completos (compara tamanho). | |
| """ | |
| from __future__ import annotations | |
| import argparse, base64, json, os, struct, sys, threading, time, urllib.request | |
| from concurrent.futures import ThreadPoolExecutor | |
| from Crypto.Cipher import AES | |
| from Crypto.Util import Counter | |
| API = "https://g.api.mega.co.nz/cs" | |
| _seq = [0] | |
| _lock = threading.Lock() | |
| def b64d(s: str) -> bytes: | |
| s += "=" * (-len(s) % 4) | |
| return base64.b64decode(s.replace("-", "+").replace("_", "/")) | |
| def api(payload, folder_id, tries=6): | |
| for attempt in range(tries): | |
| with _lock: | |
| _seq[0] += 1 | |
| sid = _seq[0] | |
| req = urllib.request.Request( | |
| f"{API}?id={sid}&n={folder_id}", | |
| data=json.dumps(payload).encode(), | |
| headers={"Content-Type": "application/json"}, | |
| ) | |
| try: | |
| out = json.loads(urllib.request.urlopen(req, timeout=90).read()) | |
| except Exception: | |
| time.sleep(2 * (attempt + 1)); continue | |
| # Um inteiro negativo no lugar do objeto e um erro do Mega (-3 = tente de novo). | |
| if isinstance(out, int): | |
| out = [out] | |
| if isinstance(out[0], int): | |
| if out[0] in (-3, -4): | |
| time.sleep(3 * (attempt + 1)); continue | |
| raise RuntimeError(f"Mega API error {out[0]}") | |
| return out | |
| raise RuntimeError("Mega API: falhou apos varias tentativas") | |
| def node_keys(k_field: str, master: bytes): | |
| """O campo `k` pode trazer varias entradas `handle:chave` separadas por `/` | |
| (a pasta e compartilhada por mais de um no). So uma decifra com a master key | |
| do link, entao devolvemos todas as candidatas e quem chama escolhe a que | |
| produz um atributo valido.""" | |
| ecb = AES.new(master, AES.MODE_ECB) | |
| out = [] | |
| for entry in k_field.split("/"): | |
| if ":" not in entry: | |
| continue | |
| enc = b64d(entry.split(":", 1)[1]) | |
| if len(enc) < 16: | |
| continue | |
| out.append(ecb.decrypt(enc[: len(enc) // 16 * 16])) | |
| return out | |
| def node_key(k_field: str, master: bytes) -> bytes: | |
| keys = node_keys(k_field, master) | |
| if not keys: | |
| raise ValueError("no key entry") | |
| return keys[0] | |
| def file_key(nk: bytes): | |
| w = struct.unpack(">8I", nk[:32]) | |
| key = struct.pack(">4I", w[0] ^ w[4], w[1] ^ w[5], w[2] ^ w[6], w[3] ^ w[7]) | |
| iv = struct.pack(">2I", w[4], w[5]) # 8 bytes de nonce | |
| return key, iv | |
| def attr_name(attr_b64: str, key: bytes) -> str | None: | |
| raw = b64d(attr_b64) | |
| dec = AES.new(key, AES.MODE_CBC, b"\0" * 16).decrypt(raw[: len(raw) // 16 * 16]) | |
| dec = dec.rstrip(b"\0") | |
| if not dec.startswith(b"MEGA"): | |
| return None | |
| try: | |
| return json.loads(dec[4:].decode("utf-8", "replace")).get("n") | |
| except Exception: | |
| return None | |
| def download(node, dest_root, folder_id, master, state): | |
| handle, key, iv, size, relpath = node | |
| out = os.path.join(dest_root, relpath) | |
| if os.path.exists(out) and os.path.getsize(out) == size: | |
| with _lock: | |
| state["skipped"] += 1 | |
| return | |
| os.makedirs(os.path.dirname(out), exist_ok=True) | |
| tmp = out + ".part" | |
| url = api([{"a": "g", "g": 1, "n": handle}], folder_id)[0]["g"] | |
| ctr = Counter.new(128, initial_value=int.from_bytes(iv + b"\0" * 8, "big")) | |
| aes = AES.new(key, AES.MODE_CTR, counter=ctr) | |
| with urllib.request.urlopen(url, timeout=180) as r, open(tmp, "wb") as f: | |
| while True: | |
| chunk = r.read(1 << 20) | |
| if not chunk: | |
| break | |
| f.write(aes.decrypt(chunk)) | |
| with _lock: | |
| state["bytes"] += len(chunk) | |
| os.replace(tmp, out) | |
| with _lock: | |
| state["done"] += 1 | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("link") | |
| ap.add_argument("--dest", required=True) | |
| ap.add_argument("--workers", type=int, default=12) | |
| a = ap.parse_args() | |
| folder_id, key_b64 = a.link.split("/folder/")[1].split("#") | |
| master = b64d(key_b64) | |
| nodes = api([{"a": "f", "c": 1, "r": 1, "ca": 1}], folder_id)[0]["f"] | |
| names, parents = {}, {} | |
| for n in nodes: | |
| parents[n["h"]] = n.get("p") | |
| if n["t"] == 1: | |
| names[n["h"]] = next( | |
| (nm for nk in node_keys(n["k"], master) | |
| if (nm := attr_name(n["a"], nk[:16]))), n["h"]) | |
| # Handle da pasta raiz do link: seu nome nao entra no caminho local | |
| # (megadl tambem o omite; manter igual permite retomar o que ele ja baixou). | |
| roots = {h for h in names if parents.get(h) not in names} | |
| def relpath_of(h): | |
| parts = [] | |
| while h in names and h not in roots: | |
| parts.append(names[h]); h = parents.get(h) | |
| return os.path.join(*reversed(parts)) if parts else "" | |
| jobs, undecodable = [], [] | |
| for n in nodes: | |
| if n["t"] != 0: | |
| continue | |
| chosen = None | |
| for nk in node_keys(n["k"], master): | |
| if len(nk) < 32: | |
| continue | |
| key, iv = file_key(nk) | |
| name = attr_name(n["a"], key) | |
| if name: | |
| chosen = (key, iv, name) | |
| break | |
| if chosen is None: | |
| undecodable.append(n["h"]) | |
| continue | |
| key, iv, name = chosen | |
| jobs.append((n["h"], key, iv, n["s"], os.path.join(relpath_of(n["p"]), name))) | |
| if undecodable: | |
| print(f"AVISO: {len(undecodable)} nos sem chave decifravel, ignorados", flush=True) | |
| total = sum(j[3] for j in jobs) | |
| print(f"{len(jobs)} arquivos, {total/1e9:.2f} GB, {a.workers} conexoes", flush=True) | |
| state = {"bytes": 0, "done": 0, "skipped": 0} | |
| t0 = time.time() | |
| stop = threading.Event() | |
| def ticker(): | |
| while not stop.wait(30): | |
| el = time.time() - t0 | |
| print(f"[{el:6.0f}s] ok={state['done']} skip={state['skipped']}/{len(jobs)} " | |
| f"{state['bytes']/1e6:8.0f} MB {state['bytes']/1e6/el:5.2f} MB/s", flush=True) | |
| threading.Thread(target=ticker, daemon=True).start() | |
| errors = [] | |
| with ThreadPoolExecutor(max_workers=a.workers) as ex: | |
| futs = {ex.submit(download, j, a.dest, folder_id, master, state): j for j in jobs} | |
| for f, j in futs.items(): | |
| try: | |
| f.result() | |
| except Exception as e: | |
| errors.append((j[4], repr(e))) | |
| stop.set() | |
| print(f"FIM em {time.time()-t0:.0f}s ok={state['done']} skip={state['skipped']} err={len(errors)}", flush=True) | |
| for p, e in errors[:40]: | |
| print("ERRO", p, e, flush=True) | |
| sys.exit(1 if errors else 0) | |
| if __name__ == "__main__": | |
| main() | |