168 lines
7.1 KiB
JavaScript
168 lines
7.1 KiB
JavaScript
"use strict";
|
||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
};
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.downloadDemoFile = downloadDemoFile;
|
||
const fs_1 = __importDefault(require("fs"));
|
||
const path_1 = __importDefault(require("path"));
|
||
const https_1 = __importDefault(require("https"));
|
||
const http_1 = __importDefault(require("http"));
|
||
const stream_1 = require("stream");
|
||
const util_1 = require("util");
|
||
const unbzip2_stream_1 = __importDefault(require("unbzip2-stream"));
|
||
const pipe = (0, util_1.promisify)(stream_1.pipeline);
|
||
/**
|
||
* Entpackt eine .bz2-Datei mithilfe von Streams nach .dem
|
||
*/
|
||
async function extractBz2Safe(srcPath, destPath) {
|
||
try {
|
||
await pipe(fs_1.default.createReadStream(srcPath), (0, unbzip2_stream_1.default)(), fs_1.default.createWriteStream(destPath));
|
||
}
|
||
catch (err) {
|
||
console.log('❌ Fehler beim Entpacken (pipe):', err);
|
||
throw new Error('Entpackung fehlgeschlagen');
|
||
}
|
||
}
|
||
/**
|
||
* Lädt eine Datei per HTTPS, speichert sie unter `dest`, zeigt optional Fortschritt.
|
||
*/
|
||
function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3000) {
|
||
return new Promise((resolve, reject) => {
|
||
let attempt = 0;
|
||
const tryDownload = () => {
|
||
const file = fs_1.default.createWriteStream(dest);
|
||
const client = url.startsWith('https') ? https_1.default : http_1.default;
|
||
let downloaded = 0;
|
||
let total = 0;
|
||
let lastPercent = -1;
|
||
const request = client.get(url, (res) => {
|
||
if (res.statusCode !== 200) {
|
||
res.resume();
|
||
file.close();
|
||
file.destroy();
|
||
if ([502, 503, 504].includes(res.statusCode) && attempt < maxRetries) {
|
||
process.stdout.write(`🔁 Retry ${attempt + 1}/${maxRetries} – HTTP ${res.statusCode}\r`);
|
||
attempt++;
|
||
setTimeout(tryDownload, retryDelay);
|
||
return;
|
||
}
|
||
if (attempt >= maxRetries) {
|
||
console.log(`❌ Max. Versuche erreicht (${maxRetries}), Datei wird übersprungen (HTTP ${res.statusCode})`);
|
||
return resolve(false);
|
||
}
|
||
return reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
total = parseInt(res.headers['content-length'] || '0', 10);
|
||
res.on('data', (chunk) => {
|
||
downloaded += chunk.length;
|
||
if (onProgress && total) {
|
||
const percent = Math.floor((downloaded / total) * 100);
|
||
if (percent !== lastPercent) {
|
||
lastPercent = percent;
|
||
onProgress(percent);
|
||
}
|
||
}
|
||
});
|
||
res.pipe(file);
|
||
file.on('finish', () => {
|
||
file.close((err) => {
|
||
if (err)
|
||
return reject(err);
|
||
resolve(true);
|
||
});
|
||
});
|
||
res.on('error', reject);
|
||
file.on('error', reject);
|
||
});
|
||
request.on('error', (err) => {
|
||
if (attempt < maxRetries) {
|
||
console.log(`🔁 Retry ${attempt + 1}/${maxRetries} wegen Verbindungsfehler: ${err.message}`);
|
||
attempt++;
|
||
setTimeout(tryDownload, retryDelay);
|
||
}
|
||
else {
|
||
reject(err);
|
||
}
|
||
});
|
||
};
|
||
tryDownload();
|
||
});
|
||
}
|
||
/**
|
||
* Hauptfunktion: lädt und entpackt eine CS2-Demo (.bz2), mit Fortschrittsanzeige.
|
||
*/
|
||
async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgress) {
|
||
if (!outputBaseDir || outputBaseDir.trim() === '') {
|
||
outputBaseDir = 'demos';
|
||
}
|
||
const appId = 730;
|
||
const matchId = match.matchid;
|
||
const timestamp = match.matchtime;
|
||
const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0];
|
||
const lastRound = match.roundstatsall?.at(-1);
|
||
const demoUrl = typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')
|
||
? lastRound.map
|
||
: undefined;
|
||
const mapName = lastRound?.reservation?.map ||
|
||
lastRound?.mapname ||
|
||
match.watchablematchinfo?.game_map ||
|
||
'unknownmap';
|
||
if (!demoUrl) {
|
||
throw new Error('❌ Keine Demo-URL im Match vorhanden');
|
||
}
|
||
const isPremier = !!lastRound?.b_switched_teams;
|
||
const matchType = isPremier ? 'premier' : 'competitive';
|
||
const tempDir = path_1.default.join(outputBaseDir, 'temp');
|
||
const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`;
|
||
const baseName = path_1.default.parse(tempFileName).name;
|
||
const tempFile = path_1.default.join(tempDir, tempFileName);
|
||
const finalDir = path_1.default.join(outputBaseDir, steamId, matchDate);
|
||
const finalFile = path_1.default.join(finalDir, `${baseName}.dem`);
|
||
const finalFileName = path_1.default.basename(finalFile);
|
||
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
||
fs_1.default.mkdirSync(finalDir, { recursive: true });
|
||
console.log(`📥 Lade Demo von ${demoUrl}...`);
|
||
try {
|
||
const success = await downloadWithHttps(demoUrl, tempFile, onProgress);
|
||
if (!success || !fs_1.default.existsSync(tempFile) || fs_1.default.statSync(tempFile).size === 0) {
|
||
console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`);
|
||
try {
|
||
if (fs_1.default.existsSync(tempFile))
|
||
fs_1.default.unlinkSync(tempFile);
|
||
}
|
||
catch {
|
||
console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`);
|
||
}
|
||
return '';
|
||
}
|
||
}
|
||
catch (err) {
|
||
throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`);
|
||
}
|
||
console.log(`✅ Gespeichert als ${tempFileName}`);
|
||
const entpackZeile = `🗜️ Entpacke ${finalFileName}...`;
|
||
process.stdout.write(entpackZeile);
|
||
await extractBz2Safe(tempFile, finalFile);
|
||
const successMessage = `✅ Entpackt: ${finalFileName}`;
|
||
const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`;
|
||
// Max-Zeichenlänge bestimmen
|
||
const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length);
|
||
if (!fs_1.default.existsSync(finalFile)) {
|
||
const paddedFail = failMessage.padEnd(maxLength, ' ');
|
||
process.stdout.write(`\r${paddedFail}\n`);
|
||
throw new Error(failMessage);
|
||
}
|
||
const paddedSuccess = successMessage.padEnd(maxLength, ' ');
|
||
process.stdout.write(`\r${paddedSuccess}\n`);
|
||
// Aufräumen
|
||
try {
|
||
fs_1.default.unlinkSync(tempFile);
|
||
console.log(`🧹 Gelöscht: ${tempFileName}`);
|
||
}
|
||
catch {
|
||
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`);
|
||
}
|
||
return finalFile;
|
||
}
|