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 });
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;
}

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 };
};
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);

View File

@ -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<string, Promise<{ path: string; existed: boolean }>>();
/**
* 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<boolean> {
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<string> {
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;
}

View File

@ -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' });