Curseforge v2 API
This commit is contained in:
parent
94b1617d7e
commit
ca190bfc94
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue