2021-07-25 22:21:29 +01:00
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import hashlib
|
2021-07-26 01:49:18 +01:00
|
|
|
import pathlib
|
2021-07-25 22:21:29 +01:00
|
|
|
from copy import deepcopy
|
2021-10-06 01:13:31 +01:00
|
|
|
from datetime import datetime
|
2021-07-25 22:21:29 +01:00
|
|
|
from enum import Enum
|
2021-10-06 01:13:31 +01:00
|
|
|
from typing import Any, Dict, Generator, List, NamedTuple, Optional, Union
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
import requests
|
2021-10-06 01:13:31 +01:00
|
|
|
from dateutil import parser
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
API = "https://addons-ecs.forgesvc.net/api/v2"
|
2021-10-06 01:13:31 +01:00
|
|
|
JSON = Union[List[Dict[str, Any]], Dict[str, Any]]
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
class ModLoader(Enum):
|
|
|
|
FORGE = 1
|
|
|
|
FABRIC = 4
|
|
|
|
|
|
|
|
|
|
|
|
class File(NamedTuple):
|
|
|
|
id: int
|
2021-10-06 01:13:31 +01:00
|
|
|
gameVersions: List[str]
|
2021-07-25 22:21:29 +01:00
|
|
|
name: str
|
|
|
|
modLoader: Optional[ModLoader]
|
2021-10-06 01:13:31 +01:00
|
|
|
date: datetime
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
@classmethod
|
2021-10-06 01:13:31 +01:00
|
|
|
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
|
|
|
|
)
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
return cls(
|
2021-10-06 01:13:31 +01:00
|
|
|
f["id"],
|
2021-07-25 22:21:29 +01:00
|
|
|
f["gameVersion"],
|
2021-10-06 01:13:31 +01:00
|
|
|
f["fileName"],
|
|
|
|
modLoader,
|
|
|
|
parser.isoparse(f["fileDate"]),
|
2021-07-25 22:21:29 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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]:
|
2021-10-06 01:13:31 +01:00
|
|
|
res = self._session.get(f"{API}/addon/{mod_id}/files")
|
2021-07-25 22:21:29 +01:00
|
|
|
res.raise_for_status()
|
|
|
|
|
2021-10-06 01:13:31 +01:00
|
|
|
latest_files = res.json()
|
2021-07-25 22:21:29 +01:00
|
|
|
|
2021-10-06 01:13:31 +01:00
|
|
|
return (File.from_json(f) for f in latest_files)
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
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"
|
|
|
|
)
|
2021-07-26 01:49:18 +01:00
|
|
|
parser.add_argument("--infile", type=pathlib.Path)
|
2021-07-25 22:21:29 +01:00
|
|
|
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")
|
|
|
|
|
2021-07-26 01:49:18 +01:00
|
|
|
update(args.infile, args.version, mod_loader)
|
2021-07-25 22:21:29 +01:00
|
|
|
|
|
|
|
|
2021-07-26 01:49:18 +01:00
|
|
|
def update(infile: pathlib.Path, version: str, mod_loader: ModLoader):
|
|
|
|
with open(infile) as mods_json:
|
2021-07-25 22:21:29 +01:00
|
|
|
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 (
|
2021-10-06 01:13:31 +01:00
|
|
|
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
|
2021-07-25 22:21:29 +01:00
|
|
|
)
|
|
|
|
|
2021-10-06 01:13:31 +01:00
|
|
|
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:
|
2021-07-25 22:21:29 +01:00
|
|
|
print(f"WARNING: No compatible files found for {mod['project']}")
|
|
|
|
print(
|
2021-10-06 01:13:31 +01:00
|
|
|
f"Versions available: {[(f.name, f.gameVersions) for f in latest_files]}"
|
2021-07-25 22:21:29 +01:00
|
|
|
)
|
|
|
|
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()
|