PixelLauncher 2.1.2 — секции настроек, панель быстрых настроек, сборки Modrinth/CurseForge

This commit is contained in:
kosta committed 2026-10-20 17:36:35 +07:00
1 parent 80dc1826a1
commit 39cf752b7b
13 files changed
+972 -49

No files matched your search

+158
View File
@@ -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)