Files
pokemon/scripts/list-acquire-from-urls.py
ginnoir 52ee1b98a1 Complete curated-list import automation and vault sync.
Adds list-import tooling, expands romhack-import for PC variants and patch batches, and syncs catalog with 2026-06-23 valhalla placements (Vanguard, Xenoverse, GBA patches, Infinity 3.3.1).
2026-06-23 03:57:50 -05:00

133 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Diff URL-list hacks against placed library + attempt direct downloads."""
import json, os, re, shutil, subprocess, sys, tempfile, unicodedata, urllib.request
from pathlib import Path
CATALOG = Path(__file__).resolve().parents[1] / "wiki" / "catalog.json"
INCOMING = "/storage1/igir/romhacks/incoming/list-import"
UA = "Mozilla/5.0 (homelab personal archival)"
# Unique games from the 7 source lists with best-known download URL (None = manual only).
ACQUIRE = [
# --- direct-download candidates ---
("Coral", "rom", "https://raw.githubusercontent.com/vbaemulator/GBC-Roms/main/Pokemon%20Coral%20Version%20v1.2.zip", "pokecommunity"),
("Poketale", "zip", "https://www.mediafire.com/file/n6yzxnclf2uys7c/Poketale.zip/file", "mediafire"),
("Vanguard", "archive", "https://www.mediafire.com/file/w9gy3qldi29imhk/Vanguard+3.0.12.7z/file", "mediafire"),
("Bizarre", "zip", "https://www.mediafire.com/file/lox59f4nubxflw6/Pokemon_Bizarre_ENGLISH.zip/file", "mediafire"),
("Spades and Clubs", "page", "https://senta-storage-system.neocities.org/main", "neocities"),
("Dreamstone Mysteries", "github", "https://github.com/dsmyst/dreamstone-mysteries", "github"),
("Giratina Strikes Back", "staging", None, "pokecommunity"),
# --- catalog-only / gated (reported, not fetched) ---
("Abstract", "manual", "https://www.pokecommunity.com/threads/pokémon-abstract-version.527324/", "pokecommunity"),
("Alchemist", "manual", "https://www.pokecommunity.com/threads/pokémon-alchemist.378280/", "pokecommunity"),
("Awakening", "manual", "https://mega.nz/221fb646-0a04-4316-a163-f9b532c89197", "mega"),
("AfricanVs", "have", None, "mega"),
("Bushido", "have", None, "eeveeexpo"),
("Consonancia", "manual", "https://www.pokecommunity.com/threads/pokémon-consonancia-full-game.537329/", "pokecommunity"),
("Concealed", "manual", "https://www.pokecommunity.com/threads/concealed.527325/", "pokecommunity"),
("Crown", "manual", "https://www.pokecommunity.com/threads/pokémon-crown.463821", "pokecommunity"),
("Decay", "have", None, "eeveeexpo"),
("Deserted", "have", None, "eeveeexpo"),
("Eon Guardians", "manual", "https://www.pokecommunity.com/threads/pokémon-eon-guardians.527913/", "pokecommunity"),
("Fire Ash", "manual", "https://www.pokecommunity.com/threads/pokémon-fire-ash.399096/", "pokecommunity"),
("Floral Tempus", "manual", "https://www.pokecommunity.com/threads/pokémon-floral-tempus-ex-1-9-4-released-12-gym-badges-elite-4-champion.409175/", "pokecommunity"),
("Hero Legacy", "manual", "https://www.pokecommunity.com/threads/pokémon-hero-legacy-eng-de-now-with-new-game-mode.538518/", "pokecommunity"),
("HGSS Sevii", "manual", "https://www.pokecommunity.com/threads/pokémon-hgss-sevii-islands-hoenn-postgame.434636/", "pokecommunity"),
("Itinerant", "manual", "https://www.pokecommunity.com/threads/pokemon-itinerant.528218/", "pokecommunity"),
("Jam Festival", "manual", "https://www.pokecommunity.com/threads/pkmn-jam-festival.536702/", "pokecommunity"),
("Legends of the Arena", "manual", "https://www.pokecommunity.com/threads/pokémon-legends-of-the-arena.298738/", "pokecommunity"),
("Mega Adventures", "manual", "https://www.pokecommunity.com/threads/pokemon-mega-adventure.362058/", "pokecommunity"),
("Nightmare", "manual", "https://www.pokecommunity.com/threads/pokémon-nightmare.504856/", "pokecommunity"),
("Odyssey II", "discord", None, "discord"),
("Legacy Edition", "discord", None, "discord"),
("Iridium", "discord", None, "discord"),
("Light Platinum DS", "manual", None, "twitter"),
("Prisme", "manual", "https://pokemonprisme.com", "website"),
("Relict", "manual", "https://phiongames.blogspot.com/2025/12/descarga-pokemon-relict.html", "blogspot"),
("Reminiscencia", "manual", "https://mega.nz/file/5oNDGI5R#0b_7aQ3fMlUCo6Gwo8dVLXEXnW-seV-o_OanYqxVdRg", "mega"),
("Reloaded", "manual", "https://pokemon-reloaded.blogspot.com", "blogspot"),
("Shattered Light", "manual", "https://eeveeexpo.com/shattered-light/", "eeveeexpo"),
("Sienna", "manual", "https://www.pokecommunity.com/threads/hack-of-the-year-2010-pokémon-sienna-complete-version-released.202372/", "pokecommunity"),
("Tectonic", "manual", "https://www.tectonic-game.com", "website"),
("Uranium", "manual", "https://mega.nz/file/gh82ST6S#makZr_hsZAcMOIKSEPUwejVJ_N4OW7Mk9wEBlWIoIG0", "mega"),
("Vega Fairy EX", "manual", "https://www.pokecommunity.com/threads/pokemon-vega-fairy-edition-ex-minus-v1-4-balance-updates-and-skarmory-evo.475538/", "pokecommunity"),
("Xenoverse", "staging", None, "mega"),
("Zeta", "staging", None, "website"),
("Insurgence", "have", None, "website"),
("Infinite Fusion", "have", None, "pokecommunity"),
("Reborn", "have", None, "website"),
("Rejuvenation", "have", None, "website"),
("Pokerogue", "web", "https://pokerogue.net", "browser"),
("Emerald Seaglass", "manual", "https://ko-fi.com/s/4a1535f351", "ko-fi"),
("Lazarus", "have", "https://ko-fi.com/nemo622", "ko-fi"),
]
def norm(s):
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().lower()
s = re.sub(r"^pokemon\s+", "", s)
s = re.sub(r"[^a-z0-9]+", " ", s).strip()
return s
def load_placed():
"""Read placed library basenames from stdin (piped ssh find output)."""
placed = set()
for line in sys.stdin:
line = line.strip()
if not line:
continue
m = re.match(r"Pokemon - (.+?) \(Hack\)", line, re.I)
if m:
placed.add(norm(m.group(1)))
return placed
def download(url, dest):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:
shutil.copyfileobj(r, f)
return os.path.getsize(dest)
def main():
placed = load_placed()
catalog = {norm(h.get("title", h.get("stem", ""))): h for h in json.loads(CATALOG.read_text(encoding="utf-8"))["hacks"]}
os.makedirs(INCOMING, exist_ok=True)
rows = []
for name, kind, url, host in ACQUIRE:
key = norm(name)
in_cat = key in catalog or any(key in ck or ck in key for ck in catalog)
in_lib = any(key in p or p in key for p in placed)
status = "HAVE" if in_lib or kind == "have" else "MISSING"
if kind == "staging" and not in_lib:
status = "STAGING"
dl = ""
if status in ("MISSING", "STAGING") and kind in ("rom", "zip", "archive", "github", "page") and url:
dest = os.path.join(INCOMING, re.sub(r"[^\w.\- ]", "_", name))
try:
if kind == "github":
# release asset lookup would need gh api; skip for now
dl = "SKIP(github-release)"
elif kind == "page":
dl = "SKIP(scrape-neocities)"
else:
ext = ".zip" if kind in ("rom", "zip") else ".7z"
dest += ext
sz = download(url, dest)
dl = f"OK {sz:,}b -> {dest}"
status = "DOWNLOADED"
except Exception as e:
dl = f"FAIL: {e}"
rows.append((status, name, "catalog" if in_cat else "NEW", host, url or "—", dl))
print(f"Placed library entries: {len(placed)}")
print(f"\n{'STATUS':12} {'NAME':28} {'CAT':5} {'HOST':12} URL")
for st, name, cat, host, url, dl in sorted(rows, key=lambda r: (r[0], r[1])):
print(f"{st:12} {name:28} {cat:5} {host:12} {url[:60]}")
if dl:
print(f"{'':12} {dl}")
if __name__ == "__main__":
main()