#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Fetch Nintendo Switch firmware release metadata from THZoria/NX_Firmware via the GitHub API and write a JSON list of {tag, download} for each release that has a .zip asset. Example: python3 generate_nx_firmware_json.py python3 generate_nx_firmware_json.py -o archive.json --min 18.0.0 """ from __future__ import annotations import argparse import json import re import sys from typing import Any from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen DEFAULT_REPO = "THZoria/NX_Firmware" DEFAULT_MIN_FW = "18.0.0" DEFAULT_OUTPUT = "archive.json" API_BASE = "https://api.github.com" FW_TAG_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") def parse_fw_tag(tag: str) -> tuple[int, int, int] | None: m = FW_TAG_RE.match(tag.strip()) if not m: return None return int(m.group(1)), int(m.group(2)), int(m.group(3)) def parse_min_version(s: str) -> tuple[int, int, int]: v = parse_fw_tag(s) if v is None: raise argparse.ArgumentTypeError("must look like MAJOR.MINOR.PATCH, e.g. 18.0.0") return v def fetch_releases(repo: str) -> list[dict[str, Any]]: """Paginate GET /repos/{owner}/{repo}/releases.""" releases: list[dict[str, Any]] = [] page = 1 headers = { "Accept": "application/vnd.github+json", "User-Agent": "generate-nx-firmware-json", } while True: url = f"{API_BASE}/repos/{repo}/releases?per_page=100&page={page}" req = Request(url, headers=headers) try: with urlopen(req, timeout=60) as resp: body = resp.read().decode("utf-8") except HTTPError as e: raise SystemExit(f"GitHub API error: {e.code} {e.reason}\n{e.read().decode('utf-8', errors='replace')}") from e except URLError as e: raise SystemExit(f"Request failed: {e}") from e chunk = json.loads(body) if not isinstance(chunk, list): raise SystemExit("Unexpected API response (expected a JSON array)") releases.extend(chunk) if len(chunk) < 100: break page += 1 return releases def pick_zip_url(assets: list[dict[str, Any]]) -> str | None: zips = [a for a in assets if str(a.get("name", "")).lower().endswith(".zip")] if not zips: return None zips.sort(key=lambda a: (0 if str(a.get("name", "")).lower().startswith("firmware") else 1, a.get("name", ""))) return str(zips[0].get("browser_download_url") or "") def build_entries( releases: list[dict[str, Any]], min_version: tuple[int, int, int], ) -> list[dict[str, str]]: out: list[dict[str, str]] = [] for rel in releases: if rel.get("draft"): continue tag = str(rel.get("tag_name", "")).strip() ver = parse_fw_tag(tag) if ver is None or ver < min_version: continue url = pick_zip_url(rel.get("assets") or []) if not url: continue out.append({"tag": tag, "download": url}) out.sort(key=lambda e: parse_fw_tag(e["tag"]) or (0, 0, 0), reverse=True) return out def main() -> None: p = argparse.ArgumentParser(description="Build NX firmware JSON index from GitHub releases.") p.add_argument( "--repo", default=DEFAULT_REPO, help=f"owner/repo (default: {DEFAULT_REPO})", ) p.add_argument( "--min", dest="min_fw", type=parse_min_version, default=parse_min_version(DEFAULT_MIN_FW), help=f"minimum firmware tag to include (default: {DEFAULT_MIN_FW})", ) p.add_argument( "-o", "--output", default=DEFAULT_OUTPUT, help=f"write JSON to this file (default: {DEFAULT_OUTPUT}); use - for stdout", ) args = p.parse_args() releases = fetch_releases(args.repo) entries = build_entries(releases, args.min_fw) text = json.dumps(entries, indent=2) + "\n" if args.output == "-": sys.stdout.write(text) else: with open(args.output, "w", encoding="utf-8") as f: f.write(text) print(f"Wrote {len(entries)} entries to {args.output}", file=sys.stderr) if __name__ == "__main__": main()