diff --git a/dist/app/downloadDemoFile.js b/dist/app/downloadDemoFile.js index 8bef29b..fcea04c 100644 --- a/dist/app/downloadDemoFile.js +++ b/dist/app/downloadDemoFile.js @@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (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")); @@ -13,8 +12,9 @@ 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(); +/** + * 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)); @@ -24,6 +24,9 @@ async function extractBz2Safe(srcPath, destPath) { 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; @@ -86,18 +89,21 @@ function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3 tryDownload(); }); } -async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgress) { +/** + * 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; - // 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 demoUrl = typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2') + ? lastRound.map + : undefined; const mapName = lastRound?.reservation?.map || lastRound?.mapname || match.watchablematchinfo?.game_map || @@ -114,63 +120,48 @@ async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgr 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); + fs_1.default.mkdirSync(tempDir, { recursive: true }); + fs_1.default.mkdirSync(finalDir, { recursive: true }); + console.log(`📥 Lade Demo von ${demoUrl}...`); try { - return await job; + 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 ''; + } } - finally { - inflight.delete(finalFile); + 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; } diff --git a/dist/cs2-demo-downloader-linux b/dist/cs2-demo-downloader-linux index 12b82d1..168d093 100644 Binary files a/dist/cs2-demo-downloader-linux and b/dist/cs2-demo-downloader-linux differ diff --git a/dist/cs2-demo-downloader-macos b/dist/cs2-demo-downloader-macos index 8821d1c..5a13e32 100644 Binary files a/dist/cs2-demo-downloader-macos and b/dist/cs2-demo-downloader-macos differ diff --git a/dist/cs2-demo-downloader-win.exe b/dist/cs2-demo-downloader-win.exe index 7651dd8..2d79045 100644 Binary files a/dist/cs2-demo-downloader-win.exe and b/dist/cs2-demo-downloader-win.exe differ diff --git a/dist/main.js b/dist/main.js index ba2a64d..2ac647c 100644 --- a/dist/main.js +++ b/dist/main.js @@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -// src/app/main.ts const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const steamSession_1 = require("./app/steamSession"); @@ -44,7 +43,7 @@ async function start() { const { shareCode, steamId } = JSON.parse(body); console.log(`📦 ShareCode empfangen: ${shareCode}`); const match = await (0, fetchMatchFromSharecode_1.fetchMatchFromShareCode)(shareCode, session); - const { path: demoFilePath, existed } = await (0, downloadDemoFile_1.downloadDemoFile)(match, steamId, resolvedDemoPath, (percent) => { + const demoFilePath = await (0, downloadDemoFile_1.downloadDemoFile)(match, resolvedDemoPath, (percent) => { process.stdout.write(`📶 Fortschritt: ${percent}%\r`); if (percent === 100) { console.log('✅ Download abgeschlossen'); @@ -58,7 +57,7 @@ async function start() { console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); // Antwort an den Client res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, path: demoFilePath, existed })); + res.end(JSON.stringify({ success: true, path: demoFilePath })); } catch (err) { console.error('❌ Fehler:', err); diff --git a/src/app/downloadDemoFile.ts b/src/app/downloadDemoFile.ts index 245b52f..6ea8f08 100644 --- a/src/app/downloadDemoFile.ts +++ b/src/app/downloadDemoFile.ts @@ -1,4 +1,3 @@ -// src/app/downloadDemoFile.ts import fs from 'fs'; import path from 'path'; import https from 'https'; @@ -9,18 +8,25 @@ import bz2 from 'unbzip2-stream'; const pipe = promisify(pipeline); -// Kleiner In-Flight-Lock, um Doppel-Downloads bei gleichzeitigen Requests zu vermeiden -const inflight = new Map>(); - +/** + * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem + */ async function extractBz2Safe(srcPath: string, destPath: string) { try { - await pipe(fs.createReadStream(srcPath), bz2(), fs.createWriteStream(destPath)); + await pipe( + fs.createReadStream(srcPath), + bz2(), + fs.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: string, dest: string, @@ -30,6 +36,7 @@ function downloadWithHttps( ): Promise { return new Promise((resolve, reject) => { let attempt = 0; + const tryDownload = () => { const file = fs.createWriteStream(dest); const client = url.startsWith('https') ? https : http; @@ -100,93 +107,105 @@ function downloadWithHttps( }); } + + +/** + * Hauptfunktion: lädt und entpackt eine CS2-Demo (.bz2), mit Fortschrittsanzeige. + */ export async function downloadDemoFile( match: any, - steamId: string, outputBaseDir = 'demos', onProgress?: (percent: number) => void -): Promise<{ path: string; existed: boolean; matchId: string; map?: string }> { - if (!outputBaseDir || outputBaseDir.trim() === '') outputBaseDir = 'demos' +): Promise { + 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 appId = 730 - const matchId: string = String(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 lastRound = match.roundstatsall?.at(-1) + const mapName = + lastRound?.reservation?.map || + lastRound?.mapname || + match.watchablematchinfo?.game_map || + 'unknownmap'; - // echte Demo-URL - const demoUrl = (typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')) - ? lastRound.map - : undefined - if (!demoUrl) throw new Error('❌ Keine Demo-URL im Match vorhanden') + if (!demoUrl) { + throw new Error('❌ Keine Demo-URL im Match vorhanden'); + } - // Mapnamen robust extrahieren - const rawMap = - lastRound?.reservation?.map ?? - lastRound?.reservation?.mapname ?? - lastRound?.mapname ?? - match.watchablematchinfo?.game_map ?? - '' + const isPremier = !!lastRound?.b_switched_teams; + const matchType = isPremier ? 'premier' : 'competitive'; - const mapName = (String(rawMap).trim() || 'unknownmap') - .replace(/^maps[\\/]/i, '') // "maps/de_inferno" -> "de_inferno" - .split(/[\\/]/).pop() || 'unknownmap' + const tempDir = path.join(outputBaseDir, 'temp'); + const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; + const baseName = path.parse(tempFileName).name; + const tempFile = path.join(tempDir, tempFileName); - const isPremier = !!lastRound?.b_switched_teams - const matchType = isPremier ? 'premier' : 'competitive' - - const tempDir = path.join(outputBaseDir, 'temp') - // Dateiname darf sich ändern -> Consumer soll nicht auf Map im Namen bauen - const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2` - const baseName = path.parse(tempFileName).name - const tempFile = path.join(tempDir, tempFileName) - - const finalDir = path.join(outputBaseDir, matchDate) - const finalFile = path.join(finalDir, `${baseName}.dem`) + const finalDir = path.join(outputBaseDir, matchDate); + const finalFile = path.join(finalDir, `${baseName}.dem`); const finalFileName = path.basename(finalFile) - // schon vorhanden? - try { - fs.accessSync(finalFile) - console.log(`♻️ Demo existiert bereits: ${finalFileName}`) - return { path: path.normalize(finalFile), existed: true, matchId, map: mapName } - } catch {/* not exists */} + fs.mkdirSync(tempDir, { recursive: true }); + fs.mkdirSync(finalDir, { recursive: true }); - if (inflight.has(finalFile)) { - return inflight.get(finalFile)! as any + console.log(`📥 Lade Demo von ${demoUrl}...`); + + try { + const success = await downloadWithHttps(demoUrl, tempFile, onProgress); + if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { + console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`); + + try { + if (fs.existsSync(tempFile)) fs.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.existsSync(finalFile)) { + const paddedFail = failMessage.padEnd(maxLength, ' '); + process.stdout.write(`\r${paddedFail}\n`); + throw new Error(failMessage); } - const job = (async () => { - fs.mkdirSync(tempDir, { recursive: true }) - fs.mkdirSync(finalDir, { recursive: true }) + const paddedSuccess = successMessage.padEnd(maxLength, ' '); + process.stdout.write(`\r${paddedSuccess}\n`); - console.log(`📥 Lade Demo von ${demoUrl}...`) - const ok = await downloadWithHttps(demoUrl, tempFile, onProgress) - if (!ok || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { - try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile) } catch {} - throw new Error('Download fehlgeschlagen oder Datei leer') - } - - const entpackInfo = `🗜️ Entpacke ${finalFileName}...` - process.stdout.write(entpackInfo) - await extractBz2Safe(tempFile, finalFile) - - if (!fs.existsSync(finalFile)) { - process.stdout.write(`\r❌ Entpackung fehlgeschlagen – Datei nicht vorhanden\n`) - throw new Error('Entpackung fehlgeschlagen') - } - process.stdout.write(`\r✅ Entpackt: ${finalFileName}\n`) - - try { fs.unlinkSync(tempFile) } catch {} - - return { path: path.normalize(finalFile), existed: false, matchId, map: mapName } - })() - - inflight.set(finalFile, job) + // Aufräumen try { - return await job - } finally { - inflight.delete(finalFile) + fs.unlinkSync(tempFile); + console.log(`🧹 Gelöscht: ${tempFileName}`); + } catch { + console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); } + + return finalFile; } diff --git a/src/main.ts b/src/main.ts index 276123d..9dcd304 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -// src/app/main.ts import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { createSteamSession } from './app/steamSession'; @@ -46,9 +45,8 @@ async function start() { console.log(`📦 ShareCode empfangen: ${shareCode}`); const match = await fetchMatchFromShareCode(shareCode, session); - const { path: demoFilePath, existed } = await downloadDemoFile( + const demoFilePath = await downloadDemoFile( match, - steamId, resolvedDemoPath, (percent: number) => { process.stdout.write(`📶 Fortschritt: ${percent}%\r`); @@ -68,7 +66,7 @@ async function start() { // Antwort an den Client res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, path: demoFilePath, existed })); + res.end(JSON.stringify({ success: true, path: demoFilePath })); } catch (err) { console.error('❌ Fehler:', err); res.writeHead(500, { 'Content-Type': 'application/json' });