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

152 lines
4.7 KiB
Python

import argparse
import json
import hashlib
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"
)
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.version, mod_loader)
def update(version: str, mod_loader: ModLoader):
with open("./mods.json") 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()