tlaternet-server/pkgs/minecraft/voor-kia/update-mods.py

157 lines
4.6 KiB
Python

import argparse
import json
import hashlib
import pathlib
from copy import deepcopy
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Generator, List, NamedTuple, Optional, Union
import requests
from dateutil import parser
API = "https://addons-ecs.forgesvc.net/api/v2"
JSON = Union[List[Dict[str, Any]], Dict[str, Any]]
class ModLoader(Enum):
FORGE = 1
FABRIC = 4
class File(NamedTuple):
id: int
gameVersions: List[str]
name: str
modLoader: Optional[ModLoader]
date: datetime
@classmethod
def from_json(cls, f: JSON):
assert isinstance(f, dict)
assert isinstance(f.get("gameVersion"), list)
assert isinstance(f.get("id"), int)
assert isinstance(f.get("fileName"), str)
assert isinstance(f.get("fileDate"), str)
modLoader = (
ModLoader.FORGE
if "Forge" in f["gameVersion"]
else ModLoader.FABRIC
if "Fabric" in f["gameVersion"]
else None
)
return cls(
f["id"],
f["gameVersion"],
f["fileName"],
modLoader,
parser.isoparse(f["fileDate"]),
)
class CurseAPI:
def __init__(self):
self._session = requests.Session()
self._session.headers[
"User-Agent"
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0"
def get_latest_files(self, mod_id: int) -> Generator[File, None, None]:
res = self._session.get(f"{API}/addon/{mod_id}/files")
res.raise_for_status()
latest_files = res.json()
return (File.from_json(f) for f in latest_files)
def get_file_url(self, mod_id: int, file_id: int) -> str:
res = self._session.get(f"{API}/addon/{mod_id}/file/{file_id}/download-url")
res.raise_for_status()
return res.text.rstrip("\n")
def download_file(self, mod_id: int, file_id: int) -> bytes:
url = self.get_file_url(mod_id, file_id)
file_ = self._session.get(url)
file_.raise_for_status()
return file_.content
def main():
parser = argparse.ArgumentParser()
parser.add_argument("version", help="The minecraft version to limit updates to")
parser.add_argument(
"--mod-loader", choices=["none", "forge", "fabric"], default="forge"
)
parser.add_argument("--infile", type=pathlib.Path)
args = parser.parse_args()
if args.mod_loader == "forge":
mod_loader = ModLoader.FORGE
elif args.mod_loader == "fabric":
mod_loader = ModLoader.FABRIC
else:
raise AssertionError("Unreachable")
update(args.infile, args.version, mod_loader)
def update(infile: pathlib.Path, version: str, mod_loader: ModLoader):
with open(infile) as mods_json:
mods = json.load(mods_json)
curse = CurseAPI()
new_mods = []
for mod in mods:
print(f"Checking for updates to {mod['project']}...")
try:
latest_files = list(curse.get_latest_files(mod["project_id"]))
except requests.HTTPError as err:
print(f"WARNING: Could not access curse API for {mod['project']}: {err}")
latest_files = [_ for _ in []]
def compatible(file_: File) -> bool:
return (
mod_loader is None
or file_.modLoader is None
or file_.modLoader == mod_loader
) and any(
file_version.startswith(version) for file_version in file_.gameVersions
)
compatible_files = list(filter(compatible, latest_files))
if compatible_files:
latest = max(compatible_files, key=lambda f: f.date)
if latest.id != mod["id"]:
print(
f"Updating {mod['project']} {mod['filename']} -> {latest.name}..."
)
contents = curse.download_file(mod["project_id"], latest.id)
sha256 = hashlib.sha256(contents).hexdigest()
new_mod = deepcopy(mod)
new_mod.update(
{"filename": latest.name, "id": latest.id, "sha256": sha256}
)
new_mods.append(new_mod)
else:
new_mods.append(mod)
else:
print(f"WARNING: No compatible files found for {mod['project']}")
print(
f"Versions available: {[(f.name, f.gameVersions) for f in latest_files]}"
)
new_mods.append(mod)
with open("temp.json", "w") as out:
json.dump(new_mods, out, sort_keys=True, indent=2)
if __name__ == "__main__":
main()