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

154 lines
4.8 KiB
Python

import argparse
import json
import hashlib
import pathlib
from copy import deepcopy
from enum import Enum
from typing import Dict, Generator, NamedTuple, Optional, Union
import requests
API = "https://addons-ecs.forgesvc.net/api/v2"
class ModLoader(Enum):
FORGE = 1
FABRIC = 4
class File(NamedTuple):
id: int
gameVersion: str
name: str
modLoader: Optional[ModLoader]
@classmethod
def from_json(cls, f: Dict[str, Union[str, int]]):
modLoader = f.get("modLoader", None)
assert isinstance(f["gameVersion"], str)
assert isinstance(f["projectFileId"], int)
assert isinstance(f["projectFileName"], str)
if modLoader is not None:
assert isinstance(modLoader, int)
return cls(
f["projectFileId"],
f["gameVersion"],
f["projectFileName"],
ModLoader(modLoader) if modLoader is not None else None,
)
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}")
res.raise_for_status()
latest_files = res.json().get("gameVersionLatestFiles", None)
if latest_files is None:
return (_ for _ in [])
else:
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:
file_version = file_.gameVersion.split(".")
target_version = version.split(".")
# We assume that major + minor version are compatible;
# this seems to generally be true, but check the output
# for possible mistakes.
#
# The patch version is completely ignored since mod
# authors generally don't register versions properly
# enough to match this.
#
# Being more strict than this usually results in
# technically compatible mods with no available versions.
return (
(file_.modLoader is None or file_.modLoader == mod_loader)
and file_version[0] == target_version[0]
and file_version[1] == target_version[1]
)
latest = max(filter(compatible, latest_files), key=lambda f: f.gameVersion)
if latest is None:
print(f"WARNING: No compatible files found for {mod['project']}")
print(
f"Versions available: {[(f.name, f.gameVersion) for f in latest_files]}"
)
new_mods.append(mod)
elif latest.id != mod["id"]:
print(f"Updating {mod['project']}...")
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)
with open("temp.json", "w") as out:
json.dump(new_mods, out, sort_keys=True, indent=2)
if __name__ == "__main__":
main()