This commit is contained in:
Linrador 2025-08-13 23:43:28 +02:00
parent db3497138b
commit ae11e8e17d
7 changed files with 153 additions and 146 deletions

View File

@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadDemoFile = downloadDemoFile; exports.downloadDemoFile = downloadDemoFile;
// src/app/downloadDemoFile.ts
const fs_1 = __importDefault(require("fs")); const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path")); const path_1 = __importDefault(require("path"));
const https_1 = __importDefault(require("https")); const https_1 = __importDefault(require("https"));
@ -13,8 +12,9 @@ const stream_1 = require("stream");
const util_1 = require("util"); const util_1 = require("util");
const unbzip2_stream_1 = __importDefault(require("unbzip2-stream")); const unbzip2_stream_1 = __importDefault(require("unbzip2-stream"));
const pipe = (0, util_1.promisify)(stream_1.pipeline); 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) { async function extractBz2Safe(srcPath, destPath) {
try { try {
await pipe(fs_1.default.createReadStream(srcPath), (0, unbzip2_stream_1.default)(), fs_1.default.createWriteStream(destPath)); 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'); 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) { function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let attempt = 0; let attempt = 0;
@ -86,18 +89,21 @@ function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3
tryDownload(); 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() === '') { if (!outputBaseDir || outputBaseDir.trim() === '') {
outputBaseDir = 'demos'; outputBaseDir = 'demos';
} }
const appId = 730; const appId = 730;
const matchId = match.matchid; const matchId = match.matchid;
const timestamp = match.matchtime; 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 matchDate = new Date(timestamp * 1000).toISOString().split('T')[0];
const lastRound = match.roundstatsall?.at(-1); 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 || const mapName = lastRound?.reservation?.map ||
lastRound?.mapname || lastRound?.mapname ||
match.watchablematchinfo?.game_map || 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 finalDir = path_1.default.join(outputBaseDir, matchDate);
const finalFile = path_1.default.join(finalDir, `${baseName}.dem`); const finalFile = path_1.default.join(finalDir, `${baseName}.dem`);
const finalFileName = path_1.default.basename(finalFile); const finalFileName = path_1.default.basename(finalFile);
// 1) Bereits vorhanden? -> sofort zurück fs_1.default.mkdirSync(tempDir, { recursive: true });
if (fs_1.default.existsSync(finalFile)) { fs_1.default.mkdirSync(finalDir, { recursive: true });
console.log(`♻️ Demo existiert bereits: ${finalFileName}`); console.log(`📥 Lade Demo von ${demoUrl}...`);
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 { 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 { catch (err) {
inflight.delete(finalFile); 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;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
dist/main.js vendored
View File

@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
// src/app/main.ts
const yargs_1 = __importDefault(require("yargs")); const yargs_1 = __importDefault(require("yargs"));
const helpers_1 = require("yargs/helpers"); const helpers_1 = require("yargs/helpers");
const steamSession_1 = require("./app/steamSession"); const steamSession_1 = require("./app/steamSession");
@ -44,7 +43,7 @@ async function start() {
const { shareCode, steamId } = JSON.parse(body); const { shareCode, steamId } = JSON.parse(body);
console.log(`📦 ShareCode empfangen: ${shareCode}`); console.log(`📦 ShareCode empfangen: ${shareCode}`);
const match = await (0, fetchMatchFromSharecode_1.fetchMatchFromShareCode)(shareCode, session); 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`); process.stdout.write(`📶 Fortschritt: ${percent}%\r`);
if (percent === 100) { if (percent === 100) {
console.log('✅ Download abgeschlossen'); console.log('✅ Download abgeschlossen');
@ -58,7 +57,7 @@ async function start() {
console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`);
// Antwort an den Client // Antwort an den Client
res.writeHead(200, { 'Content-Type': 'application/json' }); 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) { catch (err) {
console.error('❌ Fehler:', err); console.error('❌ Fehler:', err);

View File

@ -1,4 +1,3 @@
// src/app/downloadDemoFile.ts
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import https from 'https'; import https from 'https';
@ -9,18 +8,25 @@ import bz2 from 'unbzip2-stream';
const pipe = promisify(pipeline); const pipe = promisify(pipeline);
// Kleiner In-Flight-Lock, um Doppel-Downloads bei gleichzeitigen Requests zu vermeiden /**
const inflight = new Map<string, Promise<{ path: string; existed: boolean }>>(); * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem
*/
async function extractBz2Safe(srcPath: string, destPath: string) { async function extractBz2Safe(srcPath: string, destPath: string) {
try { try {
await pipe(fs.createReadStream(srcPath), bz2(), fs.createWriteStream(destPath)); await pipe(
fs.createReadStream(srcPath),
bz2(),
fs.createWriteStream(destPath)
);
} catch (err) { } catch (err) {
console.log('❌ Fehler beim Entpacken (pipe):', err); console.log('❌ Fehler beim Entpacken (pipe):', err);
throw new Error('Entpackung fehlgeschlagen'); throw new Error('Entpackung fehlgeschlagen');
} }
} }
/**
* Lädt eine Datei per HTTPS, speichert sie unter `dest`, zeigt optional Fortschritt.
*/
function downloadWithHttps( function downloadWithHttps(
url: string, url: string,
dest: string, dest: string,
@ -30,6 +36,7 @@ function downloadWithHttps(
): Promise<boolean> { ): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let attempt = 0; let attempt = 0;
const tryDownload = () => { const tryDownload = () => {
const file = fs.createWriteStream(dest); const file = fs.createWriteStream(dest);
const client = url.startsWith('https') ? https : http; 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( export async function downloadDemoFile(
match: any, match: any,
steamId: string,
outputBaseDir = 'demos', outputBaseDir = 'demos',
onProgress?: (percent: number) => void onProgress?: (percent: number) => void
): Promise<{ path: string; existed: boolean; matchId: string; map?: string }> { ): Promise<string> {
if (!outputBaseDir || outputBaseDir.trim() === '') outputBaseDir = 'demos' if (!outputBaseDir || outputBaseDir.trim() === '') {
outputBaseDir = 'demos';
}
const appId = 730 const appId = 730;
const matchId: string = String(match.matchid) const matchId = match.matchid;
const timestamp = match.matchtime const timestamp = match.matchtime;
const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0] const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0];
const lastRound = match.roundstatsall?.at(-1) const lastRound = match.roundstatsall?.at(-1);
const demoUrl =
typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')
? lastRound.map
: undefined;
// echte Demo-URL const mapName =
const demoUrl = (typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')) lastRound?.reservation?.map ||
? lastRound.map lastRound?.mapname ||
: undefined match.watchablematchinfo?.game_map ||
if (!demoUrl) throw new Error('❌ Keine Demo-URL im Match vorhanden') 'unknownmap';
// Mapnamen robust extrahieren if (!demoUrl) {
const rawMap = throw new Error('❌ Keine Demo-URL im Match vorhanden');
lastRound?.reservation?.map ?? }
lastRound?.reservation?.mapname ??
lastRound?.mapname ??
match.watchablematchinfo?.game_map ??
''
const mapName = (String(rawMap).trim() || 'unknownmap') const isPremier = !!lastRound?.b_switched_teams;
.replace(/^maps[\\/]/i, '') // "maps/de_inferno" -> "de_inferno" const matchType = isPremier ? 'premier' : 'competitive';
.split(/[\\/]/).pop() || 'unknownmap'
const isPremier = !!lastRound?.b_switched_teams const tempDir = path.join(outputBaseDir, 'temp');
const matchType = isPremier ? 'premier' : 'competitive' const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`;
const baseName = path.parse(tempFileName).name;
const tempFile = path.join(tempDir, tempFileName);
const tempDir = path.join(outputBaseDir, 'temp') const finalDir = path.join(outputBaseDir, matchDate);
// Dateiname darf sich ändern -> Consumer soll nicht auf Map im Namen bauen const finalFile = path.join(finalDir, `${baseName}.dem`);
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) const finalFileName = path.basename(finalFile)
// schon vorhanden? fs.mkdirSync(tempDir, { recursive: true });
try { fs.mkdirSync(finalDir, { recursive: true });
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)) { console.log(`📥 Lade Demo von ${demoUrl}...`);
return inflight.get(finalFile)! as any
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}`);
} }
const job = (async () => {
fs.mkdirSync(tempDir, { recursive: true })
fs.mkdirSync(finalDir, { recursive: true })
console.log(`📥 Lade Demo von ${demoUrl}...`) console.log(`✅ Gespeichert als ${tempFileName}`);
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}...` const entpackZeile = `🗜️ Entpacke ${finalFileName}...`;
process.stdout.write(entpackInfo) process.stdout.write(entpackZeile);
await extractBz2Safe(tempFile, finalFile)
if (!fs.existsSync(finalFile)) { await extractBz2Safe(tempFile, 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 {} 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);
inflight.set(finalFile, job) if (!fs.existsSync(finalFile)) {
try { const paddedFail = failMessage.padEnd(maxLength, ' ');
return await job process.stdout.write(`\r${paddedFail}\n`);
} finally { throw new Error(failMessage);
inflight.delete(finalFile)
} }
const paddedSuccess = successMessage.padEnd(maxLength, ' ');
process.stdout.write(`\r${paddedSuccess}\n`);
// Aufräumen
try {
fs.unlinkSync(tempFile);
console.log(`🧹 Gelöscht: ${tempFileName}`);
} catch {
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`);
}
return finalFile;
} }

View File

@ -1,4 +1,3 @@
// src/app/main.ts
import yargs from 'yargs'; import yargs from 'yargs';
import { hideBin } from 'yargs/helpers'; import { hideBin } from 'yargs/helpers';
import { createSteamSession } from './app/steamSession'; import { createSteamSession } from './app/steamSession';
@ -46,9 +45,8 @@ async function start() {
console.log(`📦 ShareCode empfangen: ${shareCode}`); console.log(`📦 ShareCode empfangen: ${shareCode}`);
const match = await fetchMatchFromShareCode(shareCode, session); const match = await fetchMatchFromShareCode(shareCode, session);
const { path: demoFilePath, existed } = await downloadDemoFile( const demoFilePath = await downloadDemoFile(
match, match,
steamId,
resolvedDemoPath, resolvedDemoPath,
(percent: number) => { (percent: number) => {
process.stdout.write(`📶 Fortschritt: ${percent}%\r`); process.stdout.write(`📶 Fortschritt: ${percent}%\r`);
@ -68,7 +66,7 @@ async function start() {
// Antwort an den Client // Antwort an den Client
res.writeHead(200, { 'Content-Type': 'application/json' }); 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) { } catch (err) {
console.error('❌ Fehler:', err); console.error('❌ Fehler:', err);
res.writeHead(500, { 'Content-Type': 'application/json' }); res.writeHead(500, { 'Content-Type': 'application/json' });