PixelLauncher 2.1.2 — секции настроек, панель быстрых настроек, сборки Modrinth/CurseForge
This commit is contained in:
1 parent
80dc1826a1
commit
39cf752b7b
13 files changed
+972
-49
No files matched your search
@@ -0,0 +1,158 @@
|
||||
# -*- 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)
|
||||
Reference in new issue
Block a user