Upload via GUI (43 Dateien)
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* technicdiscover.js – Technic-Modpacks direkt über die Technic-Plattform
|
||||
* durchsuchen und installieren, ohne vorher manuell eine Datei herunterladen
|
||||
* zu müssen. Genutzt wird dieselbe öffentliche API wie bei Prism Launcher:
|
||||
*
|
||||
* Suche: GET https://api.technicpack.net/search?build=multimc&q=<term>
|
||||
* Beliebt: GET https://api.technicpack.net/trending?build=multimc
|
||||
* Pack-Infos: GET https://api.technicpack.net/modpack/<slug>?build=multimc
|
||||
* -> entweder "url" (ein fertiges ZIP) oder "solder" (Solder-Server)
|
||||
* Solder: GET <solder>modpack/<slug> – Liste der Builds
|
||||
* GET <solder>modpack/<slug>/<build> – Mods dieser Fassung
|
||||
* { minecraft, forge, mods: [{ name, url, ... }] }
|
||||
*
|
||||
* "build=multimc" ist die Kennung, mit der sich auch andere Drittlauncher
|
||||
* bei der Technic-Plattform melden – kein echter Minecraft-Build, sondern
|
||||
* nur ein Client-Identifier, den die API erwartet.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const forge = require('./forge');
|
||||
const technicimport = require('./technicimport');
|
||||
|
||||
const API_BASE = 'https://api.technicpack.net/';
|
||||
const API_BUILD = 'multimc';
|
||||
const UA = 'AeroMC-Launcher/1.0';
|
||||
|
||||
async function getJson(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getBuffer(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/*
|
||||
* query leer -> beliebte Packs (trending), sonst Suchtreffer.
|
||||
* -> [{ slug, name, iconUrl }]
|
||||
*/
|
||||
async function search(query) {
|
||||
const q = String(query || '').trim();
|
||||
const url = q
|
||||
? `${API_BASE}search?build=${API_BUILD}&q=${encodeURIComponent(q)}`
|
||||
: `${API_BASE}trending?build=${API_BUILD}`;
|
||||
const data = await getJson(url);
|
||||
const packs = Array.isArray(data.modpacks) ? data.modpacks : [];
|
||||
return packs
|
||||
.filter((p) => p && p.slug && p.slug !== 'vanilla')
|
||||
.map((p) => ({ slug: p.slug, name: p.name || p.slug, iconUrl: p.iconUrl || '' }));
|
||||
}
|
||||
|
||||
/*
|
||||
* Entpackt einen ZIP-Buffer nach destDir.
|
||||
* Enthält der ZIP-Inhalt bereits eine erkennbare Ordnerstruktur (mods/,
|
||||
* config/, …), wird sie 1:1 übernommen. Liegen nur einzelne Dateien direkt
|
||||
* im ZIP-Wurzelverzeichnis (typisch für ältere, einzeln gepackte Solder-
|
||||
* Mods), landen sie stattdessen im mods-Ordner.
|
||||
*/
|
||||
function extractZipBuffer(buf, destDir) {
|
||||
const idx = forge.readZipIndex(buf);
|
||||
const namen = Object.keys(idx);
|
||||
const hatOrdnerStruktur = namen.some((n) => /^(mods|config|resources|resourcepacks|scripts|structures|bin)\//i.test(n));
|
||||
for (const name of namen) {
|
||||
if (name.endsWith('/')) continue; // reines Verzeichnis – wird beim Schreiben automatisch angelegt
|
||||
const relPfad = hatOrdnerStruktur ? name : path.posix.join('mods', name);
|
||||
const zielPfad = path.join(destDir, ...relPfad.split('/'));
|
||||
if (!zielPfad.startsWith(destDir + path.sep)) continue; // Pfad-Ausbruch verhindern
|
||||
fs.mkdirSync(path.dirname(zielPfad), { recursive: true });
|
||||
fs.writeFileSync(zielPfad, forge.readZipEntry(buf, idx[name]));
|
||||
}
|
||||
}
|
||||
|
||||
// Solder-Basis-URLs enden mal mit "/api/", mal ohne abschließenden Slash
|
||||
function solderUrl(basis, pfad) {
|
||||
const b = String(basis || '').replace(/\/+$/, '');
|
||||
return `${b}/${pfad}`;
|
||||
}
|
||||
|
||||
async function installSolderBuild(solderBasis, slug, gameDir, onProgress) {
|
||||
const info = await getJson(solderUrl(solderBasis, 'modpack/' + encodeURIComponent(slug)));
|
||||
const build = info.recommended || info.latest || (info.builds || [])[0];
|
||||
if (!build) throw new Error('Keine Fassung (Build) für dieses Modpack gefunden.');
|
||||
const daten = await getJson(solderUrl(solderBasis, `modpack/${encodeURIComponent(slug)}/${encodeURIComponent(build)}`));
|
||||
const mods = Array.isArray(daten.mods) ? daten.mods : [];
|
||||
|
||||
for (let i = 0; i < mods.length; i++) {
|
||||
const mod = mods[i];
|
||||
if (onProgress) onProgress({ current: i + 1, total: mods.length, name: mod.name || '' });
|
||||
if (!mod || !mod.url) continue;
|
||||
try {
|
||||
const buf = await getBuffer(mod.url);
|
||||
extractZipBuffer(buf, gameDir);
|
||||
} catch { /* einzelner Mod fehlgeschlagen – Rest trotzdem weiter versuchen */ }
|
||||
}
|
||||
|
||||
let loader = 'vanilla';
|
||||
const loaderVersion = daten.forge || '';
|
||||
if (loaderVersion) loader = 'forge';
|
||||
return { version: daten.minecraft || '', loader, loaderVersion };
|
||||
}
|
||||
|
||||
async function installSingleZip(url, gameDir) {
|
||||
const buf = await getBuffer(url);
|
||||
// in einen Temp-Ordner entpacken, damit wir wie beim Ordner-Import
|
||||
// (technicimport.js) Version/Loader aus bin/modpack.jar erkennen können,
|
||||
// bevor die Dateien an ihren endgültigen Platz kommen
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'technic-dl-'));
|
||||
try {
|
||||
extractZipBuffer(buf, tmp);
|
||||
let base = tmp;
|
||||
if (!fs.existsSync(path.join(base, 'bin')) && !fs.existsSync(path.join(base, 'mods'))) {
|
||||
// manche Packs haben einen umschließenden Ordner im ZIP
|
||||
const sub = fs.readdirSync(tmp).map((n) => path.join(tmp, n))
|
||||
.find((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } });
|
||||
if (sub) base = sub;
|
||||
}
|
||||
const { version, loader, loaderVersion } = technicimport.detectVersion(base);
|
||||
fs.cpSync(base, gameDir, { recursive: true });
|
||||
return { version, loader, loaderVersion };
|
||||
} finally {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Lädt ein Modpack von der Technic-Plattform und legt daraus eine neue
|
||||
* Instanz an. onProgress(info) wird bei Solder-Packs pro Mod aufgerufen.
|
||||
* -> { ok, instance } oder { ok: false, message }
|
||||
*/
|
||||
async function installFromPlatform(store, slug, onProgress) {
|
||||
let created = null;
|
||||
try {
|
||||
const detail = await getJson(`${API_BASE}modpack/${encodeURIComponent(slug)}?build=${API_BUILD}`);
|
||||
// die Technic-API antwortet bei unbekanntem Slug mit HTTP 200 und
|
||||
// {"error": "..."} statt einem Fehlerstatus – daher explizit prüfen
|
||||
if (detail.error) throw new Error(detail.error);
|
||||
|
||||
created = store.createOrReplaceImportedInstance({
|
||||
name: detail.displayName || detail.name || slug,
|
||||
notes: detail.description || '',
|
||||
});
|
||||
const gameDir = store.gameDir(created.id);
|
||||
fs.mkdirSync(gameDir, { recursive: true });
|
||||
|
||||
let ergebnis;
|
||||
if (detail.solder) {
|
||||
ergebnis = await installSolderBuild(detail.solder, slug, gameDir, onProgress);
|
||||
} else if (detail.url) {
|
||||
ergebnis = await installSingleZip(detail.url, gameDir);
|
||||
} else {
|
||||
throw new Error('Für dieses Modpack ist kein Download hinterlegt.');
|
||||
}
|
||||
store.updateInstance(created.id, {
|
||||
minecraft: {
|
||||
version: ergebnis.version || detail.minecraft || '',
|
||||
loader: ergebnis.loader || 'vanilla',
|
||||
loaderVersion: ergebnis.loaderVersion || '',
|
||||
},
|
||||
});
|
||||
return { ok: true, instance: store.getInstance(created.id) };
|
||||
} catch (err) {
|
||||
// fehlgeschlagene Installation nicht als leere Karteileiche stehen lassen
|
||||
if (created) { try { store.deleteInstance(created.id); } catch { /* egal */ } }
|
||||
return { ok: false, message: 'Installation fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { search, installFromPlatform };
|
||||
Reference in New Issue
Block a user