update-mods.py: Fix issues revealed by 1.17 updates #52

Manually merged
tlater merged 1 commit from tlater/fix-minecraft-update into master 2021-10-06 01:54:53 +01:00
2 changed files with 51 additions and 47 deletions

View file

@ -97,6 +97,7 @@
# For the minecraft mod update script # For the minecraft mod update script
(python3.withPackages (pypkgs: (python3.withPackages (pypkgs:
with pypkgs; [ with pypkgs; [
dateutil
requests requests
ipython ipython

View file

@ -3,13 +3,16 @@ import json
import hashlib import hashlib
import pathlib import pathlib
from copy import deepcopy from copy import deepcopy
from datetime import datetime
from enum import Enum from enum import Enum
from typing import Dict, Generator, NamedTuple, Optional, Union from typing import Any, Dict, Generator, List, NamedTuple, Optional, Union
import requests import requests
from dateutil import parser
API = "https://addons-ecs.forgesvc.net/api/v2" API = "https://addons-ecs.forgesvc.net/api/v2"
JSON = Union[List[Dict[str, Any]], Dict[str, Any]]
class ModLoader(Enum): class ModLoader(Enum):
@ -19,25 +22,34 @@ class ModLoader(Enum):
class File(NamedTuple): class File(NamedTuple):
id: int id: int
gameVersion: str gameVersions: List[str]
name: str name: str
modLoader: Optional[ModLoader] modLoader: Optional[ModLoader]
date: datetime
@classmethod @classmethod
def from_json(cls, f: Dict[str, Union[str, int]]): def from_json(cls, f: JSON):
modLoader = f.get("modLoader", None) assert isinstance(f, dict)
assert isinstance(f["gameVersion"], str) assert isinstance(f.get("gameVersion"), list)
assert isinstance(f["projectFileId"], int) assert isinstance(f.get("id"), int)
assert isinstance(f["projectFileName"], str) assert isinstance(f.get("fileName"), str)
if modLoader is not None: assert isinstance(f.get("fileDate"), str)
assert isinstance(modLoader, int)
modLoader = (
ModLoader.FORGE
if "Forge" in f["gameVersion"]
else ModLoader.FABRIC
if "Fabric" in f["gameVersion"]
else None
)
return cls( return cls(
f["projectFileId"], f["id"],
f["gameVersion"], f["gameVersion"],
f["projectFileName"], f["fileName"],
ModLoader(modLoader) if modLoader is not None else None, modLoader,
parser.isoparse(f["fileDate"]),
) )
@ -49,15 +61,12 @@ class CurseAPI:
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0" ] = "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]: def get_latest_files(self, mod_id: int) -> Generator[File, None, None]:
res = self._session.get(f"{API}/addon/{mod_id}") res = self._session.get(f"{API}/addon/{mod_id}/files")
res.raise_for_status() res.raise_for_status()
latest_files = res.json().get("gameVersionLatestFiles", None) latest_files = res.json()
if latest_files is None: return (File.from_json(f) for f in latest_files)
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: 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 = self._session.get(f"{API}/addon/{mod_id}/file/{file_id}/download-url")
@ -107,43 +116,37 @@ def update(infile: pathlib.Path, version: str, mod_loader: ModLoader):
latest_files = [_ for _ in []] latest_files = [_ for _ in []]
def compatible(file_: File) -> bool: 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 ( return (
(file_.modLoader is None or file_.modLoader == mod_loader) mod_loader is None
and file_version[0] == target_version[0] or file_.modLoader is None
and file_version[1] == target_version[1] or file_.modLoader == mod_loader
) and any(
file_version.startswith(version) for file_version in file_.gameVersions
) )
latest = max(filter(compatible, latest_files), key=lambda f: f.gameVersion) 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()
if latest is None: 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"WARNING: No compatible files found for {mod['project']}")
print( print(
f"Versions available: {[(f.name, f.gameVersion) for f in latest_files]}" f"Versions available: {[(f.name, f.gameVersions) for f in latest_files]}"
) )
new_mods.append(mod) 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: with open("temp.json", "w") as out:
json.dump(new_mods, out, sort_keys=True, indent=2) json.dump(new_mods, out, sort_keys=True, indent=2)