Files

210 lines
7.7 KiB
Python

# -*- coding: utf-8 -*-
"""Клиент API Modrinth (https://api.modrinth.com/v2) — без ключей и библиотек.
Поиск модов, получение версий проекта и скачивание файлов.
Списки игнорируются через обычный http (умеет прокси и зеркала).
"""
import json
import time
import urllib.parse
from . import http
BASE = "https://api.modrinth.com/v2"
_cache = {} # url -> (время, данные)
# slugs официальных проектов
FABRIC_API_SLUG = "fabric-api"
QUILTED_API_SLUG = "quilted-fabric-api"
def _get_json(url, ttl=300):
hit = _cache.get(url)
if hit and time.time() - hit[0] < ttl:
return hit[1]
data = json.loads(http.get_bytes(url, timeout=30).decode("utf-8"))
_cache[url] = (time.time(), data)
return data
def search(query, game_version="", loader="", limit=20, offset=0):
"""Поиск модов. Возвращает (список, всего_найдено)."""
facets = [["project_type:mod"]]
if game_version:
facets.append(["versions:" + game_version])
if loader:
facets.append(["categories:" + loader])
params = {
"query": query or "",
"facets": json.dumps(facets, separators=(",", ":")),
"limit": limit,
"offset": offset,
"index": "relevance" if query else "downloads",
}
url = BASE + "/search?" + urllib.parse.urlencode(params)
data = _get_json(url, ttl=300)
out = []
for h in data.get("hits") or []:
out.append({
"id": h.get("project_id") or "",
"title": h.get("title") or h.get("slug") or "?",
"slug": h.get("slug") or "",
"desc": (h.get("description") or "").strip(),
"downloads": int(h.get("downloads") or 0),
"follows": int(h.get("follows") or 0),
"icon": h.get("icon_url") or "",
"author": h.get("author") or "",
"categories": h.get("categories") or [],
})
return out, int(data.get("total_hits") or 0)
def project(pid):
"""Карточка проекта: заголовок, описание."""
d = _get_json(BASE + "/project/" + str(pid), ttl=600)
return {
"id": d.get("id") or "",
"title": d.get("title") or "?",
"desc": (d.get("description") or "").strip(),
"body": d.get("body") or "",
"downloads": int(d.get("downloads") or 0),
"icon": d.get("icon_url") or "",
}
def versions(pid, game_version="", loader=""):
"""Версии проекта под версию игры/загрузчик (свежие первыми)."""
url = BASE + "/project/" + str(pid) + "/version"
qs = []
if game_version:
qs.append("game_versions=" + urllib.parse.quote(
json.dumps([game_version])))
if loader:
qs.append("loaders=" + urllib.parse.quote(json.dumps([loader])))
if qs:
url += "?" + "&".join(qs)
raw = _get_json(url, ttl=300)
out = []
for v in raw if isinstance(raw, list) else []:
files = v.get("files") or []
if not files:
continue
primary = next((f for f in files if f.get("primary")), files[0])
hashes = primary.get("hashes") or {}
out.append({
"id": v.get("id") or "",
"name": (v.get("name") or v.get("version_number") or "?").strip(),
"number": (v.get("version_number") or "").strip(),
"date": (v.get("date_published") or "")[:10],
"url": primary.get("url") or "",
"filename": primary.get("filename") or "",
"size": int(primary.get("size") or 0),
"sha1": hashes.get("sha1") or "",
})
return out
def download_mod(file_url, dest, size=0, progress=None, cancel=None):
"""Скачивает файл мода в dest. True при успехе."""
return http.download_file(file_url, dest, size=size,
progress=progress, cancel=cancel)
def lookup_files(hashes, algorithm="sha1", batch=100):
"""Определяет версии Modrinth по хэшам файлов.
POST /version_files: {хэш: версия, в которой лежит этот файл}.
"""
out = {}
lst = [h for h in hashes if h]
for i in range(0, len(lst), batch):
data = http.post_json(BASE + "/version_files",
{"hashes": lst[i:i + batch],
"algorithm": algorithm}, timeout=30)
if isinstance(data, dict):
out.update({k: v for k, v in data.items() if isinstance(v, dict)})
return out
def _find_installed(mods_dir, prefixes):
"""Есть ли уже мод с одним из префиксов в имени файла."""
import os
if not os.path.isdir(mods_dir):
return False
low = [p.lower() for p in prefixes]
for name in os.listdir(mods_dir):
lname = name.lower()
if lname.endswith(".jar") and any(lname.startswith(p) for p in low):
return True
return False
def search_modpacks(query, game_version="", loader="", limit=20, offset=0):
"""Поиск сборок Modrinth (project_type=modpack)."""
facets = [["project_type:modpack"]]
if game_version:
facets.append(["versions:" + game_version])
if loader:
facets.append(["categories:" + loader])
params = {
"query": query or "",
"facets": json.dumps(facets, separators=(",", ":")),
"limit": limit,
"offset": offset,
"index": "relevance" if query else "downloads",
}
url = BASE + "/search?" + urllib.parse.urlencode(params)
data = _get_json(url, ttl=300)
out = []
for h in data.get("hits") or []:
out.append({
"id": h.get("project_id") or h.get("project_id") or h.get("slug") or "",
"slug": h.get("slug") or "",
"title": h.get("title") or h.get("slug") or "?",
"desc": (h.get("description") or "").strip(),
"downloads": int(h.get("downloads") or 0),
"icon": h.get("icon_url") or "",
"game_versions": h.get("versions") or [],
"categories": h.get("categories") or [],
})
return out, int(data.get("total_hits") or 0)
def get_modpack_versions(pid, game_version="", loader=""):
"""Версии сборки Modrinth."""
url = BASE + "/project/" + str(pid) + "/version"
qs = []
if game_version:
qs.append("game_versions=" + urllib.parse.quote(json.dumps([game_version])))
if loader:
qs.append("loaders=" + urllib.parse.quote(json.dumps([loader])))
if qs:
url += "?" + "&".join(qs)
raw = _get_json(url, ttl=300)
out = []
for v in raw if isinstance(raw, list) else []:
files = v.get("files") or []
if not files:
continue
primary = next((f for f in files if f.get("primary")), files[0])
hashes = primary.get("hashes") or {}
out.append({
"id": v.get("id") or "",
"name": (v.get("name") or v.get("version_number") or "?").strip(),
"number": (v.get("version_number") or "").strip(),
"date": (v.get("date_published") or "")[:10],
"url": primary.get("url") or "",
"filename": primary.get("filename") or "",
"size": int(primary.get("size") or 0),
"sha1": hashes.get("sha1") or "",
"game_versions": v.get("game_versions") or [],
"loaders": v.get("loaders") or [],
})
return out
def download_pack(file_url, dest, size=0, progress=None, cancel=None):
return http.download_file(file_url, dest, size=size, progress=progress, cancel=cancel)