Upload via GUI (19 Dateien)
This commit is contained in:
+263
@@ -0,0 +1,263 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* services.js – externe Dienste & Systemabfragen für AeroMC
|
||||
* ----------------------------------------------------------------
|
||||
* Bewusst frei von Electron-Abhängigkeiten, damit headless (mit `node`)
|
||||
* testbar. Nutzt globales fetch (Node 18+ / Electron 28).
|
||||
*
|
||||
* - scanJava() : findet installierte Java-Runtimes
|
||||
* - getMcVersions() : Mojang-Versionsliste (mit Datei-Cache)
|
||||
* - modrinthSearch() : Mod-Suche
|
||||
* - modrinthInstall() : passende Mod-Version in einen Ordner laden
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
|
||||
const UA = 'AeroMC-Launcher/0.2 (Minecraft-Launcher; contact: viper)';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java-Erkennung
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function javaCandidateDirs() {
|
||||
const pf = process.env['ProgramFiles'] || 'C:\\Program Files';
|
||||
const pfx = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
||||
const appdata = process.env['APPDATA'] || '';
|
||||
const localappdata = process.env['LOCALAPPDATA'] || '';
|
||||
const vendors = ['Java', 'Eclipse Adoptium', 'Eclipse Foundation', 'Zulu', 'Amazon Corretto',
|
||||
'BellSoft', 'Microsoft', 'AdoptOpenJDK', 'Semeru', 'JetBrains'];
|
||||
const roots = [];
|
||||
for (const base of [pf, pfx]) {
|
||||
for (const v of vendors) roots.push(path.join(base, v));
|
||||
}
|
||||
// Runtimes der offiziellen Minecraft-Launcher
|
||||
if (appdata) roots.push(path.join(appdata, '.minecraft', 'runtime'));
|
||||
if (localappdata) roots.push(path.join(localappdata, 'Packages'));
|
||||
return roots;
|
||||
}
|
||||
|
||||
// findet rekursiv (begrenzte Tiefe) alle java.exe unter einem Startordner
|
||||
function findJavaExes(root, depth, acc) {
|
||||
if (depth < 0 || !fs.existsSync(root)) return;
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
|
||||
for (const e of entries) {
|
||||
const full = path.join(root, e.name);
|
||||
if (e.isFile() && /^javaw?\.exe$/i.test(e.name)) {
|
||||
// bevorzugt java.exe (Konsole) statt javaw.exe
|
||||
if (/^java\.exe$/i.test(e.name)) acc.add(full);
|
||||
} else if (e.isDirectory()) {
|
||||
// Tiefe begrenzen; typische Struktur .../<jdk>/bin/java.exe
|
||||
findJavaExes(full, depth - 1, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function probeJava(javaPath) {
|
||||
// `java -version` schreibt nach stderr; spawnSync liefert stdout UND stderr.
|
||||
try {
|
||||
const r = spawnSync(javaPath, ['-version'], { encoding: 'utf8', timeout: 5000 });
|
||||
const out = (r.stderr || '') + (r.stdout || '');
|
||||
if (!out.trim()) return null;
|
||||
return parseJavaVersion(javaPath, out);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJavaVersion(javaPath, out) {
|
||||
const m = out.match(/version "([^"]+)"/i);
|
||||
const raw = m ? m[1] : '';
|
||||
let major = 0;
|
||||
if (raw) {
|
||||
const parts = raw.split('.');
|
||||
major = raw.startsWith('1.') ? parseInt(parts[1], 10) : parseInt(parts[0], 10);
|
||||
}
|
||||
const arch = /64-bit/i.test(out) ? 'x64' : (/32-bit|Client VM/i.test(out) ? 'x86' : '?');
|
||||
const vendorLine = out.split('\n').map((l) => l.trim()).find((l) => /runtime|openjdk|hotspot|jre|jdk/i.test(l)) || '';
|
||||
return { path: javaPath, version: raw || 'unbekannt', major: major || 0, arch, info: vendorLine };
|
||||
}
|
||||
|
||||
function scanJava() {
|
||||
const found = new Set();
|
||||
|
||||
// 1) JAVA_HOME
|
||||
if (process.env.JAVA_HOME) {
|
||||
const p = path.join(process.env.JAVA_HOME, 'bin', 'java.exe');
|
||||
if (fs.existsSync(p)) found.add(p);
|
||||
}
|
||||
// 2) java aus PATH
|
||||
try {
|
||||
const where = execFileSync('where', ['java'], { encoding: 'utf8', timeout: 4000 });
|
||||
where.split(/\r?\n/).forEach((l) => { l = l.trim(); if (l && /java\.exe$/i.test(l) && fs.existsSync(l)) found.add(l); });
|
||||
} catch { /* nix */ }
|
||||
// 3) bekannte Vendor-Ordner
|
||||
for (const root of javaCandidateDirs()) findJavaExes(root, 4, found);
|
||||
|
||||
// prüfen & dedupen nach Version+Pfad
|
||||
const results = [];
|
||||
for (const p of found) {
|
||||
const info = probeJava(p);
|
||||
if (info) results.push(info);
|
||||
}
|
||||
// sortieren: 64-bit vor 32-bit, höhere Major-Version zuerst
|
||||
results.sort((a, b) => {
|
||||
if ((b.arch === 'x64') - (a.arch === 'x64') !== 0) return (b.arch === 'x64') - (a.arch === 'x64');
|
||||
return b.major - a.major;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mojang-Versionsliste (mit Cache)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MC_MANIFEST_URL = 'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json';
|
||||
|
||||
async function getMcVersions(cacheDir, maxAgeMs = 6 * 3600 * 1000) {
|
||||
const cacheFile = path.join(cacheDir, 'mc-versions.json');
|
||||
// Cache frisch genug?
|
||||
try {
|
||||
const st = fs.statSync(cacheFile);
|
||||
if (Date.now() - st.mtimeMs < maxAgeMs) {
|
||||
return Object.assign({ cached: true }, JSON.parse(fs.readFileSync(cacheFile, 'utf8')));
|
||||
}
|
||||
} catch { /* kein Cache */ }
|
||||
|
||||
try {
|
||||
const res = await fetch(MC_MANIFEST_URL, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
const slim = {
|
||||
latest: data.latest,
|
||||
versions: data.versions.map((v) => ({ id: v.id, type: v.type, releaseTime: v.releaseTime, url: v.url, sha1: v.sha1 })),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
};
|
||||
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.writeFileSync(cacheFile, JSON.stringify(slim)); } catch { /* egal */ }
|
||||
return Object.assign({ cached: false }, slim);
|
||||
} catch (err) {
|
||||
// offline: alten Cache nehmen, falls vorhanden
|
||||
try {
|
||||
const old = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
return Object.assign({ cached: true, offline: true }, old);
|
||||
} catch {
|
||||
return { offline: true, error: err.message, latest: {}, versions: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modrinth (Mod-Suche & Installation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MODRINTH = 'https://api.modrinth.com/v2';
|
||||
|
||||
const MODRINTH_SORTS = ['relevance', 'downloads', 'follows', 'newest', 'updated'];
|
||||
|
||||
async function modrinthSearch({ query = '', gameVersion = '', loader = '', category = '', sort = 'relevance', limit = 30, offset = 0 } = {}) {
|
||||
const facets = [['project_type:mod']];
|
||||
if (gameVersion) facets.push(['versions:' + gameVersion]);
|
||||
if (loader && loader !== 'vanilla') facets.push(['categories:' + loader]);
|
||||
if (category) facets.push(['categories:' + category]);
|
||||
const url = new URL(MODRINTH + '/search');
|
||||
url.searchParams.set('query', query);
|
||||
url.searchParams.set('limit', String(limit));
|
||||
url.searchParams.set('offset', String(offset));
|
||||
url.searchParams.set('index', MODRINTH_SORTS.includes(sort) ? sort : 'relevance');
|
||||
url.searchParams.set('facets', JSON.stringify(facets));
|
||||
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('Modrinth-Suche fehlgeschlagen: HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
return {
|
||||
total: data.total_hits,
|
||||
hits: (data.hits || []).map((h) => ({
|
||||
projectId: h.project_id,
|
||||
slug: h.slug,
|
||||
title: h.title,
|
||||
description: h.description,
|
||||
author: h.author,
|
||||
downloads: h.downloads,
|
||||
iconUrl: h.icon_url || '',
|
||||
categories: h.categories || [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// findet die passende Datei-Version für Spielversion + Loader
|
||||
async function modrinthResolveFile({ projectId, gameVersion, loader }) {
|
||||
const url = new URL(MODRINTH + '/project/' + projectId + '/version');
|
||||
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
|
||||
if (loader && loader !== 'vanilla') url.searchParams.set('loaders', JSON.stringify([loader]));
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('Versionsabruf fehlgeschlagen: HTTP ' + res.status);
|
||||
const versions = await res.json();
|
||||
if (!versions.length) return null;
|
||||
const v = versions[0]; // neueste passende
|
||||
const file = v.files.find((f) => f.primary) || v.files[0];
|
||||
if (!file) return null;
|
||||
return {
|
||||
versionId: v.id,
|
||||
versionNumber: v.version_number,
|
||||
filename: file.filename,
|
||||
url: file.url,
|
||||
sha1: file.hashes && file.hashes.sha1,
|
||||
size: file.size,
|
||||
dependencies: v.dependencies || [],
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadFile(url, destPath) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||
fs.writeFileSync(destPath, buf);
|
||||
return buf.length;
|
||||
}
|
||||
|
||||
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
|
||||
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir }) {
|
||||
const file = await modrinthResolveFile({ projectId, gameVersion, loader });
|
||||
if (!file) {
|
||||
return { ok: false, reason: 'no-match',
|
||||
message: `Keine passende Version für ${gameVersion} / ${loader} gefunden.` };
|
||||
}
|
||||
const dest = path.join(modsDir, file.filename);
|
||||
const bytes = await downloadFile(file.url, dest);
|
||||
return {
|
||||
ok: true,
|
||||
dependencies: file.dependencies || [],
|
||||
entry: {
|
||||
projectId,
|
||||
title: title || projectId,
|
||||
filename: file.filename,
|
||||
versionId: file.versionId,
|
||||
versionNumber: file.versionNumber,
|
||||
gameVersion,
|
||||
loader,
|
||||
installedAt: new Date().toISOString(),
|
||||
bytes,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Projekttitel nachschlagen (für Abhängigkeiten, die nur eine ID liefern)
|
||||
async function modrinthProjectTitle(projectId) {
|
||||
try {
|
||||
const res = await fetch(`${MODRINTH}/project/${projectId}`, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) return projectId;
|
||||
const data = await res.json();
|
||||
return data.title || projectId;
|
||||
} catch { return projectId; }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanJava, probeJava,
|
||||
getMcVersions,
|
||||
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle, downloadFile,
|
||||
};
|
||||
Reference in New Issue
Block a user