minor fixes

This commit is contained in:
SashegDev
2026-06-07 16:36:50 +03:00
parent ec7ef01760
commit b493b3278b
15 changed files with 138 additions and 106 deletions
@@ -69,7 +69,7 @@ public class Bootstrap {
private static String getServerVersion() { private static String getServerVersion() {
try { try {
URL url = new URL(BASE_URL.replace("download?type=jar", "version")); URL url = new URL(BASE_URL + "/launcher/version");
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) { if (conn.getResponseCode() == 200) {
@@ -114,7 +114,7 @@ public class LauncherAPI {
} }
idx = end; idx = end;
} }
versions.sort((a, b) -> b.compareTo(a)); versions.sort(LauncherAPI::compareVersions);
break; break;
case "neoforge": case "neoforge":
String neoforgeXml = ZHttpClient.downloadString("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml"); String neoforgeXml = ZHttpClient.downloadString("https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml");
@@ -129,7 +129,7 @@ public class LauncherAPI {
} }
neoidx = end; neoidx = end;
} }
versions.sort((a, b) -> b.compareTo(a)); versions.sort(LauncherAPI::compareVersions);
break; break;
default: default:
break; break;
@@ -141,6 +141,23 @@ public class LauncherAPI {
} }
} }
private static int compareVersions(String a, String b) {
String[] partsA = a.split("\\.");
String[] partsB = b.split("\\.");
int len = Math.min(partsA.length, partsB.length);
for (int i = 0; i < len; i++) {
try {
int numA = Integer.parseInt(partsA[i]);
int numB = Integer.parseInt(partsB[i]);
if (numA != numB) return Integer.compare(numB, numA);
} catch (NumberFormatException e) {
int cmp = partsA[i].compareTo(partsB[i]);
if (cmp != 0) return cmp;
}
}
return Integer.compare(partsB.length, partsA.length);
}
private boolean isNeoForgeCompatible(String version, String mcVersion) { private boolean isNeoForgeCompatible(String version, String mcVersion) {
if (mcVersion.startsWith("1.21")) { if (mcVersion.startsWith("1.21")) {
return version.contains("1.21") && !version.contains("1.20"); return version.contains("1.21") && !version.contains("1.20");
@@ -12,8 +12,10 @@ public class AuthService {
public ApiResponse<LoginResult> register(String username, String password) { public ApiResponse<LoginResult> register(String username, String password) {
try { try {
String response = post("/auth/register", JsonObject json = new JsonObject();
"{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}"); json.addProperty("username", username);
json.addProperty("password", password);
String response = post("/auth/register", json.toString());
// If registration succeeds, auto-login // If registration succeeds, auto-login
AuthManager.AuthResult result = AuthManager.login(username, password); AuthManager.AuthResult result = AuthManager.login(username, password);
@@ -74,8 +76,9 @@ public class AuthService {
public ApiResponse<Boolean> activatePass(String passCode) { public ApiResponse<Boolean> activatePass(String passCode) {
try { try {
String response = post("/auth/pass/activate", JsonObject json = new JsonObject();
"{\"pass_code\":\"" + passCode + "\"}"); json.addProperty("pass_code", passCode);
String response = post("/auth/pass/activate", json.toString());
AuthManager.refreshUserInfo(); AuthManager.refreshUserInfo();
return ApiResponse.success(true); return ApiResponse.success(true);
} catch (Exception e) { } catch (Exception e) {
@@ -186,7 +186,7 @@ public class LaunchService {
} }
String args = Config.getExtraJvmArgs(); String args = Config.getExtraJvmArgs();
if (args != null && !args.isEmpty()) { if (args != null && !args.isEmpty()) {
for (String arg : args.split("\\s+")) { for (String arg : args.split("\n")) {
arg = arg.trim(); arg = arg.trim();
if (!arg.isEmpty()) extraArgs.add(arg); if (!arg.isEmpty()) extraArgs.add(arg);
} }
@@ -123,12 +123,18 @@ public class AuthManager {
public static void logout() { public static void logout() {
if (session != null && session.refreshToken != null) { if (session != null && session.refreshToken != null) {
try { try {
post("/auth/logout", "{\"refresh_token\":\"" + session.refreshToken + "\"}"); JsonObject json = new JsonObject();
} catch (Exception ignored) {} json.addProperty("refresh_token", session.refreshToken);
post("/auth/logout", json.toString());
} catch (Exception e) {
LauncherLogger.warn("Logout error: " + e.getMessage());
}
} }
session = null; session = null;
userInfo = null; userInfo = null;
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {} try { Files.deleteIfExists(AUTH_FILE); } catch (Exception e) {
LauncherLogger.warn("Failed to delete auth.json: " + e.getMessage());
}
} }
public static boolean isLoggedIn() { public static boolean isLoggedIn() {
@@ -140,23 +146,28 @@ public class AuthManager {
} }
public static String getUsername() { public static String getUsername() {
return session != null ? session.username : "Player"; AuthSession localSession = session;
return localSession != null ? localSession.username : "Player";
} }
public static String getUuid() { public static String getUuid() {
return session != null ? session.uuid : "00000000-0000-0000-0000-000000000000"; AuthSession localSession = session;
return localSession != null ? localSession.uuid : "00000000-0000-0000-0000-000000000000";
} }
public static String getAccessToken() { public static String getAccessToken() {
if (session == null) return "0"; AuthSession localSession = session;
if (localSession == null) return "0";
if (isAccessTokenExpired()) { if (isAccessTokenExpired()) {
boolean refreshed = tryRefresh(); boolean refreshed = tryRefresh();
if (!refreshed) { if (!refreshed) {
if (session == null) return "0"; localSession = session;
return session.accessToken != null ? session.accessToken : "0"; if (localSession == null) return "0";
return localSession.accessToken != null ? localSession.accessToken : "0";
} }
} }
return session != null && session.accessToken != null ? session.accessToken : "0"; localSession = session;
return localSession != null && localSession.accessToken != null ? localSession.accessToken : "0";
} }
private static boolean isAccessTokenExpired() { private static boolean isAccessTokenExpired() {
@@ -174,8 +185,9 @@ public class AuthManager {
return false; return false;
} }
try { try {
String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}"; JsonObject json = new JsonObject();
SimpleHttpResponse resp = post("/auth/refresh", body); json.addProperty("refresh_token", session.refreshToken);
SimpleHttpResponse resp = post("/auth/refresh", json.toString());
if (resp.statusCode() == 200) { if (resp.statusCode() == 200) {
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class); AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
@@ -8,6 +8,7 @@ import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher; import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.LauncherLogger;
import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import java.io.BufferedReader; import java.io.BufferedReader;
@@ -160,14 +161,15 @@ public class MinecraftLib {
} }
private void safeDeleteDirectory(Path dir) { private void safeDeleteDirectory(Path dir) {
try { try (var stream = Files.walk(dir)) {
Files.walk(dir) stream.sorted((a, b) -> b.compareTo(a))
.sorted((a, b) -> b.compareTo(a))
.forEach(p -> { .forEach(p -> {
try { Files.deleteIfExists(p); } try { Files.deleteIfExists(p); }
catch (IOException ignored) {} catch (IOException e) { /* ignore */ }
}); });
} catch (IOException ignored) {} } catch (IOException e) {
LauncherLogger.warn("safeDeleteDirectory: " + e.getMessage());
}
} }
private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException { private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException {
@@ -20,6 +20,7 @@ import java.net.http.HttpResponse;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
@@ -464,12 +465,19 @@ public class PackDownloader {
*/ */
private void downloadFile(FileInfo file, Path destination) throws Exception { private void downloadFile(FileInfo file, Path destination) throws Exception {
String url = ZHttpClient.getBaseUrl() + file.getUrl(); String url = ZHttpClient.getBaseUrl() + file.getUrl();
String accessToken = AuthManager.getAccessToken();
HttpRequest request = HttpRequest.newBuilder() HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(java.net.URI.create(url)) .uri(java.net.URI.create(url))
.GET() .timeout(Duration.ofSeconds(60))
.build(); .header("User-Agent", "ZernMC-Launcher/1.0")
.GET();
if (accessToken != null && !accessToken.equals("0")) {
builder.header("Authorization", "Bearer " + accessToken);
}
HttpRequest request = builder.build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse<InputStream> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofInputStream()); HttpResponse.BodyHandlers.ofInputStream());
@@ -11,7 +11,9 @@ import java.net.http.HttpResponse;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class ForgeInstaller { public class ForgeInstaller {
@@ -240,29 +242,35 @@ public class ForgeInstaller {
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries...")); System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
// List of problematic libraries and their alternate URLs // List of problematic libraries and their alternate URLs
Map<String, String> alternativeUrls = new HashMap<>();
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar");
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar");
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) { // Map from maven path to list of mirror URLs (tried in order)
Map<String, List<String>> alternativeUrls = new HashMap<>();
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar", Arrays.asList(
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar",
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar"
));
for (Map.Entry<String, List<String>> entry : alternativeUrls.entrySet()) {
Path target = librariesDir.resolve(entry.getKey()); Path target = librariesDir.resolve(entry.getKey());
if (!Files.exists(target)) { if (!Files.exists(target)) {
Files.createDirectories(target.getParent()); Files.createDirectories(target.getParent());
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName())); System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
boolean downloaded = false;
for (String mirrorUrl : entry.getValue()) {
for (int attempt = 1; attempt <= 3; attempt++) { for (int attempt = 1; attempt <= 3; attempt++) {
try { try {
downloadFileWithProgress(entry.getValue(), target); downloadFileWithProgress(mirrorUrl, target);
downloaded = true;
break; break;
} catch (Exception e) { } catch (Exception e) {
if (attempt == 3) throw e; if (attempt == 3 && mirrorUrl.equals(entry.getValue().get(entry.getValue().size() - 1))) throw e;
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3...")); System.out.println(ZAnsi.yellow("Retry " + attempt + "/3..."));
Thread.sleep(2000); try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
} }
}
if (downloaded) break;
} }
} }
} }
@@ -183,6 +183,7 @@ public class VersionInstaller {
} }
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)"); ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)");
@@ -41,12 +41,24 @@ public class Config {
} }
} }
try {
maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096")); maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096"));
} catch (NumberFormatException e) {
System.err.println(ZAnsi.yellow("Config: invalid maxMemory value, using default"));
}
ramManuallySet = Boolean.parseBoolean(props.getProperty("ramManuallySet", "false")); ramManuallySet = Boolean.parseBoolean(props.getProperty("ramManuallySet", "false"));
serverUrl = props.getProperty("serverUrl", serverUrl); serverUrl = props.getProperty("serverUrl", serverUrl);
lastUsername = props.getProperty("lastUsername", lastUsername); lastUsername = props.getProperty("lastUsername", lastUsername);
try {
windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280")); windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280"));
} catch (NumberFormatException e) {
System.err.println(ZAnsi.yellow("Config: invalid windowWidth value, using default"));
}
try {
windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720")); windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720"));
} catch (NumberFormatException e) {
System.err.println(ZAnsi.yellow("Config: invalid windowHeight value, using default"));
}
extraJvmArgs = props.getProperty("extraJvmArgs", ""); extraJvmArgs = props.getProperty("extraJvmArgs", "");
javaPath = props.getProperty("javaPath", "java"); javaPath = props.getProperty("javaPath", "java");
locale = props.getProperty("locale", "en"); locale = props.getProperty("locale", "en");
@@ -235,7 +247,6 @@ public class Config {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("-XX:ParallelGCThreads=").append(Math.max(1, cores)); sb.append("-XX:ParallelGCThreads=").append(Math.max(1, cores));
sb.append(" -XX:ConcGCThreads=").append(Math.max(1, cores / 2)); sb.append(" -XX:ConcGCThreads=").append(Math.max(1, cores / 2));
sb.append(" -XX:+UseContainerSupport");
sb.append(" -XX:+AlwaysPreTouch"); sb.append(" -XX:+AlwaysPreTouch");
if (ramMB >= 8192) { if (ramMB >= 8192) {
sb.append(" -XX:+UseZGC"); sb.append(" -XX:+UseZGC");
+3 -10
View File
@@ -60,8 +60,8 @@ async def list_users(
query += " FROM users" query += " FROM users"
if search: if search:
query += " AND (username LIKE ? OR email LIKE ?)" query += " AND username LIKE ?"
params.extend([f"%{search}%", f"%{search}%"]) params.append(f"%{search}%")
query += " ORDER BY role DESC, username" query += " ORDER BY role DESC, username"
@@ -108,19 +108,13 @@ async def get_user_detail(
"""Детальная информация о пользователе""" """Детальная информация о пользователе"""
with get_db() as conn: with get_db() as conn:
row = conn.execute(""" row = conn.execute("""
SELECT id, username, email, uuid, role, created_at, last_login, is_active, banned_until SELECT id, username, uuid, role, created_at, last_login, is_active, banned_until
FROM users WHERE id = ? FROM users WHERE id = ?
""", (user_id,)).fetchone() """, (user_id,)).fetchone()
if not row: if not row:
raise HTTPException(404, "Пользователь не найден") raise HTTPException(404, "Пользователь не найден")
# Модераторы не видят email обычных пользователей
if current_user["role"] < ROLE_ELDER and row["role"] < ROLE_MODERATOR:
email = None
else:
email = row["email"]
# Получаем активную проходку # Получаем активную проходку
pass_info = None pass_info = None
if row["role"] >= ROLE_PASS_HOLDER or current_user["role"] >= ROLE_ELDER: if row["role"] >= ROLE_PASS_HOLDER or current_user["role"] >= ROLE_ELDER:
@@ -151,7 +145,6 @@ async def get_user_detail(
return { return {
"id": row["id"], "id": row["id"],
"username": row["username"], "username": row["username"],
"email": email,
"uuid": row["uuid"], "uuid": row["uuid"],
"role": row["role"], "role": row["role"],
"role_name": ROLE_NAMES.get(row["role"], "Неизвестно"), "role_name": ROLE_NAMES.get(row["role"], "Неизвестно"),
+2 -2
View File
@@ -135,7 +135,7 @@ async def list_friends(current_user: dict = Depends(get_current_user)):
"role": row[2], "role": row[2],
"online": bool(row[3]), "online": bool(row[3]),
"current_pack": row[4], "current_pack": row[4],
"last_seen": row[5].isoformat() if row[5] else None "last_seen": row[5] if row[5] else None
}) })
return {"friends": friends} return {"friends": friends}
@@ -155,7 +155,7 @@ async def list_friend_requests(current_user: dict = Depends(get_current_user)):
"id": row[0], "id": row[0],
"username": row[1], "username": row[1],
"role": row[2], "role": row[2],
"created_at": row[3].isoformat() if row[3] else None "created_at": row[3] if row[3] else None
}) })
return {"requests": requests} return {"requests": requests}
+9 -32
View File
@@ -647,18 +647,12 @@ class CacheControlMiddleware:
await self.app(scope, receive, send) await self.app(scope, receive, send)
return return
# Add caching headers for static files
async def send_wrapper(status, headers, *args, **kwargs): async def send_wrapper(status, headers, *args, **kwargs):
# Add cache headers for static files cache_headers = [(b"cache-control", b"public, max-age=86400")]
cache_headers = [
(b"cache-control", b"public, max-age=86400"), # 24 hours
(b"etag", b'"file-etag"'),
]
headers = list(headers) + cache_headers headers = list(headers) + cache_headers
await send(status, headers, *args, **kwargs) await send(status, headers, *args, **kwargs)
# Use original send await self.app(scope, receive, send_wrapper)
await self.app(scope, receive, send)
app.add_middleware(CacheControlMiddleware) app.add_middleware(CacheControlMiddleware)
@@ -962,7 +956,7 @@ async def get_pack_diff(
@app.get("/pack/{pack_name}") @app.get("/pack/{pack_name}")
async def get_pack_manifest(pack_name: str, request: Request): async def get_pack_manifest(pack_name: str, request: Request, current_user: dict = Depends(get_current_user)):
"""Get pack manifest with caching""" """Get pack manifest with caching"""
client_ip = request.client.host if request.client else "unknown" client_ip = request.client.host if request.client else "unknown"
@@ -1009,7 +1003,12 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request):
client_ip = request.client.host if request.client else None client_ip = request.client.host if request.client else None
# Security: prevent path traversal # Security: prevent path traversal
if ".." in file_path: try:
full_path = full_path.resolve()
pack_root = (PACKS_DIR / pack_name).resolve()
if not str(full_path).startswith(str(pack_root)):
raise HTTPException(403, "Invalid file path")
except (ValueError, OSError):
raise HTTPException(403, "Invalid file path") raise HTTPException(403, "Invalid file path")
if not full_path.exists() or not full_path.is_file(): if not full_path.exists() or not full_path.is_file():
@@ -1461,28 +1460,6 @@ async def download_legacy_launcher():
raise HTTPException(404, "No legacy launcher files available") raise HTTPException(404, "No legacy launcher files available")
@app.get("/launcher/download/zip/{filename}")
async def download_launcher_zip(filename: str):
"""Download specific launcher ZIP archive"""
if ".." in filename:
raise HTTPException(400, "Invalid filename")
valid_patterns = ["ZernMCLauncher-", "ZernMC-win-"]
if not any(filename.startswith(p) for p in valid_patterns) or not filename.endswith(".zip"):
raise HTTPException(400, "Invalid filename")
file_path = BUILDS_DIR / filename
if not file_path.exists():
raise HTTPException(404, "ZIP file not found")
return FileResponse(
path=file_path,
filename=filename,
media_type="application/zip"
)
# ====================== ЛАУНЧЕР МЕТА ЭНДПОИНТЫ ====================== # ====================== ЛАУНЧЕР МЕТА ЭНДПОИНТЫ ======================
@app.get("/launcher/meta") @app.get("/launcher/meta")
+12 -12
View File
@@ -5,6 +5,8 @@ from pathlib import Path
import json import json
from typing import Optional, Dict from typing import Optional, Dict
import structlog import structlog
import asyncio
import aiofiles
from models import PackMeta, FileEntry from models import PackMeta, FileEntry
@@ -33,9 +35,9 @@ def calculate_sha256_sync(file_path: Path) -> str:
return hash_sha.hexdigest() return hash_sha.hexdigest()
async def calculate_sha256(file_path: Path) -> str: async def calculate_sha256(file_path: Path) -> str:
"""Calculate SHA256 hash of a file (async wrapper)""" """Calculate SHA256 hash of a file (async)"""
# Используем синхронную версию для простоты loop = asyncio.get_running_loop()
return calculate_sha256_sync(file_path) return await loop.run_in_executor(None, calculate_sha256_sync, file_path)
async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
"""Scan pack directory and update manifest if needed""" """Scan pack directory and update manifest if needed"""
@@ -51,11 +53,11 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
if not force_rescan and pack_name in _manifest_cache: if not force_rescan and pack_name in _manifest_cache:
return _manifest_cache[pack_name] return _manifest_cache[pack_name]
# Load existing meta if available (синхронно) # Load existing meta if available
if meta_path.exists(): if meta_path.exists():
try: try:
with open(meta_path, 'r', encoding='utf-8') as f: async with aiofiles.open(meta_path, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.loads(await f.read())
current_meta = PackMeta.model_validate(data) current_meta = PackMeta.model_validate(data)
except Exception as e: except Exception as e:
logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}") logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}")
@@ -114,9 +116,8 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
pack_config_path = pack_path / "instance.json" pack_config_path = pack_path / "instance.json"
if pack_config_path.exists(): if pack_config_path.exists():
try: try:
# Синхронное чтение конфига async with aiofiles.open(pack_config_path, 'r', encoding='utf-8') as f:
with open(pack_config_path, 'r', encoding='utf-8') as f: config = json.loads(await f.read())
config = json.load(f)
minecraft_version = config.get("minecraftVersion", minecraft_version) minecraft_version = config.get("minecraftVersion", minecraft_version)
loader_type = config.get("loaderType", loader_type) loader_type = config.get("loaderType", loader_type)
loader_version = config.get("loaderVersion") loader_version = config.get("loaderVersion")
@@ -137,9 +138,8 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
asset_index=asset_index asset_index=asset_index
) )
# Save to disk (синхронно) async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f:
with open(meta_path, 'w', encoding='utf-8') as f: await f.write(new_meta.model_dump_json(indent=2))
f.write(new_meta.model_dump_json(indent=2))
# Update cache # Update cache
_manifest_cache[pack_name] = new_meta _manifest_cache[pack_name] = new_meta
+1 -1
View File
@@ -37,7 +37,7 @@ async def sync_playtime(
with get_db() as conn: with get_db() as conn:
cursor = conn.execute( cursor = conn.execute(
"SELECT id, minutes FROM playtime WHERE user_id = ? AND pack_name = ?", "SELECT id, minutes FROM playtime WHERE user_id = ? AND pack_name = ?",
(current_user["user_id"], req.pack_name) (current_user["id"], req.pack_name)
) )
existing = cursor.fetchone() existing = cursor.fetchone()
if existing: if existing: