diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-05 12:31:02 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-05 12:31:02 +0300 |
| commit | 774b25378524ffbac5533e2d20622fb02ffbf60e (patch) | |
| tree | dc257be7aeb818ff2a13adf2e4f220df816818a1 | |
| parent | 0254e84a6e069bf2ce418ff9f95d6f7bee092470 (diff) | |
| download | Project-Tick-774b25378524ffbac5533e2d20622fb02ffbf60e.tar.gz Project-Tick-774b25378524ffbac5533e2d20622fb02ffbf60e.zip | |
NOISSUE fix optifine and some metadata loaders
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
| -rw-r--r-- | meta/meta/common/modloadermp.py | 4 | ||||
| -rw-r--r-- | meta/meta/common/optifine.py | 9 | ||||
| -rw-r--r-- | meta/meta/common/risugami.py | 6 | ||||
| -rw-r--r-- | meta/meta/common/stationloader.py | 6 | ||||
| -rw-r--r-- | meta/meta/run/generate_modloadermp.py | 70 | ||||
| -rw-r--r-- | meta/meta/run/generate_optifine.py | 169 | ||||
| -rw-r--r-- | meta/meta/run/generate_risugami.py | 64 | ||||
| -rw-r--r-- | meta/meta/run/generate_stationloader.py | 79 | ||||
| -rw-r--r-- | meta/meta/run/update_modloadermp.py | 37 | ||||
| -rw-r--r-- | meta/meta/run/update_optifine.py | 477 | ||||
| -rw-r--r-- | meta/meta/run/update_risugami.py | 54 | ||||
| -rw-r--r-- | meta/meta/run/update_stationloader.py | 74 |
12 files changed, 594 insertions, 455 deletions
diff --git a/meta/meta/common/modloadermp.py b/meta/meta/common/modloadermp.py index 1c71b94614..49c765dbec 100644 --- a/meta/meta/common/modloadermp.py +++ b/meta/meta/common/modloadermp.py @@ -1,5 +1,7 @@ +from os.path import join + BASE_DIR = "modloadermp" MODLOADERMP_COMPONENT = "modloadermp" -VERSIONS_FILE = "modloadermp/versions.json" +VERSIONS_FILE = join(BASE_DIR, "versions.json") diff --git a/meta/meta/common/optifine.py b/meta/meta/common/optifine.py index 2fbb854c66..8000d95349 100644 --- a/meta/meta/common/optifine.py +++ b/meta/meta/common/optifine.py @@ -1,10 +1,7 @@ -# upstream directory name where OptiFine pages/versions are stored +from os.path import join + BASE_DIR = "optifine" -# launcher package uid (folder under `launcher/`) -- keep this as the canonical package id -# Use 'net.optifine' so generated launcher metadata lives in `launcher/net.optifine/` OPTIFINE_COMPONENT = "net.optifine" -# upstream component path (under `upstream/`) and combined versions file path -OPTIFINE_UPSTREAM_DIR = "optifine" -VERSIONS_FILE = "optifine/versions.json" +VERSIONS_FILE = join(BASE_DIR, "versions.json") diff --git a/meta/meta/common/risugami.py b/meta/meta/common/risugami.py index c04e8bc01b..3c5a6ea798 100644 --- a/meta/meta/common/risugami.py +++ b/meta/meta/common/risugami.py @@ -1,5 +1,7 @@ +from os.path import join + BASE_DIR = "risugami" -RISUGAMI_COMPONENT = "risugami" +RISUGAMI_COMPONENT = "risugami.modloader" -VERSIONS_FILE = "risugami/versions.json" +VERSIONS_FILE = join(BASE_DIR, "versions.json") diff --git a/meta/meta/common/stationloader.py b/meta/meta/common/stationloader.py index 3ce88888c6..0bed892fb5 100644 --- a/meta/meta/common/stationloader.py +++ b/meta/meta/common/stationloader.py @@ -1,5 +1,7 @@ +from os.path import join + BASE_DIR = "station-loader" -STATIONLOADER_COMPONENT = "station-loader" +STATIONLOADER_COMPONENT = "net.modificationstation.stationloader" -VERSIONS_FILE = "station-loader/versions.json" +VERSIONS_FILE = join(BASE_DIR, "versions.json") diff --git a/meta/meta/run/generate_modloadermp.py b/meta/meta/run/generate_modloadermp.py index a1c6c20cbb..f4ea0d4db5 100644 --- a/meta/meta/run/generate_modloadermp.py +++ b/meta/meta/run/generate_modloadermp.py @@ -1,8 +1,11 @@ import os +import json + from meta.common import ensure_component_dir, launcher_path, upstream_path from meta.common.modloadermp import MODLOADERMP_COMPONENT, VERSIONS_FILE -from meta.model import MetaPackage - +from meta.common.mojang import MINECRAFT_COMPONENT +from meta.common.risugami import RISUGAMI_COMPONENT +from meta.model import MetaPackage, MetaVersion, Library, MojangLibraryDownloads, MojangArtifact, Dependency LAUNCHER_DIR = launcher_path() UPSTREAM_DIR = upstream_path() @@ -11,14 +14,73 @@ ensure_component_dir(MODLOADERMP_COMPONENT) def main(): + src = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + if not os.path.exists(src): + print(f"Missing upstream file: {src}") + return + + with open(src, "r", encoding="utf-8") as f: + entries = json.load(f) + + if not entries: + print("No ModLoaderMP entries found, writing stub package") + package = MetaPackage( + uid=MODLOADERMP_COMPONENT, + name="ModLoaderMP", + description="ModLoaderMP - multiplayer companion to Risugami's ModLoader", + ) + package.write(os.path.join(LAUNCHER_DIR, MODLOADERMP_COMPONENT, "package.json")) + return + + all_versions = [] + + for key, data in entries.items(): + mc_version = data.get("mc_version") + label = data.get("label", f"ModLoaderMP {key}") + requires_modloader = data.get("requires_modloader") + + v = MetaVersion( + name=label, + uid=MODLOADERMP_COMPONENT, + version=key, + type="release", + order=11, + ) + + # Dependencies: Minecraft + Risugami ModLoader + deps = [] + if mc_version: + deps.append(Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)) + if requires_modloader: + deps.append(Dependency(uid=RISUGAMI_COMPONENT, equals=requires_modloader)) + if deps: + v.requires = deps + + # Attach download artifact if available + url = data.get("url") or data.get("download_url") + if url: + artifact_kwargs = {"url": url} + if data.get("size") is not None: + artifact_kwargs["size"] = data["size"] + if data.get("sha256") is not None: + artifact_kwargs["sha256"] = data["sha256"] + artifact = MojangArtifact(**artifact_kwargs) + lib = Library(downloads=MojangLibraryDownloads(artifact=artifact)) + v.jar_mods = [lib] + + v.write(os.path.join(LAUNCHER_DIR, MODLOADERMP_COMPONENT, f"{v.version}.json")) + all_versions.append(v.version) + package = MetaPackage( uid=MODLOADERMP_COMPONENT, name="ModLoaderMP", - description="ModLoaderMP metadata (auto-generated stub)", + description="ModLoaderMP - multiplayer companion to Risugami's ModLoader", + recommended=all_versions[:3], ) - package.write(os.path.join(LAUNCHER_DIR, MODLOADERMP_COMPONENT, "package.json")) + print(f"Generated {len(all_versions)} ModLoaderMP versions") + if __name__ == "__main__": main() diff --git a/meta/meta/run/generate_optifine.py b/meta/meta/run/generate_optifine.py index 802d3af530..3c77718930 100644 --- a/meta/meta/run/generate_optifine.py +++ b/meta/meta/run/generate_optifine.py @@ -1,12 +1,12 @@ import os import json +import re from datetime import datetime -from typing import List from meta.common import ensure_component_dir, launcher_path, upstream_path -from meta.common.optifine import OPTIFINE_COMPONENT, VERSIONS_FILE, OPTIFINE_UPSTREAM_DIR -from meta.model import MetaPackage, MetaVersion, Library, MojangLibraryDownloads, MojangArtifact - +from meta.common.optifine import OPTIFINE_COMPONENT, VERSIONS_FILE, BASE_DIR +from meta.common.mojang import MINECRAFT_COMPONENT +from meta.model import MetaPackage, MetaVersion, Library, MojangLibraryDownloads, MojangArtifact, Dependency LAUNCHER_DIR = launcher_path() UPSTREAM_DIR = upstream_path() @@ -15,96 +15,139 @@ ensure_component_dir(OPTIFINE_COMPONENT) def _parse_date(d: str): - # dates on the site are like DD.MM.YYYY + """Parse dates like DD.MM.YYYY from OptiFine site.""" try: return datetime.strptime(d, "%d.%m.%Y") except Exception: return None -def main(): - # Prefer per-version files in the upstream component directory, fallback to combined versions.json - # upstream files live under `upstream/optifine`, launcher metadata should go under `launcher/net.optifine` - comp_dir = os.path.join(UPSTREAM_DIR, OPTIFINE_UPSTREAM_DIR) +def _extract_mc_version(key: str) -> str: + """Extract Minecraft version from an OptiFine version key. + + Examples: + '1.21.4_HD_U_J3' -> '1.21.4' + 'preview_OptiFine_1.21.8_HD_U_J6_pre16' -> '1.21.8' + '1.8.9_HD_U_M5' -> '1.8.9' + """ + # Strip preview prefix + clean = re.sub(r"^preview_(?:OptiFine_)?", "", key, flags=re.IGNORECASE) + # Match the Minecraft version at the start (e.g. 1.21.4, 1.8.9, 1.7.10) + m = re.match(r"(\d+\.\d+(?:\.\d+)?)", clean) + return m.group(1) if m else None + + +def _is_preview(key: str, data: dict) -> bool: + """Check if a version is a preview/pre-release.""" + filename = data.get("filename", "") + return "preview" in key.lower() or "preview" in filename.lower() or "pre" in key.lower() + + +def _load_entries() -> dict: + """Load upstream OptiFine version entries from per-version files or combined index.""" + comp_dir = os.path.join(UPSTREAM_DIR, BASE_DIR) entries = {} + if os.path.isdir(comp_dir): - files = [f for f in os.listdir(comp_dir) if f.endswith(".json")] - # If there are many per-version files (excluding the combined file), read them - per_files = [f for f in files if f != VERSIONS_FILE] - if per_files: - for fn in per_files: - path = os.path.join(comp_dir, fn) - try: - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - key = os.path.splitext(fn)[0] - entries[key] = data - except Exception: - print(f"Warning: failed to read upstream per-version file: {path}") - # fallback to combined index + for fn in os.listdir(comp_dir): + if not fn.endswith(".json") or fn == "versions.json": + continue + # Skip the nested optifine/ subdir if it exists + path = os.path.join(comp_dir, fn) + if not os.path.isfile(path): + continue + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + key = os.path.splitext(fn)[0] + entries[key] = data + except Exception: + print(f"Warning: failed to read {path}") + + # Fallback to combined index if not entries: src = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) if not os.path.exists(src): print(f"Missing upstream file: {src}") - return + return {} with open(src, "r", encoding="utf-8") as f: entries = json.load(f) - versions: List[str] = [] - parsed_versions = [] - - for key, data in entries.items(): - # key already normalized by the updater - v = MetaVersion(name="OptiFine", uid=OPTIFINE_COMPONENT, version=key) - v.type = "release" - v.order = 10 + return entries - filename = data.get("filename") - download_page = data.get("download_page") - resolved = data.get("resolved_url") or download_page - label = data.get("label") - changelog = data.get("changelog") - date = data.get("date") - size = data.get("size") - sha256 = data.get("sha256") - # attach jar mod as a simple artifact entry; prefer resolved_url and include sha256/size - lib = Library() - artifact_kwargs = {} - if resolved: - artifact_kwargs["url"] = resolved - else: - artifact_kwargs["url"] = download_page - if size is not None: - artifact_kwargs["size"] = size - if sha256 is not None: - artifact_kwargs["sha256"] = sha256 +def main(): + entries = _load_entries() + if not entries: + print("No OptiFine entries found") + return - artifact = MojangArtifact(**artifact_kwargs) - lib.downloads = MojangLibraryDownloads(artifact=artifact) + parsed_versions = [] - v.jar_mods = [lib] + for key, data in entries.items(): + mc_version = _extract_mc_version(key) + is_preview = _is_preview(key, data) + + v = MetaVersion( + name="OptiFine", + uid=OPTIFINE_COMPONENT, + version=key, + type="snapshot" if is_preview else "release", + order=10, + ) + + # Add Minecraft version dependency if we could extract it + if mc_version: + v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)] + + # Use label as display name if available + label = data.get("label") if label: v.name = label + # Parse release date + date = data.get("date") if date: dt = _parse_date(date) if dt: v.release_time = dt - v.write(os.path.join(LAUNCHER_DIR, OPTIFINE_COMPONENT, f"{v.version}.json")) - parsed_versions.append((v.version, v.release_time)) + # Build jar mod artifact — use stable download?f= URL + filename = data.get("filename") + if filename: + resolved = f"https://optifine.net/download?f={filename}" + else: + resolved = data.get("resolved_url") or data.get("download_page") + if resolved: + artifact_kwargs = {"url": resolved} + if data.get("size") is not None: + artifact_kwargs["size"] = data["size"] + if data.get("sha256") is not None: + artifact_kwargs["sha256"] = data["sha256"] - # choose recommended: latest non-preview by release_time if available - parsed_versions.sort(key=lambda x: (x[1] or datetime.min), reverse=True) - recommended = [p[0] for p in parsed_versions[:3]] + artifact = MojangArtifact(**artifact_kwargs) + lib = Library(downloads=MojangLibraryDownloads(artifact=artifact)) + v.jar_mods = [lib] - package = MetaPackage(uid=OPTIFINE_COMPONENT, name="OptiFine") - package.recommended = recommended - package.description = "OptiFine installer and downloads" - package.project_url = "https://optifine.net" + v.write(os.path.join(LAUNCHER_DIR, OPTIFINE_COMPONENT, f"{v.version}.json")) + parsed_versions.append((v.version, v.release_time, is_preview)) + + # Recommended: latest non-preview releases by date + releases = [(ver, rt) for ver, rt, preview in parsed_versions if not preview] + releases.sort(key=lambda x: (x[1] or datetime.min), reverse=True) + recommended = [r[0] for r in releases[:3]] + + package = MetaPackage( + uid=OPTIFINE_COMPONENT, + name="OptiFine", + description="OptiFine - Minecraft performance and graphics mod", + project_url="https://optifine.net", + recommended=recommended, + ) package.write(os.path.join(LAUNCHER_DIR, OPTIFINE_COMPONENT, "package.json")) + print(f"Generated {len(parsed_versions)} OptiFine versions ({len(recommended)} recommended)") + if __name__ == "__main__": main() diff --git a/meta/meta/run/generate_risugami.py b/meta/meta/run/generate_risugami.py index 07962f25e4..f61370d90c 100644 --- a/meta/meta/run/generate_risugami.py +++ b/meta/meta/run/generate_risugami.py @@ -1,8 +1,10 @@ import os +import json + from meta.common import ensure_component_dir, launcher_path, upstream_path from meta.common.risugami import RISUGAMI_COMPONENT, VERSIONS_FILE -from meta.model import MetaPackage - +from meta.common.mojang import MINECRAFT_COMPONENT +from meta.model import MetaPackage, MetaVersion, Library, MojangLibraryDownloads, MojangArtifact, Dependency LAUNCHER_DIR = launcher_path() UPSTREAM_DIR = upstream_path() @@ -11,16 +13,66 @@ ensure_component_dir(RISUGAMI_COMPONENT) def main(): - # If an upstream versions file exists, we could parse it later. - # For now create a minimal package.json so the meta tooling recognizes the component. + src = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + if not os.path.exists(src): + print(f"Missing upstream file: {src}") + return + + with open(src, "r", encoding="utf-8") as f: + entries = json.load(f) + + if not entries: + print("No Risugami ModLoader entries found, writing stub package") + package = MetaPackage( + uid=RISUGAMI_COMPONENT, + name="Risugami ModLoader", + description="Risugami's ModLoader for classic/legacy Minecraft", + ) + package.write(os.path.join(LAUNCHER_DIR, RISUGAMI_COMPONENT, "package.json")) + return + + all_versions = [] + + for key, data in entries.items(): + mc_version = data.get("mc_version") + label = data.get("label", f"ModLoader {key}") + + v = MetaVersion( + name=label, + uid=RISUGAMI_COMPONENT, + version=key, + type="release", + order=10, + ) + + if mc_version: + v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)] + + # Attach download artifact if available + url = data.get("url") or data.get("download_url") + if url: + artifact_kwargs = {"url": url} + if data.get("size") is not None: + artifact_kwargs["size"] = data["size"] + if data.get("sha256") is not None: + artifact_kwargs["sha256"] = data["sha256"] + artifact = MojangArtifact(**artifact_kwargs) + lib = Library(downloads=MojangLibraryDownloads(artifact=artifact)) + v.jar_mods = [lib] + + v.write(os.path.join(LAUNCHER_DIR, RISUGAMI_COMPONENT, f"{v.version}.json")) + all_versions.append(v.version) + package = MetaPackage( uid=RISUGAMI_COMPONENT, name="Risugami ModLoader", - description="Risugami ModLoader metadata (auto-generated stub)", + description="Risugami's ModLoader for classic/legacy Minecraft", + recommended=all_versions[:3], ) - package.write(os.path.join(LAUNCHER_DIR, RISUGAMI_COMPONENT, "package.json")) + print(f"Generated {len(all_versions)} Risugami ModLoader versions") + if __name__ == "__main__": main() diff --git a/meta/meta/run/generate_stationloader.py b/meta/meta/run/generate_stationloader.py index 636772e600..26f79614c0 100644 --- a/meta/meta/run/generate_stationloader.py +++ b/meta/meta/run/generate_stationloader.py @@ -1,8 +1,11 @@ import os +import json +from datetime import datetime + from meta.common import ensure_component_dir, launcher_path, upstream_path from meta.common.stationloader import STATIONLOADER_COMPONENT, VERSIONS_FILE -from meta.model import MetaPackage - +from meta.common.mojang import MINECRAFT_COMPONENT +from meta.model import MetaPackage, MetaVersion, Library, MojangLibraryDownloads, MojangArtifact, Dependency LAUNCHER_DIR = launcher_path() UPSTREAM_DIR = upstream_path() @@ -11,14 +14,80 @@ ensure_component_dir(STATIONLOADER_COMPONENT) def main(): + src = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + if not os.path.exists(src): + print(f"Missing upstream file: {src}") + return + + with open(src, "r", encoding="utf-8") as f: + entries = json.load(f) + + if not entries: + print("No StationLoader entries found, writing stub package") + package = MetaPackage( + uid=STATIONLOADER_COMPONENT, + name="StationAPI", + description="StationAPI mod loader for Minecraft b1.7.3", + ) + package.write(os.path.join(LAUNCHER_DIR, STATIONLOADER_COMPONENT, "package.json")) + return + + all_versions = [] + + for key, data in entries.items(): + mc_version = data.get("mc_version", "b1.7.3") + label = data.get("label", f"StationAPI {key}") + prerelease = data.get("prerelease", False) + + v = MetaVersion( + name=label, + uid=STATIONLOADER_COMPONENT, + version=key, + type="snapshot" if prerelease else "release", + order=10, + ) + + v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)] + + # Parse release date (ISO 8601 from GitHub) + date = data.get("date") + if date: + try: + v.release_time = datetime.fromisoformat(date.replace("Z", "+00:00")) + except Exception: + pass + + # Attach download artifact if available + url = data.get("url") + if url: + artifact_kwargs = {"url": url} + if data.get("size") is not None: + artifact_kwargs["size"] = data["size"] + if data.get("sha256") is not None: + artifact_kwargs["sha256"] = data["sha256"] + artifact = MojangArtifact(**artifact_kwargs) + lib = Library(downloads=MojangLibraryDownloads(artifact=artifact)) + v.jar_mods = [lib] + + v.write(os.path.join(LAUNCHER_DIR, STATIONLOADER_COMPONENT, f"{v.version}.json")) + all_versions.append((v.version, v.release_time, prerelease)) + + # Recommended: latest non-prerelease versions + releases = [(ver, rt) for ver, rt, pre in all_versions if not pre] + releases.sort(key=lambda x: (x[1] or datetime.min), reverse=True) + recommended = [r[0] for r in releases[:3]] + package = MetaPackage( uid=STATIONLOADER_COMPONENT, - name="Station Loader", - description="Station Loader metadata (auto-generated stub)", + name="StationAPI", + description="StationAPI mod loader for Minecraft b1.7.3", + project_url="https://github.com/modificationstation/StationAPI", + recommended=recommended, ) - package.write(os.path.join(LAUNCHER_DIR, STATIONLOADER_COMPONENT, "package.json")) + print(f"Generated {len(all_versions)} StationLoader versions ({len(recommended)} recommended)") + if __name__ == "__main__": main() diff --git a/meta/meta/run/update_modloadermp.py b/meta/meta/run/update_modloadermp.py index 05d6bed6d8..7d36e2648a 100644 --- a/meta/meta/run/update_modloadermp.py +++ b/meta/meta/run/update_modloadermp.py @@ -1,23 +1,48 @@ import json import os -from meta.common import upstream_path, ensure_upstream_dir, default_session +from meta.common import upstream_path, ensure_upstream_dir from meta.common.modloadermp import VERSIONS_FILE, BASE_DIR UPSTREAM_DIR = upstream_path() - ensure_upstream_dir(BASE_DIR) -sess = default_session() +# ModLoaderMP is a legacy/archived project (companion to Risugami's ModLoader for SMP). +# No active upstream API exists; versions are curated from known historical releases. +KNOWN_VERSIONS = { + "1.2.5": {"mc_version": "1.2.5", "label": "ModLoaderMP 1.2.5", "requires_modloader": "1.2.5"}, + "1.2.4": {"mc_version": "1.2.4", "label": "ModLoaderMP 1.2.4", "requires_modloader": "1.2.4"}, + "1.2.3": {"mc_version": "1.2.3", "label": "ModLoaderMP 1.2.3", "requires_modloader": "1.2.3"}, + "1.1": {"mc_version": "1.1", "label": "ModLoaderMP 1.1", "requires_modloader": "1.1"}, + "1.0": {"mc_version": "1.0", "label": "ModLoaderMP 1.0", "requires_modloader": "1.0"}, + "b1.8.1": {"mc_version": "b1.8.1", "label": "ModLoaderMP b1.8.1", "requires_modloader": "b1.8.1"}, + "b1.7.3": {"mc_version": "b1.7.3", "label": "ModLoaderMP b1.7.3", "requires_modloader": "b1.7.3"}, + "b1.6.6": {"mc_version": "b1.6.6", "label": "ModLoaderMP b1.6.6", "requires_modloader": "b1.6.6"}, +} def main(): - # Placeholder updater: upstream source not implemented yet. out_path = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + + existing = {} + if os.path.exists(out_path): + try: + with open(out_path, "r") as f: + existing = json.load(f) + except Exception: + pass + + for key, data in KNOWN_VERSIONS.items(): + if key not in existing: + existing[key] = data + else: + for field, value in data.items(): + existing[key].setdefault(field, value) + with open(out_path, "w") as f: - json.dump({}, f, indent=4) + json.dump(existing, f, indent=4) - print(f"Wrote placeholder upstream file: {out_path}") + print(f"Wrote {len(existing)} ModLoaderMP entries to {out_path}") if __name__ == "__main__": diff --git a/meta/meta/run/update_optifine.py b/meta/meta/run/update_optifine.py index 833e08e263..4eef132e0d 100644 --- a/meta/meta/run/update_optifine.py +++ b/meta/meta/run/update_optifine.py @@ -1,206 +1,115 @@ import json import os import re -from urllib.parse import urljoin, urlparse, parse_qs +import hashlib import concurrent.futures -import threading - -try: - from meta.common import upstream_path, ensure_upstream_dir, default_session - from meta.common.optifine import VERSIONS_FILE, BASE_DIR - HAVE_META = True -except Exception: - # meta.common or its dependencies (requests) may not be available in this environment. - HAVE_META = False - def upstream_path(): - return "upstream" - - def ensure_upstream_dir(path): - path = os.path.join(upstream_path(), path) - if not os.path.exists(path): - os.makedirs(path, exist_ok=True) - - def default_session(): - raise RuntimeError("HTTP session unavailable: install 'requests' and 'cachecontrol'") +from urllib.parse import urljoin, urlparse, parse_qs - VERSIONS_FILE = "versions.json" - BASE_DIR = "optifine" +from meta.common import upstream_path, ensure_upstream_dir, default_session +from meta.common.optifine import VERSIONS_FILE, BASE_DIR UPSTREAM_DIR = upstream_path() - ensure_upstream_dir(BASE_DIR) -sess = None -if HAVE_META: - sess = default_session() +sess = default_session() +# Configurable via environment +TIMEOUT = float(os.environ.get("OPTIFINE_TIMEOUT", "10")) +HASH_TIMEOUT = float(os.environ.get("OPTIFINE_HASH_TIMEOUT", "120")) +CONCURRENCY = max(1, int(os.environ.get("OPTIFINE_CONCURRENCY", "8"))) +COMPUTE_HASH = os.environ.get("OPTIFINE_COMPUTE_HASH", "1").lower() in ("1", "true", "yes") -def _resolve_href(href: str): - """Return (filename, resolved_href). - Handles cases where href is a redirect wrapper (e.g., adfoc.us with an inner - 'url=' parameter) or where the 'f' query parameter is present. - """ +def _resolve_href(href: str): + """Return (filename, resolved_href) from an OptiFine download link.""" parsed = urlparse(href) q = parse_qs(parsed.query) - # Direct f parameter f = q.get("f") if f: return f[0], href - # Some wrappers embed an inner url parameter that contains the real target inner = q.get("url") if inner: - # inner may be a list; pick first - inner_url = inner[0] - inner_parsed = urlparse(inner_url) - inner_q = parse_qs(inner_parsed.query) - inner_f = inner_q.get("f") + inner_parsed = urlparse(inner[0]) + inner_f = parse_qs(inner_parsed.query).get("f") if inner_f: - return inner_f[0], inner_url + return inner_f[0], inner[0] - # fallback: last path component return os.path.basename(parsed.path), href -def _clean_key(filename: str) -> str: - # Remove OptiFine prefix, any trailing ad-wrapper segments, and the .jar suffix - key = re.sub(r"^OptiFine[_-]", "", filename, flags=re.IGNORECASE) - key = re.sub(r"\.jar$", "", key, flags=re.IGNORECASE) - # Strip trailing ad/adload/adloadx wrapper fragments that appear in some links - key = re.sub(r"[_-]ad[a-z0-9_-]*$", "", key, flags=re.IGNORECASE) - return key - - def _strip_ad_wrapper(filename: str) -> str: - """Remove trailing ad/adload/adloadx wrapper fragments from a filename. - - Example: OptiFine_1.20.1_HD_U_H7_adloadx.jar -> OptiFine_1.20.1_HD_U_H7.jar - """ + """Remove trailing ad/adload/adloadx fragments from a filename.""" if not filename: return filename root, ext = os.path.splitext(filename) - # remove trailing segments that start with _ad or -ad root = re.sub(r"[_-]ad[a-z0-9_-]*$", "", root, flags=re.IGNORECASE) return root + ext -def _guess_platforms(filename: str, label: str = None, changelog: str = None): - """Heuristically guess platform compatibility tags for an OptiFine build. - - Returns a list like ['mojang', 'neoforge', 'fabric'] based on keywords. - """ - text = " ".join(filter(None, [filename or "", label or "", changelog or ""])) - tl = text.lower() - platforms = [] - # OptiFine always targets vanilla (Mojang) builds - platforms.append("mojang") - # Forge / NeoForge variants - if "neoforge" in tl or "neo-forge" in tl or "forge" in tl: - platforms.append("neoforge") - # Fabric - if "fabric" in tl: - platforms.append("fabric") - # Quilt - if "quilt" in tl: - platforms.append("quilt") - # LiteLoader / older loaders - if "liteloader" in tl: - platforms.append("liteloader") - - # Deduplicate while preserving order - seen = set() - out = [] - for p in platforms: - if p not in seen: - seen.add(p) - out.append(p) - return out +def _clean_key(filename: str) -> str: + """Normalize filename to version key: strip OptiFine_ prefix and .jar suffix.""" + key = re.sub(r"^OptiFine[_-]", "", filename, flags=re.IGNORECASE) + key = re.sub(r"\.jar$", "", key, flags=re.IGNORECASE) + key = re.sub(r"[_-]ad[a-z0-9_-]*$", "", key, flags=re.IGNORECASE) + return key def _score_entry(entry: dict) -> int: + """Score an entry to pick the best when duplicates exist.""" url = (entry.get("download_page") or "").lower() s = 0 - if "optifine.net/adloadx" in url or "optifine.net/adload" in url or "optifine.net/download" in url: + if any(k in url for k in ("optifine.net/adloadx", "optifine.net/adload", "optifine.net/download")): s += 10 - if url.endswith(".jar") or entry.get("filename", "").lower().endswith(".jar"): + if url.endswith(".jar") or (entry.get("filename") or "").lower().endswith(".jar"): s += 5 if "preview" in (entry.get("filename") or "").lower(): s -= 2 return s -def main(): - url = "https://optifine.net/downloads" - print(f"Fetching OptiFine downloads page: {url}") - # configurable timeouts (seconds) - default_timeout = float(os.environ.get("OPTIFINE_TIMEOUT", "10")) - - try: - r = sess.get(url, timeout=default_timeout) - r.raise_for_status() - html = r.text - except Exception as e: - print(f"Error fetching downloads page: {e}") - html = "" - +def _scrape_downloads(html: str, base_url: str) -> dict: + """Parse OptiFine downloads page and return {key: entry_dict}.""" versions = {} - # Try parsing with BeautifulSoup if available; be permissive about href forms try: from bs4 import BeautifulSoup - soup = BeautifulSoup(html, "html.parser") - anchors = soup.find_all("a", href=True) - inspected = 0 - matched = 0 - for a in anchors: - inspected += 1 + + for a in soup.find_all("a", href=True): href = a["href"] href_l = href.lower() - - # Accept several formats: any URL containing '?f=' (adload/adloadx/download), or direct .jar links if "?f=" not in href_l and not href_l.endswith(".jar"): continue - matched += 1 filename, resolved = _resolve_href(href) - # strip ad/adload/adloadx wrapper parts from filename filename = _strip_ad_wrapper(filename) - # Try to get version text from the same table row or nearby text ver_text = None changelog = None date = None + tr = a.find_parent("tr") if tr: tds = tr.find_all("td") if tds: ver_text = tds[0].get_text(strip=True) - # find changelog link in the row ch = tr.find("a", href=lambda h: h and "changelog" in h) if ch: changelog = ch.get("href") - # find date cell date_td = tr.find("td", class_=lambda c: c and "colDate" in c) if date_td: date = date_td.get_text(strip=True) if not ver_text: - # fallback: anchor text or nearby text nodes - if a.string and a.string.strip(): - ver_text = a.string.strip() - else: - prev = a.find_previous(string=True) - if prev: - ver_text = prev.strip() + ver_text = (a.string or "").strip() or filename key = _clean_key(filename) data = { "filename": filename, - "download_page": urljoin(url, resolved), - "label": ver_text or filename, + "download_page": urljoin(base_url, resolved), + "label": ver_text, "changelog": changelog, "date": date, } @@ -208,13 +117,9 @@ def main(): existing = versions.get(key) if existing is None or _score_entry(data) > _score_entry(existing): versions[key] = data - platforms = _guess_platforms(data.get("filename"), data.get("label"), data.get("changelog")) - print(f"Added {key}: platforms: {', '.join(platforms)}") - - print(f"Inspected {inspected} anchors, matched {matched} potential downloads") - except Exception: - # Fallback: regex parse (case-insensitive) - print("BeautifulSoup not available or parsing failed, falling back to regex parse") + except ImportError: + # Fallback: regex parse + print("BeautifulSoup not available, falling back to regex parse") for match in re.finditer(r'href="([^"]*\?f=[^"\s]+)"', html, flags=re.IGNORECASE): href = match.group(1) filename, resolved = _resolve_href(href) @@ -222,234 +127,108 @@ def main(): key = _clean_key(filename) data = { "filename": filename, - "download_page": urljoin(url, resolved), + "download_page": urljoin(base_url, resolved), "label": filename, } existing = versions.get(key) if existing is None or _score_entry(data) > _score_entry(existing): versions[key] = data - platforms = _guess_platforms(data.get("filename"), data.get("label"), data.get("changelog")) - print(f"Added {key}: platforms: {', '.join(platforms)}") - # Determine base output directory. Some upstream implementations return a - # path that already includes BASE_DIR, avoid duplicating it. - if UPSTREAM_DIR.endswith(BASE_DIR): - base_out_dir = UPSTREAM_DIR - else: - base_out_dir = os.path.join(UPSTREAM_DIR, BASE_DIR) - - # Ensure output directory exists (defensive: collapse duplicate trailing BASE_DIR segments) - parts = base_out_dir.split(os.sep) - while len(parts) >= 2 and parts[-1] == BASE_DIR and parts[-2] == BASE_DIR: - parts.pop(-1) - base_out_dir = os.sep.join(parts) - os.makedirs(base_out_dir, exist_ok=True) - - out_path = os.path.join(base_out_dir, VERSIONS_FILE) - # Attempt to resolve final download URLs and optionally compute hashes - # Default to computing SHA256 for each resolved file unless explicitly disabled - compute_hash = os.environ.get("OPTIFINE_COMPUTE_HASH", "1").lower() in ("1", "true", "yes") - resolved_count = 0 - hashed_count = 0 - - if HAVE_META and sess is not None: + return versions + + +def _make_download_url(filename: str) -> str: + """Build a stable OptiFine download URL from a filename. + + Uses the permanent https://optifine.net/download?f=FILENAME format + instead of the adloadx/downloadx token URLs which expire. + """ + return f"https://optifine.net/download?f={filename}" + + +def _resolve_and_hash(key: str, data: dict) -> dict: + """Build stable download URL and optionally compute SHA256 for a single entry.""" + filename = data.get("filename") + if not filename: + return data + + # Use stable download URL instead of expiring token-based downloadx URLs + download_url = _make_download_url(filename) + data["resolved_url"] = download_url + + # Compute hash if enabled + if COMPUTE_HASH: try: - # Use a ThreadPoolExecutor to parallelize network I/O for resolving URLs - concurrency = int(os.environ.get("OPTIFINE_CONCURRENCY", "8")) - if concurrency < 1: - concurrency = 1 - - total = len(versions) - counter = {"idx": 0} - counter_lock = threading.Lock() - - def _process_item(item): - key, data = item - with counter_lock: - counter["idx"] += 1 - idx = counter["idx"] - - dp = data.get("download_page") - if not dp: - return key, data, False, False - - print(f"[{idx}/{total}] Resolving {key} ({data.get('filename')}) -> {dp}") - - # Each worker creates its own session to avoid any session thread-safety issues - sess_local = None - if HAVE_META: - try: - sess_local = default_session() - except Exception: - sess_local = None - - # Fallback to global sess if default_session unavailable - if sess_local is None: - sess_local = sess - - final_url = None - try: - # Try HEAD first - try: - resp = sess_local.head(dp, allow_redirects=True, timeout=default_timeout) - except Exception as e_head: - # Try GET as fallback for hosts that block HEAD - try: - resp = sess_local.get(dp, allow_redirects=True, timeout=default_timeout) - except Exception: - resp = None - - if resp is not None: - final_url = getattr(resp, "url", None) - - # Try to extract downloadx link from page HTML (short GET if needed) - page_text = None - if resp is not None and hasattr(resp, "text") and resp.text: - page_text = resp.text - else: - try: - rtmp = sess_local.get(dp, allow_redirects=True, timeout=5) - page_text = getattr(rtmp, "text", None) - final_url = getattr(rtmp, "url", final_url) - except Exception: - page_text = None - - if page_text: - m = re.search(r"(downloadx\?f=[^\"'\s>]+)", page_text, flags=re.IGNORECASE) - if m: - candidate = m.group(1) - base_for_join = final_url or dp - final_url = urljoin(base_for_join, candidate) - print(f" Extracted downloadx link for {key}: {final_url}") - - # If still not a .jar/f param, do a full GET and inspect final URL - if not final_url or (".jar" not in final_url and "?f=" not in final_url): - try: - resp2 = sess_local.get(dp, allow_redirects=True, timeout=30) - final_url = getattr(resp2, "url", final_url) - except Exception: - pass - - hashed = False - if final_url: - data["resolved_url"] = final_url - print(f" Resolved {key} -> {final_url}") - - if compute_hash: - try: - import hashlib - - print(f" Hashing {key} from {final_url} ...") - h = hashlib.sha256() - size = 0 - hash_timeout = float(os.environ.get("OPTIFINE_HASH_TIMEOUT", "120")) - r2 = sess_local.get(final_url, stream=True, timeout=hash_timeout) - r2.raise_for_status() - for chunk in r2.iter_content(8192): - if not chunk: - continue - h.update(chunk) - size += len(chunk) - data["sha256"] = h.hexdigest() - data["size"] = size - hashed = True - print(f" Hashed {key}: sha256={data['sha256']} size={data['size']}") - except Exception as e_hash: - print(f" Warning: failed to hash {final_url}: {e_hash}") - - return key, data, bool(final_url), hashed - except Exception as e: - print(f" Error processing {key}: {e}") - return key, data, False, False - - items = list(versions.items()) - if concurrency == 1: - # run serially - results = map(_process_item, items) - else: - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex: - results = ex.map(_process_item, items) - - # Collect results and write per-version files as each item completes - for key, data, resolved_flag, hashed_flag in results: - versions[key] = data - # Ensure per-version dir exists - try: - os.makedirs(base_out_dir, exist_ok=True) - per_path = os.path.join(base_out_dir, f"{key}.json") - with open(per_path, "w") as pf: - json.dump(data, pf, indent=4) - print(f"Wrote per-version file: {per_path}") - except Exception as e: - print(f"Warning: failed to write per-version file for {key}: {e}") - - if resolved_flag: - resolved_count += 1 - if hashed_flag: - hashed_count += 1 - except KeyboardInterrupt: - print("Interrupted by user (KeyboardInterrupt). Writing partial results...") - - # Write combined index (ensure parent exists) - os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True) - with open(out_path, "w") as f: - json.dump(versions, f, indent=4) + h = hashlib.sha256() + size = 0 + r = sess.get(download_url, stream=True, timeout=HASH_TIMEOUT) + r.raise_for_status() + for chunk in r.iter_content(8192): + if chunk: + h.update(chunk) + size += len(chunk) + data["sha256"] = h.hexdigest() + data["size"] = size + except Exception as e: + print(f" Warning: hash failed for {key}: {e}") + + return data + + +def main(): + url = "https://optifine.net/downloads" + print(f"Fetching OptiFine downloads page: {url}") - # Also write per-version JSON files under the upstream component directory try: - for key, data in versions.items(): - per_path = os.path.join(base_out_dir, f"{key}.json") - with open(per_path, "w") as pf: - json.dump(data, pf, indent=4) - print(f"Wrote per-version file: {per_path}") + r = sess.get(url, timeout=TIMEOUT) + r.raise_for_status() + html = r.text except Exception as e: - print(f"Warning: failed to write per-version files: {e}") - - print(f"Wrote {len(versions)} OptiFine entries to {out_path}") - if HAVE_META and sess is not None: - print(f"Resolved {resolved_count} final URLs") - if compute_hash: - print(f"Computed {hashed_count} SHA256 hashes (OPTIFINE_COMPUTE_HASH=1)") - # If some entries are missing sha256 (e.g., were written before hashing completed), - # compute them now in parallel and update files. - missing = [ (k,v) for k,v in versions.items() if v.get("resolved_url") and not v.get("sha256") ] - if missing: - print(f"Computing missing SHA256 for {len(missing)} entries...") - def _compute_and_write(item): - k, v = item - url_final = v.get("resolved_url") - try: - import hashlib - hash_timeout = float(os.environ.get("OPTIFINE_HASH_TIMEOUT", "120")) - h = hashlib.sha256() - size = 0 - r = sess.get(url_final, stream=True, timeout=hash_timeout) - r.raise_for_status() - for chunk in r.iter_content(8192): - if not chunk: - continue - h.update(chunk) - size += len(chunk) - v["sha256"] = h.hexdigest() - v["size"] = size - per_path = os.path.join(base_out_dir, f"{k}.json") - with open(per_path, "w") as pf: - json.dump(v, pf, indent=4) - print(f" Hashed {k}: {v['sha256']} size={v['size']}") - return True - except Exception as e: - print(f" Warning: failed to compute hash for {k}: {e}") - return False - - concurrency = int(os.environ.get("OPTIFINE_CONCURRENCY", "8")) - if concurrency < 1: - concurrency = 1 - completed = 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex: - for ok in ex.map(_compute_and_write, missing): - if ok: - completed += 1 - print(f"Completed extra hashing: {completed}/{len(missing)}") + print(f"Error fetching downloads page: {e}") + return + + versions = _scrape_downloads(html, url) + print(f"Scraped {len(versions)} OptiFine entries") + + if not versions: + print("No versions found, aborting") + return + + out_dir = os.path.join(UPSTREAM_DIR, BASE_DIR) + os.makedirs(out_dir, exist_ok=True) + + # Resolve URLs and compute hashes in parallel + def _process(item): + key, data = item + print(f" Resolving {key}...") + data = _resolve_and_hash(key, data) + return key, data + + items = list(versions.items()) + if CONCURRENCY == 1: + results = [_process(item) for item in items] + else: + with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as ex: + results = list(ex.map(_process, items)) + + # Write per-version files and build combined index + versions = {} + for key, data in results: + versions[key] = data + per_path = os.path.join(out_dir, f"{key}.json") + with open(per_path, "w") as f: + json.dump(data, f, indent=4) + + # Write combined index + combined_path = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + os.makedirs(os.path.dirname(combined_path) or ".", exist_ok=True) + with open(combined_path, "w") as f: + json.dump(versions, f, indent=4) + + resolved = sum(1 for v in versions.values() if v.get("resolved_url")) + hashed = sum(1 for v in versions.values() if v.get("sha256")) + print(f"Wrote {len(versions)} entries to {combined_path}") + print(f"Resolved: {resolved}, Hashed: {hashed}") if __name__ == "__main__": diff --git a/meta/meta/run/update_risugami.py b/meta/meta/run/update_risugami.py index 66bd59eff8..26d162fac8 100644 --- a/meta/meta/run/update_risugami.py +++ b/meta/meta/run/update_risugami.py @@ -1,24 +1,64 @@ import json import os -from meta.common import upstream_path, ensure_upstream_dir, default_session +from meta.common import upstream_path, ensure_upstream_dir from meta.common.risugami import VERSIONS_FILE, BASE_DIR UPSTREAM_DIR = upstream_path() - ensure_upstream_dir(BASE_DIR) -sess = default_session() +# Risugami's ModLoader is a legacy/archived project (last updated around MC 1.6.2). +# No active upstream API exists; versions are curated from known historical releases. +# To add a version, add an entry to this dict with the Minecraft version it targets. +KNOWN_VERSIONS = { + "1.6.2": {"mc_version": "1.6.2", "label": "ModLoader 1.6.2"}, + "1.6.1": {"mc_version": "1.6.1", "label": "ModLoader 1.6.1"}, + "1.5.2": {"mc_version": "1.5.2", "label": "ModLoader 1.5.2"}, + "1.5.1": {"mc_version": "1.5.1", "label": "ModLoader 1.5.1"}, + "1.4.7": {"mc_version": "1.4.7", "label": "ModLoader 1.4.7"}, + "1.4.6": {"mc_version": "1.4.6", "label": "ModLoader 1.4.6"}, + "1.4.5": {"mc_version": "1.4.5", "label": "ModLoader 1.4.5"}, + "1.4.4": {"mc_version": "1.4.4", "label": "ModLoader 1.4.4"}, + "1.4.2": {"mc_version": "1.4.2", "label": "ModLoader 1.4.2"}, + "1.3.2": {"mc_version": "1.3.2", "label": "ModLoader 1.3.2"}, + "1.3.1": {"mc_version": "1.3.1", "label": "ModLoader 1.3.1"}, + "1.2.5": {"mc_version": "1.2.5", "label": "ModLoader 1.2.5"}, + "1.2.4": {"mc_version": "1.2.4", "label": "ModLoader 1.2.4"}, + "1.2.3": {"mc_version": "1.2.3", "label": "ModLoader 1.2.3"}, + "1.1": {"mc_version": "1.1", "label": "ModLoader 1.1"}, + "1.0": {"mc_version": "1.0", "label": "ModLoader 1.0"}, + "b1.8.1": {"mc_version": "b1.8.1", "label": "ModLoader b1.8.1"}, + "b1.7.3": {"mc_version": "b1.7.3", "label": "ModLoader b1.7.3"}, + "b1.6.6": {"mc_version": "b1.6.6", "label": "ModLoader b1.6.6"}, + "b1.5_01": {"mc_version": "b1.5_01", "label": "ModLoader b1.5_01"}, + "b1.4_01": {"mc_version": "b1.4_01", "label": "ModLoader b1.4_01"}, + "b1.3_01": {"mc_version": "b1.3_01", "label": "ModLoader b1.3_01"}, +} def main(): - # Placeholder updater: upstream source not implemented yet. - # Create an empty versions file so the meta pipeline can proceed. + # Merge with any existing entries to preserve manually-added data (sha256, urls, etc.) out_path = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + existing = {} + if os.path.exists(out_path): + try: + with open(out_path, "r") as f: + existing = json.load(f) + except Exception: + pass + + for key, data in KNOWN_VERSIONS.items(): + if key not in existing: + existing[key] = data + else: + # Preserve existing fields, add any missing ones from known data + for field, value in data.items(): + existing[key].setdefault(field, value) + with open(out_path, "w") as f: - json.dump({}, f, indent=4) + json.dump(existing, f, indent=4) - print(f"Wrote placeholder upstream file: {out_path}") + print(f"Wrote {len(existing)} Risugami ModLoader entries to {out_path}") if __name__ == "__main__": diff --git a/meta/meta/run/update_stationloader.py b/meta/meta/run/update_stationloader.py index 3e0a4b34bb..b421976eb8 100644 --- a/meta/meta/run/update_stationloader.py +++ b/meta/meta/run/update_stationloader.py @@ -5,19 +5,85 @@ from meta.common import upstream_path, ensure_upstream_dir, default_session from meta.common.stationloader import VERSIONS_FILE, BASE_DIR UPSTREAM_DIR = upstream_path() - ensure_upstream_dir(BASE_DIR) sess = default_session() +# StationAPI/StationLoader GitHub releases +GITHUB_API_URL = "https://api.github.com/repos/modificationstation/StationAPI/releases" + def main(): - # Placeholder updater: upstream source not implemented yet. out_path = os.path.join(UPSTREAM_DIR, VERSIONS_FILE) + + # Load existing entries to preserve manually-added data + existing = {} + if os.path.exists(out_path): + try: + with open(out_path, "r") as f: + existing = json.load(f) + except Exception: + pass + + # Fetch releases from GitHub + print(f"Fetching StationAPI releases from {GITHUB_API_URL}") + try: + r = sess.get(GITHUB_API_URL, timeout=15) + r.raise_for_status() + releases = r.json() + except Exception as e: + print(f"Error fetching GitHub releases: {e}") + # Write whatever we have + with open(out_path, "w") as f: + json.dump(existing, f, indent=4) + return + + for release in releases: + tag = release.get("tag_name", "") + if not tag: + continue + + key = tag.lstrip("v") + prerelease = release.get("prerelease", False) + published = release.get("published_at") + + # Find the main jar asset + jar_url = None + jar_size = None + jar_name = None + for asset in release.get("assets", []): + name = asset.get("name", "") + if name.endswith(".jar"): + jar_url = asset.get("browser_download_url") + jar_size = asset.get("size") + jar_name = name + break + + data = { + "mc_version": "b1.7.3", + "label": f"StationAPI {key}", + "tag": tag, + "prerelease": prerelease, + } + if published: + data["date"] = published + if jar_url: + data["url"] = jar_url + if jar_size is not None: + data["size"] = jar_size + if jar_name: + data["filename"] = jar_name + + if key not in existing: + existing[key] = data + else: + for field, value in data.items(): + existing[key].setdefault(field, value) + with open(out_path, "w") as f: - json.dump({}, f, indent=4) + json.dump(existing, f, indent=4) - print(f"Wrote placeholder upstream file: {out_path}") + print(f"Wrote {len(existing)} StationLoader entries to {out_path}") if __name__ == "__main__": |
