diff --git a/archive.json b/archive.json new file mode 100644 index 0000000..9857e88 --- /dev/null +++ b/archive.json @@ -0,0 +1,82 @@ +[ + { + "tag": "22.1.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/22.1.0/Firmware.22.1.0.zip" + }, + { + "tag": "22.0.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/22.0.0/Firmware.22.0.0.zip" + }, + { + "tag": "21.2.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/21.2.0/Firmware.21.2.0.zip" + }, + { + "tag": "21.1.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/21.1.0/Firmware.21.1.0.zip" + }, + { + "tag": "21.0.1", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/21.0.1/Firmware.21.0.1.zip" + }, + { + "tag": "21.0.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/21.0.0/Firmware.21.0.0.zip" + }, + { + "tag": "20.5.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.5.0/Firmware.20.5.0.zip" + }, + { + "tag": "20.4.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.4.0/Firmware.20.4.0.zip" + }, + { + "tag": "20.3.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.3.0/Firmware.20.3.0.zip" + }, + { + "tag": "20.2.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.2.0/Firmware.20.2.0.zip" + }, + { + "tag": "20.1.5", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.1.5/Firmware.20.1.5.zip" + }, + { + "tag": "20.1.1", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.1.1/Firmware.20.1.1.zip" + }, + { + "tag": "20.1.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.1.0/Firmware.20.1.0.zip" + }, + { + "tag": "20.0.1", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.0.1/Firmware.20.0.1.zip" + }, + { + "tag": "20.0.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/20.0.0/Firmware.20.0.0.zip" + }, + { + "tag": "19.0.1", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/19.0.1/Firmware.19.0.1.zip" + }, + { + "tag": "19.0.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/19.0.0/Firmware.19.0.0.zip" + }, + { + "tag": "18.1.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/18.1.0/Firmware.18.1.0.zip" + }, + { + "tag": "18.0.1", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/18.0.1/Firmware.18.0.1.zip" + }, + { + "tag": "18.0.0", + "download": "https://github.com/THZoria/NX_Firmware/releases/download/18.0.0/Firmware.18.0.0.zip" + } +] diff --git a/generate_fw_archive.py b/generate_fw_archive.py new file mode 100755 index 0000000..1f21318 --- /dev/null +++ b/generate_fw_archive.py @@ -0,0 +1,137 @@ +#!/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()