# -*- 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)