ironie-cs2-demo-downloader/dist/app/downloadDemoFile.js
2025-08-14 15:06:59 +02:00

215 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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);
// In-Flight-Lock pro Ziel-.dem
const inflight = new Map();
/**
* 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, 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 finalDir = path_1.default.join(outputBaseDir, matchDate);
// Stabiler finaler Basisname (wichtig fürs De-Duplizieren)
const baseFinalName = `match${appId}_${mapName}_${matchId}_${matchType}`;
const finalFile = path_1.default.join(finalDir, `${baseFinalName}.dem`);
const finalFileName = path_1.default.basename(finalFile);
const partialFile = `${finalFile}.part`;
// Bereits vorhanden? -> sofort zurück
if (fs_1.default.existsSync(finalFile)) {
console.log(`♻️ Demo existiert bereits: ${finalFileName}`);
return finalFile;
}
// Parallele Requests für dieselbe Zieldatei zusammenlegen
if (inflight.has(finalFile)) {
return await inflight.get(finalFile);
}
// Eindeutiger Temp-Dateiname im temp/
const rand = Math.random().toString(36).slice(2, 8);
const tempFileName = `${baseFinalName}_${rand}.bz2`;
const tempFile = path_1.default.join(tempDir, tempFileName);
const job = (async () => {
fs_1.default.mkdirSync(tempDir, { recursive: true });
fs_1.default.mkdirSync(finalDir, { recursive: true });
// Stale .part von früheren Abbrüchen entfernen
try {
if (fs_1.default.existsSync(partialFile))
fs_1.default.unlinkSync(partialFile);
}
catch { }
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 { }
return '';
}
}
catch (err) {
throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`);
}
console.log(`✅ Gespeichert als ${tempFileName}`);
const entpackZeile = `🗜️ Entpacke nach ${path_1.default.basename(partialFile)}...`;
process.stdout.write(entpackZeile);
try {
await extractBz2Safe(tempFile, partialFile);
}
catch (e) {
try {
if (fs_1.default.existsSync(partialFile))
fs_1.default.unlinkSync(partialFile);
}
catch { }
try {
if (fs_1.default.existsSync(tempFile))
fs_1.default.unlinkSync(tempFile);
}
catch { }
throw e;
}
// Prüfen & atomar nach .dem verschieben
const successMessage = `✅ Entpackt: ${finalFileName}`;
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`;
const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length);
if (!fs_1.default.existsSync(partialFile)) {
process.stdout.write(`\r${failMessage.padEnd(maxLength, ' ')}\n`);
try {
if (fs_1.default.existsSync(tempFile))
fs_1.default.unlinkSync(tempFile);
}
catch { }
throw new Error('Entpackung fehlgeschlagen');
}
fs_1.default.renameSync(partialFile, finalFile);
process.stdout.write(`\r${successMessage.padEnd(maxLength, ' ')}\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;
})();
inflight.set(finalFile, job);
try {
return await job;
}
finally {
inflight.delete(finalFile);
}
}