"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; // src/app/downloadDemoFile.ts 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); // Kleiner In-Flight-Lock, um Doppel-Downloads bei gleichzeitigen Requests zu vermeiden const inflight = new Map(); 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'); } } 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(); }); } 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; // Wenn du die Berliner TZ möchtest: // const matchDate = new Date(timestamp * 1000).toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' }); 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, matchDate); const finalFile = path_1.default.join(finalDir, `${baseName}.dem`); const finalFileName = path_1.default.basename(finalFile); // 1) Bereits vorhanden? -> sofort zurück if (fs_1.default.existsSync(finalFile)) { console.log(`♻️ Demo existiert bereits: ${finalFileName}`); return { path: finalFile, existed: true }; } // 2) In-Flight-Lock prüfen if (inflight.has(finalFile)) { return inflight.get(finalFile); } // 3) Download-/Entpack-Job definieren und in Map eintragen const job = (async () => { 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) { try { if (fs_1.default.existsSync(tempFile)) fs_1.default.unlinkSync(tempFile); } catch { } throw new Error('Download fehlgeschlagen oder Datei leer'); } } catch (err) { throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : String(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`; 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 { path: finalFile, existed: false }; })(); inflight.set(finalFile, job); try { return await job; } finally { inflight.delete(finalFile); } }