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}") raise ValueError(f"No AddOn provider for {self}")
def download(self) -> bool: def download(self) -> bool:
try:
logger.info("Downloading %s", self.url) logger.info("Downloading %s", self.url)
self.create_download_dir() self.create_download_dir()
provider = self.get_provider() provider = self.get_provider()
changed = provider.download(self) changed = provider.download(self)
return changed return changed
except Exception as e:
logger.exception(e)
return False
def install(self) -> None: def install(self) -> None:
logger.info("Installing %s", self) logger.info("Installing %s", self)

View file

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

View file

@ -1,4 +1,7 @@
import logging import logging
import os
import urllib.error
from typing import Dict
from .web import Web from .web import Web
from .. import http from .. import http
@ -8,7 +11,16 @@ logger = logging.getLogger(__name__)
class CurseForge(Web): 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 @classmethod
def is_supported(cls, url: str) -> bool: def is_supported(cls, url: str) -> bool:
@ -43,14 +55,15 @@ class CurseForge(Web):
slug.replace("-", " ") slug.replace("-", " ")
)) ))
addons = http.open( addons = http.open(
url=f"{cls.api_url}/api/v2/addon/search", url=f"{cls.api_url}/v1/mods/search",
headers=cls.headers(),
params={ params={
"gameId": 1, # World of Warcraft "gameId": 1, # World of Warcraft
"pageSize": 50, "pageSize": 50,
"searchFilter": query "searchFilter": query
} }
).json() ).json()
for a in addons: for a in addons["data"]:
if a["slug"] == slug: if a["slug"] == slug:
addon.provider_data["curse_id"] = a["id"] addon.provider_data["curse_id"] = a["id"]
addon.name = a["name"] addon.name = a["name"]
@ -58,27 +71,41 @@ class CurseForge(Web):
raise ValueError("AddOn slug not found in CurseForge search results.") raise ValueError("AddOn slug not found in CurseForge search results.")
@classmethod @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) curse_id = cls._get_curse_id(addon)
logger.debug("Getting latest AddOn info from CurseForge") 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 key(index: int, file: dict) -> tuple:
def _get_latest_file_url(cls, addon: Addon) -> str: version_priorities = [73246, 67408, 517] # https://api.curseforge.com/v1/games/1/version-types
info = cls._get_curse_addon_info(addon) return (
flavor_priorities = [ min(version_priorities.index(v["gameVersionTypeId"]) for v in file["sortableGameVersions"]),
"wow_burning_crusade", file["releaseType"], # releaseType: 1: release, 2: beta, 3: alpha
"wow_classic", index,
"wow_retail",
]
files = sorted(
info["latestFiles"],
key=lambda f: (
flavor_priorities.index(f["gameVersionFlavor"]),
f["releaseType"] # releaseType: 1: release, 2: beta, 3: alpha
),
) )
files = sorted(enumerate(files), key=lambda x: key(*x))
try: try:
return files[0]["downloadUrl"] _, file = files[0]
except IndexError: except IndexError:
raise FileNotFoundError("No file found") 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")