159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Клиент CurseForge API (https://api.curseforge.com/v1).
|
|
|
|
Официальный API требует заголовок x-api-key. Без ключа используется публичное
|
|
зеркало api.curse.tools — формат ответов там тот же (v1), так что код один.
|
|
Если ключ задан (set_api_key), запросы идут сначала на официальный API.
|
|
"""
|
|
import json
|
|
import urllib.parse
|
|
|
|
from . import http
|
|
|
|
OFFICIAL = "https://api.curseforge.com/v1"
|
|
MIRROR = "https://api.curse.tools/v1/cf"
|
|
HEADERS = {"Accept": "application/json",
|
|
"User-Agent": "PixelLauncher/2.1.1 (lightweight Minecraft launcher)"}
|
|
|
|
# числовые id загрузчиков в CurseForge
|
|
LOADER_NAMES = {1: "forge", 2: "cauldron", 3: "liteloader", 4: "fabric",
|
|
5: "quilt", 6: "neoforge"}
|
|
|
|
_api_key = ""
|
|
|
|
|
|
def set_api_key(key):
|
|
"""Ключ CurseForge API (необязателен — есть зеркало без ключа)."""
|
|
global _api_key
|
|
_api_key = (key or "").strip()
|
|
|
|
|
|
def get_api_key():
|
|
return _api_key
|
|
|
|
|
|
def _bases():
|
|
if _api_key:
|
|
return [OFFICIAL, MIRROR]
|
|
return [MIRROR]
|
|
|
|
|
|
def _get(path, params=None, timeout=30):
|
|
"""GET API. Бросает RuntimeError с текстом последней ошибки."""
|
|
last = None
|
|
for base in _bases():
|
|
url = base + path
|
|
if params:
|
|
url += "?" + urllib.parse.urlencode(params)
|
|
headers = dict(HEADERS)
|
|
if base == OFFICIAL and _api_key:
|
|
headers["x-api-key"] = _api_key
|
|
try:
|
|
raw = http.get_bytes(url, timeout=timeout, headers=headers)
|
|
return json.loads(raw.decode("utf-8"))
|
|
except Exception as e: # noqa: BLE001
|
|
last = e
|
|
raise RuntimeError(str(last) if last else "CurseForge недоступен")
|
|
|
|
|
|
def search_modpacks(search_filter="", game_version="", modloader_type=None,
|
|
page_size=20, index=0):
|
|
"""Поиск сборок (classId 4471). Возвращает (список, всего_найдено)."""
|
|
params = {
|
|
"gameId": 432,
|
|
"classId": 4471,
|
|
"pageSize": page_size,
|
|
"index": index,
|
|
"sortField": 2, # downloads
|
|
"sortOrder": "desc",
|
|
}
|
|
if search_filter:
|
|
params["searchFilter"] = search_filter
|
|
if game_version:
|
|
params["gameVersion"] = game_version
|
|
if modloader_type:
|
|
params["modLoaderType"] = modloader_type
|
|
data = _get("/mods/search", params)
|
|
out = []
|
|
for m in (data.get("data") or []):
|
|
logo = m.get("logo") or {}
|
|
files = m.get("latestFiles") or []
|
|
latest = next((f for f in files if f.get("downloadUrl")),
|
|
files[0] if files else None)
|
|
latest = latest or {}
|
|
loaders = []
|
|
for idx in (m.get("latestFilesIndexes") or []):
|
|
name = LOADER_NAMES.get(idx.get("modLoader"))
|
|
if name and name not in loaders:
|
|
loaders.append(name)
|
|
out.append({
|
|
"id": m.get("id"),
|
|
"name": m.get("name") or "?",
|
|
"slug": m.get("slug") or "",
|
|
"summary": (m.get("summary") or "").strip(),
|
|
"downloads": int(m.get("downloadCount") or 0),
|
|
"icon": logo.get("thumbnailUrl") or logo.get("url") or "",
|
|
"file_id": latest.get("id"),
|
|
"file_name": latest.get("fileName") or "",
|
|
"download_url": latest.get("downloadUrl") or "",
|
|
"size": int(latest.get("fileLength") or 0),
|
|
"game_versions": [x.get("gameVersion")
|
|
for x in (m.get("latestFilesIndexes") or [])],
|
|
"loaders": loaders,
|
|
})
|
|
return out, int((data.get("pagination") or {}).get("totalCount") or 0)
|
|
|
|
|
|
def get_files(mod_id, game_version="", modloader_type=None, page_size=50):
|
|
"""Файлы сборки/мода. Возвращает список словарей."""
|
|
params = {"pageSize": page_size}
|
|
if game_version:
|
|
params["gameVersion"] = game_version
|
|
if modloader_type:
|
|
params["modLoaderType"] = modloader_type
|
|
data = _get("/mods/%s/files" % mod_id, params)
|
|
out = []
|
|
for f in (data.get("data") or []):
|
|
out.append(_file(f))
|
|
return out
|
|
|
|
|
|
def get_file(mod_id, file_id):
|
|
"""Один файл по id (нужен downloadUrl для скачивания)."""
|
|
data = _get("/mods/%s/files/%s" % (mod_id, file_id))
|
|
f = data.get("data") if isinstance(data, dict) else None
|
|
return _file(f) if isinstance(f, dict) else {}
|
|
|
|
|
|
def _file(f):
|
|
"""Приводит ответ API к общему виду."""
|
|
hashes = {}
|
|
for h in (f.get("hashes") or []):
|
|
algo = h.get("algo")
|
|
if algo == 1:
|
|
hashes["sha1"] = h.get("value") or ""
|
|
elif algo == 2:
|
|
hashes["md5"] = h.get("value") or ""
|
|
return {
|
|
"id": f.get("id"),
|
|
"mod_id": f.get("modId"),
|
|
"name": f.get("displayName") or f.get("fileName") or "",
|
|
"file_name": f.get("fileName") or "",
|
|
"download_url": f.get("downloadUrl") or "",
|
|
"size": int(f.get("fileLength") or 0),
|
|
"date": (f.get("fileDate") or "")[:10],
|
|
"game_versions": f.get("gameVersions") or [],
|
|
"sha1": hashes.get("sha1", ""),
|
|
"md5": hashes.get("md5", ""),
|
|
}
|
|
|
|
|
|
def get_download_url(mod_id, file_id):
|
|
"""Прямая ссылка на файл (downloadUrl из API)."""
|
|
return get_file(mod_id, file_id).get("download_url") or ""
|
|
|
|
|
|
def download_pack(url, dest, size=0, progress=None, cancel=None):
|
|
return http.download_file(url, dest, size=size, progress=progress,
|
|
cancel=cancel)
|