diff --git a/dist/app/downloadDemoFile.js b/dist/app/downloadDemoFile.js index 0ed43f5..8bef29b 100644 --- a/dist/app/downloadDemoFile.js +++ b/dist/app/downloadDemoFile.js @@ -4,6 +4,7 @@ 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")); @@ -12,9 +13,8 @@ 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 - */ +// 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)); @@ -24,9 +24,6 @@ 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; @@ -89,9 +86,6 @@ function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3 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'; @@ -99,11 +93,11 @@ async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgr 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 || @@ -117,51 +111,66 @@ async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgr 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 finalDir = path_1.default.join(outputBaseDir, 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); + // 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 { - 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 + 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 { - fs_1.default.unlinkSync(tempFile); - console.log(`🧹 Gelöscht: ${tempFileName}`); + return await job; } - catch { - console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + finally { + inflight.delete(finalFile); } - return finalFile; } diff --git a/dist/cs2-demo-downloader-linux b/dist/cs2-demo-downloader-linux index 22d5719..12b82d1 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 72ee39d..8821d1c 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 144a9f8..7651dd8 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 6915f10..ba2a64d 100644 --- a/dist/main.js +++ b/dist/main.js @@ -3,6 +3,7 @@ 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"); @@ -43,7 +44,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 demoFilePath = await (0, downloadDemoFile_1.downloadDemoFile)(match, steamId, resolvedDemoPath, (percent) => { + const { path: demoFilePath, existed } = await (0, downloadDemoFile_1.downloadDemoFile)(match, steamId, resolvedDemoPath, (percent) => { process.stdout.write(`📶 Fortschritt: ${percent}%\r`); if (percent === 100) { console.log('✅ Download abgeschlossen'); @@ -57,7 +58,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 })); + res.end(JSON.stringify({ success: true, path: demoFilePath, existed })); } catch (err) { console.error('❌ Fehler:', err); diff --git a/src/app/downloadDemoFile.ts b/src/app/downloadDemoFile.ts index b3e1380..245b52f 100644 --- a/src/app/downloadDemoFile.ts +++ b/src/app/downloadDemoFile.ts @@ -1,3 +1,4 @@ +// src/app/downloadDemoFile.ts import fs from 'fs'; import path from 'path'; import https from 'https'; @@ -8,25 +9,18 @@ import bz2 from 'unbzip2-stream'; const pipe = promisify(pipeline); -/** - * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem - */ +// Kleiner In-Flight-Lock, um Doppel-Downloads bei gleichzeitigen Requests zu vermeiden +const inflight = new Map>(); + 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, @@ -36,7 +30,6 @@ 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; @@ -107,106 +100,93 @@ 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 { - 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]; +): Promise<{ path: string; existed: boolean; matchId: string; map?: string }> { + if (!outputBaseDir || outputBaseDir.trim() === '') outputBaseDir = 'demos' - const lastRound = match.roundstatsall?.at(-1); - const demoUrl = - typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2') - ? lastRound.map - : undefined; + const appId = 730 + const matchId: string = String(match.matchid) + const timestamp = match.matchtime + const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0] - const mapName = - lastRound?.reservation?.map || - lastRound?.mapname || - match.watchablematchinfo?.game_map || - 'unknownmap'; + const lastRound = match.roundstatsall?.at(-1) - if (!demoUrl) { - throw new Error('❌ Keine Demo-URL im Match vorhanden'); - } + // 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') - const isPremier = !!lastRound?.b_switched_teams; - const matchType = isPremier ? 'premier' : 'competitive'; + // Mapnamen robust extrahieren + const rawMap = + lastRound?.reservation?.map ?? + lastRound?.reservation?.mapname ?? + lastRound?.mapname ?? + match.watchablematchinfo?.game_map ?? + '' - 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 mapName = (String(rawMap).trim() || 'unknownmap') + .replace(/^maps[\\/]/i, '') // "maps/de_inferno" -> "de_inferno" + .split(/[\\/]/).pop() || 'unknownmap' - const finalDir = path.join(outputBaseDir, steamId, matchDate); - const finalFile = path.join(finalDir, `${baseName}.dem`); + 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 finalFileName = path.basename(finalFile) - fs.mkdirSync(tempDir, { recursive: true }); - fs.mkdirSync(finalDir, { recursive: true }); - - console.log(`📥 Lade Demo von ${demoUrl}...`); - + // schon vorhanden? 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 ''; + fs.accessSync(finalFile) + console.log(`♻️ Demo existiert bereits: ${finalFileName}`) + return { path: path.normalize(finalFile), existed: true, matchId, map: mapName } + } catch {/* not exists */} + + if (inflight.has(finalFile)) { + return inflight.get(finalFile)! as any + } + + const job = (async () => { + fs.mkdirSync(tempDir, { recursive: true }) + fs.mkdirSync(finalDir, { recursive: true }) + + 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') } - } catch (err) { - throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); - } - - console.log(`✅ Gespeichert als ${tempFileName}`); + const entpackInfo = `🗜️ Entpacke ${finalFileName}...` + process.stdout.write(entpackInfo) + await extractBz2Safe(tempFile, finalFile) - const entpackZeile = `🗜️ Entpacke ${finalFileName}...`; - process.stdout.write(entpackZeile); + 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`) - await extractBz2Safe(tempFile, finalFile); + try { fs.unlinkSync(tempFile) } catch {} - const successMessage = `✅ Entpackt: ${finalFileName}`; - const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`; + return { path: path.normalize(finalFile), existed: false, matchId, map: mapName } + })() - // 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 paddedSuccess = successMessage.padEnd(maxLength, ' '); - process.stdout.write(`\r${paddedSuccess}\n`); - - // Aufräumen + inflight.set(finalFile, job) try { - fs.unlinkSync(tempFile); - console.log(`🧹 Gelöscht: ${tempFileName}`); - } catch { - console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + return await job + } finally { + inflight.delete(finalFile) } - - return finalFile; } diff --git a/src/main.ts b/src/main.ts index eda1b12..276123d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +// src/app/main.ts import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { createSteamSession } from './app/steamSession'; @@ -45,7 +46,7 @@ async function start() { console.log(`📦 ShareCode empfangen: ${shareCode}`); const match = await fetchMatchFromShareCode(shareCode, session); - const demoFilePath = await downloadDemoFile( + const { path: demoFilePath, existed } = await downloadDemoFile( match, steamId, resolvedDemoPath, @@ -67,7 +68,7 @@ async function start() { // Antwort an den Client res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, path: demoFilePath })); + res.end(JSON.stringify({ success: true, path: demoFilePath, existed })); } catch (err) { console.error('❌ Fehler:', err); res.writeHead(500, { 'Content-Type': 'application/json' });