Curseforge v2 API

This commit is contained in:
Casper V. Kristensen 2022-07-09 02:11:06 +02:00
parent 94b1617d7e
commit ca190bfc94
3 changed files with 63 additions and 28 deletions

View file

@ -65,11 +65,15 @@ class Addon:
raise ValueError(f"No AddOn provider for {self}")
def download(self) -> bool:
try:
logger.info("Downloading %s", self.url)
self.create_download_dir()
provider = self.get_provider()
changed = provider.download(self)
return changed
except Exception as e:
logger.exception(e)
return False
def install(self) -> None:
logger.info("Installing %s", self)

View file

@ -9,6 +9,7 @@ from functools import lru_cache
from http.client import HTTPResponse
from pathlib import Path
from typing import Mapping
from typing import Optional
from urllib.request import Request
@ -43,17 +44,20 @@ class Response:
return json.loads(self.bytes)
def open(url: str, params: Mapping = None) -> Response:
def open(url: str, params: Mapping = None, headers: Optional[Mapping] = None) -> Response:
while True:
try:
if params is not None:
url += "?" + urllib.parse.urlencode(params)
request = Request(url)
request.add_header("User-Agent", HTTP_USER_AGENT)
if headers is not None:
for header_key, header_value in headers.items():
request.add_header(header_key, header_value)
http_response = urllib.request.urlopen(request, timeout=HTTP_TIMEOUT)
return Response(http_response)
except TimeoutError as e:
logger.exception("Timeout", exc_info=e)
print(e)
def download_zip(url: str, dest: Path) -> None:

View file

@ -1,4 +1,7 @@
import logging
import os
import urllib.error
from typing import Dict
from .web import Web
from .. import http
@ -8,7 +11,16 @@ logger = logging.getLogger(__name__)
class CurseForge(Web):
api_url = "https://addons-ecs.forgesvc.net"
api_url = "https://api.curseforge.com"
@classmethod
def headers(cls) -> Dict[str, str]:
api_key = os.getenv("CURSE_API_KEY")
if not api_key:
logger.warning("No CURSE_API_KEY!")
return {
"x-api-key": api_key,
}
@classmethod
def is_supported(cls, url: str) -> bool:
@ -43,14 +55,15 @@ class CurseForge(Web):
slug.replace("-", " ")
))
addons = http.open(
url=f"{cls.api_url}/api/v2/addon/search",
url=f"{cls.api_url}/v1/mods/search",
headers=cls.headers(),
params={
"gameId": 1, # World of Warcraft
"pageSize": 50,
"searchFilter": query
}
).json()
for a in addons:
for a in addons["data"]:
if a["slug"] == slug:
addon.provider_data["curse_id"] = a["id"]
addon.name = a["name"]
@ -58,27 +71,41 @@ class CurseForge(Web):
raise ValueError("AddOn slug not found in CurseForge search results.")
@classmethod
def _get_curse_addon_info(cls, addon: Addon) -> dict:
def _get_latest_file_url(cls, addon: Addon) -> str:
curse_id = cls._get_curse_id(addon)
logger.debug("Getting latest AddOn info from CurseForge")
return http.open(f"{cls.api_url}/api/v2/addon/{curse_id}").json()
files = http.open(f"{cls.api_url}/v1/mods/{curse_id}/files", headers=cls.headers()).json()["data"]
@classmethod
def _get_latest_file_url(cls, addon: Addon) -> str:
info = cls._get_curse_addon_info(addon)
flavor_priorities = [
"wow_burning_crusade",
"wow_classic",
"wow_retail",
]
files = sorted(
info["latestFiles"],
key=lambda f: (
flavor_priorities.index(f["gameVersionFlavor"]),
f["releaseType"] # releaseType: 1: release, 2: beta, 3: alpha
),
def key(index: int, file: dict) -> tuple:
version_priorities = [73246, 67408, 517] # https://api.curseforge.com/v1/games/1/version-types
return (
min(version_priorities.index(v["gameVersionTypeId"]) for v in file["sortableGameVersions"]),
file["releaseType"], # releaseType: 1: release, 2: beta, 3: alpha
index,
)
files = sorted(enumerate(files), key=lambda x: key(*x))
try:
return files[0]["downloadUrl"]
_, file = files[0]
except IndexError:
raise FileNotFoundError("No file found")
return cls._get_file_url(file)
@classmethod
def _get_file_url(cls, file: dict) -> str:
url = file["downloadUrl"]
if url is not None:
return url
# addon authors can choose to "hide" the download url from the new api, but it can be bruteforced
id = str(file["id"])
file_name = file["fileName"]
for index in range(len(id)):
x, y = id[:index], id[index:]
url = f"https://edge.forgecdn.net/files/{x}/{y}/{file_name}"
try:
http.open(url).head()
return url
except (urllib.error.HTTPError, urllib.error.URLError) as e:
pass
raise FileNotFoundError("No file URL found")