Files

189 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Импорт модпаков .mrpack (формат Modrinth) и каталог модпаков.
mrpack — zip с modrinth.index.json: индекс описывает версию Minecraft,
загрузчик (fabric/quilt/forge/neoforge) и список файлов для скачивания;
плюс каталоги overrides/ (client-overrides/) — готовые файлы папки игры.
"""
import json
import os
import zipfile
from . import forge, http, mods
from .java_fetch import _safe_join
PACKS_BASE = "https://pixellauncher.ru/packs"
CATALOG_URL = PACKS_BASE + "/catalog.json"
def fetch_catalog(timeout=15):
"""Каталог модпаков с сайта: [{title, desc, url, mc, loader, size}, ...]."""
raw = http.get_bytes(CATALOG_URL, timeout=timeout)
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, list):
raise ValueError("пустой каталог модпаков")
return [x for x in data
if isinstance(x, dict) and isinstance(x.get("url"), str)
and x["url"]]
def pack_url(entry):
"""Абсолютный URL пака: относительные пути тянутся с сайта."""
u = str(entry.get("url") or "").strip()
if u.startswith(("http://", "https://")):
return u
return PACKS_BASE + "/" + u.lstrip("/")
def read_index(path):
"""Читает индекс пака. Возвращает (индекс, mc_ver, loader, loader_ver)."""
with zipfile.ZipFile(path) as z:
with z.open("modrinth.index.json") as f:
data = json.loads(f.read().decode("utf-8"))
deps = data.get("dependencies") or {}
mc = deps.get("minecraft") or ""
loader, loader_ver = "", ""
if deps.get("fabric-loader"):
loader, loader_ver = "fabric", deps["fabric-loader"]
elif deps.get("quilt-loader"):
loader, loader_ver = "quilt", deps["quilt-loader"]
elif deps.get("neoforge"):
loader, loader_ver = "neoforge", deps["neoforge"]
elif deps.get("forge"):
loader, loader_ver = "forge", deps["forge"]
if not mc:
raise RuntimeError("в паке не указана версия Minecraft")
return data, mc, loader, loader_ver
def _primary_download(downloads):
"""Достаёт основной файл из "downloads".
Формат в индексе Modrinth встречается в трёх видах:
["https://..."] — просто список ссылок,
[{"hashes": {...}, "url": "...", ...}] — старый формат с объектами,
{"primary": "https://...", "alternate": [...]} — новый формат.
"""
if isinstance(downloads, dict):
downloads = ([downloads.get("primary")] +
list(downloads.get("alternate") or []))
if not downloads:
return None
first = downloads[0]
if isinstance(first, str):
return {"url": first, "filename": "", "size": 0}
primary = next((d for d in downloads
if isinstance(d, dict) and d.get("primary")), first)
if not isinstance(primary, dict):
return None
return primary
def _files_from_index(index):
"""[(путь_относительно_игры, url, filename, size)] — клиентские файлы."""
out = []
for entry in index.get("files") or []:
env = entry.get("env") or {}
if env.get("client") == "unsupported":
continue
downloads = entry.get("downloads") or []
if not downloads:
continue
primary = _primary_download(downloads)
if not primary:
continue
url = primary.get("url") or ""
if not url:
continue
rel = (entry.get("path") or "").replace("\\", "/")
if not rel or rel.endswith("/"):
continue
out.append((rel, url, primary.get("filename") or rel.rsplit("/", 1)[-1],
int(primary.get("size") or entry.get("fileSize") or 0)))
return out
def install(game_dir, pack_path, log=None, progress=None, cancel=None):
"""Ставит пак в game_dir. Возвращает (id_версии, скачано_файлов).
progress(fraction) и cancel — как у http.download_file.
"""
if log:
log(f"Пак: читаю {os.path.basename(pack_path)}…")
index, mc, loader, loader_ver = read_index(pack_path)
files = _files_from_index(index)
total = sum(max(1, f[3]) for f in files) or 1
done = 0
# лоадер
vid = None
if loader:
if log:
log(f"Пак: Minecraft {mc}, {loader} {loader_ver}")
if loader in ("fabric", "quilt"):
vid = mods.install(game_dir, loader, mc, loader_version=loader_ver)
else: # forge / neoforge — официальным инсталлятором
vid = forge.install(game_dir, loader, mc,
loader_version=loader_ver or None, log=log)
done += max(1, total // (len(files) + 2))
# скачивание файлов
got = 0
for rel, url, filename, size in files:
if cancel is not None and cancel.is_set():
raise http.Cancelled
dest = _safe_join(game_dir, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
if log:
log(f"Пак: {rel}")
if http.download_file(url, dest, size=size, cancel=cancel):
got += 1
done += max(1, size or 1)
if progress:
progress(min(0.95, done / total))
# overrides
ov = 0
with zipfile.ZipFile(pack_path) as z:
for name in z.namelist():
target = None
for prefix in ("overrides/", "client-overrides/"):
if name.startswith(prefix) and not name.endswith("/"):
target = name[len(prefix):]
break
if not target:
continue
dest = _safe_join(game_dir, target)
os.makedirs(os.path.dirname(dest), exist_ok=True)
with z.open(name) as src, open(dest, "wb") as out:
out.write(src.read())
ov += 1
if log and ov:
log(f"Пак: распаковано {ov} файлов overrides")
return vid, got
def install_from_url(game_dir, url, log=None, progress=None, cancel=None, filename_hint="pack.mrpack"):
"""Скачивает модпак по прямой ссылке и устанавливает."""
import tempfile
dest = os.path.join(tempfile.gettempdir(), "PixelLauncher",
filename_hint.replace("/", "_"))
os.makedirs(os.path.dirname(dest), exist_ok=True)
def prog(done_b, total_b):
if progress and total_b:
progress(min(0.4, (done_b / total_b) * 0.4))
http.download_file(url, dest, progress=prog, cancel=cancel)
def p2(f):
if progress:
progress(0.4 + f * 0.6)
vid, got = install(game_dir, dest, log=log, progress=p2, cancel=cancel)
try:
os.unlink(dest)
except OSError:
pass
return vid, got