This commit is contained in:
Linrador 2025-08-11 15:37:19 +02:00
parent 0712aea017
commit db3497138b
7 changed files with 139 additions and 148 deletions

View File

@ -4,6 +4,7 @@ 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"));
@ -12,9 +13,8 @@ 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
* Entpackt eine .bz2-Datei mithilfe von Streams nach .dem const inflight = new Map();
*/
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,9 +24,6 @@ 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;
@ -89,9 +86,6 @@ function downloadWithHttps(url, dest, onProgress, maxRetries = 3, retryDelay = 3
tryDownload(); tryDownload();
}); });
} }
/**
* Hauptfunktion: lädt und entpackt eine CS2-Demo (.bz2), mit Fortschrittsanzeige.
*/
async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgress) { async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgress) {
if (!outputBaseDir || outputBaseDir.trim() === '') { if (!outputBaseDir || outputBaseDir.trim() === '') {
outputBaseDir = 'demos'; outputBaseDir = 'demos';
@ -99,11 +93,11 @@ async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgr
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') const demoUrl = typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2') ? lastRound.map : undefined;
? 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 ||
@ -117,51 +111,66 @@ async function downloadDemoFile(match, steamId, outputBaseDir = 'demos', onProgr
const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`;
const baseName = path_1.default.parse(tempFileName).name; const baseName = path_1.default.parse(tempFileName).name;
const tempFile = path_1.default.join(tempDir, tempFileName); 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 finalFile = path_1.default.join(finalDir, `${baseName}.dem`);
const finalFileName = path_1.default.basename(finalFile); const finalFileName = path_1.default.basename(finalFile);
fs_1.default.mkdirSync(tempDir, { recursive: true }); // 1) Bereits vorhanden? -> sofort zurück
fs_1.default.mkdirSync(finalDir, { recursive: true }); if (fs_1.default.existsSync(finalFile)) {
console.log(`📥 Lade Demo von ${demoUrl}...`); console.log(`♻️ Demo existiert bereits: ${finalFileName}`);
try { return { path: finalFile, existed: true };
const success = await downloadWithHttps(demoUrl, tempFile, onProgress); }
if (!success || !fs_1.default.existsSync(tempFile) || fs_1.default.statSync(tempFile).size === 0) { // 2) In-Flight-Lock prüfen
console.warn(`⚠️ Download fehlgeschlagen oder Datei leer lösche ${tempFileName}`); if (inflight.has(finalFile)) {
try { return inflight.get(finalFile);
if (fs_1.default.existsSync(tempFile)) }
fs_1.default.unlinkSync(tempFile); // 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) {
catch (err) { throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : String(err)}`);
throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); }
} console.log(`✅ Gespeichert als ${tempFileName}`);
console.log(`✅ Gespeichert als ${tempFileName}`); const entpackZeile = `🗜️ Entpacke ${finalFileName}...`;
const entpackZeile = `🗜️ Entpacke ${finalFileName}...`; process.stdout.write(entpackZeile);
process.stdout.write(entpackZeile); await extractBz2Safe(tempFile, finalFile);
await extractBz2Safe(tempFile, finalFile); const successMessage = `✅ Entpackt: ${finalFileName}`;
const successMessage = `✅ Entpackt: ${finalFileName}`; const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`;
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`; const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length);
// Max-Zeichenlänge bestimmen if (!fs_1.default.existsSync(finalFile)) {
const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); const paddedFail = failMessage.padEnd(maxLength, ' ');
if (!fs_1.default.existsSync(finalFile)) { process.stdout.write(`\r${paddedFail}\n`);
const paddedFail = failMessage.padEnd(maxLength, ' '); throw new Error(failMessage);
process.stdout.write(`\r${paddedFail}\n`); }
throw new Error(failMessage); const paddedSuccess = successMessage.padEnd(maxLength, ' ');
} process.stdout.write(`\r${paddedSuccess}\n`);
const paddedSuccess = successMessage.padEnd(maxLength, ' '); // Aufräumen
process.stdout.write(`\r${paddedSuccess}\n`); try {
// Aufräumen 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 {
fs_1.default.unlinkSync(tempFile); return await job;
console.log(`🧹 Gelöscht: ${tempFileName}`);
} }
catch { finally {
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); inflight.delete(finalFile);
} }
return finalFile;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
dist/main.js vendored
View File

@ -3,6 +3,7 @@ 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");
@ -43,7 +44,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 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`); process.stdout.write(`📶 Fortschritt: ${percent}%\r`);
if (percent === 100) { if (percent === 100) {
console.log('✅ Download abgeschlossen'); console.log('✅ Download abgeschlossen');
@ -57,7 +58,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 })); res.end(JSON.stringify({ success: true, path: demoFilePath, existed }));
} }
catch (err) { catch (err) {
console.error('❌ Fehler:', err); console.error('❌ Fehler:', err);

View File

@ -1,3 +1,4 @@
// 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';
@ -8,25 +9,18 @@ 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
* Entpackt eine .bz2-Datei mithilfe von Streams nach .dem const inflight = new Map<string, Promise<{ path: string; existed: boolean }>>();
*/
async function extractBz2Safe(srcPath: string, destPath: string) { async function extractBz2Safe(srcPath: string, destPath: string) {
try { try {
await pipe( await pipe(fs.createReadStream(srcPath), bz2(), fs.createWriteStream(destPath));
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,
@ -36,7 +30,6 @@ 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;
@ -107,106 +100,93 @@ 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, steamId: string,
outputBaseDir = 'demos', outputBaseDir = 'demos',
onProgress?: (percent: number) => void onProgress?: (percent: number) => void
): Promise<string> { ): Promise<{ path: string; existed: boolean; matchId: string; map?: string }> {
if (!outputBaseDir || outputBaseDir.trim() === '') { if (!outputBaseDir || outputBaseDir.trim() === '') outputBaseDir = 'demos'
outputBaseDir = 'demos';
}
const appId = 730;
const matchId = match.matchid;
const timestamp = match.matchtime;
const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0];
const lastRound = match.roundstatsall?.at(-1); const appId = 730
const demoUrl = const matchId: string = String(match.matchid)
typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2') const timestamp = match.matchtime
? lastRound.map const matchDate = new Date(timestamp * 1000).toISOString().split('T')[0]
: undefined;
const mapName = const lastRound = match.roundstatsall?.at(-1)
lastRound?.reservation?.map ||
lastRound?.mapname ||
match.watchablematchinfo?.game_map ||
'unknownmap';
if (!demoUrl) { // echte Demo-URL
throw new Error('❌ Keine Demo-URL im Match vorhanden'); 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; // Mapnamen robust extrahieren
const matchType = isPremier ? 'premier' : 'competitive'; const rawMap =
lastRound?.reservation?.map ??
lastRound?.reservation?.mapname ??
lastRound?.mapname ??
match.watchablematchinfo?.game_map ??
''
const tempDir = path.join(outputBaseDir, 'temp'); const mapName = (String(rawMap).trim() || 'unknownmap')
const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; .replace(/^maps[\\/]/i, '') // "maps/de_inferno" -> "de_inferno"
const baseName = path.parse(tempFileName).name; .split(/[\\/]/).pop() || 'unknownmap'
const tempFile = path.join(tempDir, tempFileName);
const finalDir = path.join(outputBaseDir, steamId, matchDate); const isPremier = !!lastRound?.b_switched_teams
const finalFile = path.join(finalDir, `${baseName}.dem`); 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) const finalFileName = path.basename(finalFile)
fs.mkdirSync(tempDir, { recursive: true }); // schon vorhanden?
fs.mkdirSync(finalDir, { recursive: true });
console.log(`📥 Lade Demo von ${demoUrl}...`);
try { try {
const success = await downloadWithHttps(demoUrl, tempFile, onProgress); fs.accessSync(finalFile)
if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { console.log(`♻️ Demo existiert bereits: ${finalFileName}`)
console.warn(`⚠️ Download fehlgeschlagen oder Datei leer lösche ${tempFileName}`); return { path: path.normalize(finalFile), existed: true, matchId, map: mapName }
} catch {/* not exists */}
try {
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); if (inflight.has(finalFile)) {
} catch { return inflight.get(finalFile)! as any
console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`); }
}
const job = (async () => {
return ''; 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}...`; if (!fs.existsSync(finalFile)) {
process.stdout.write(entpackZeile); 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}`; return { path: path.normalize(finalFile), existed: false, matchId, map: mapName }
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`; })()
// Max-Zeichenlänge bestimmen inflight.set(finalFile, job)
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
try { try {
fs.unlinkSync(tempFile); return await job
console.log(`🧹 Gelöscht: ${tempFileName}`); } finally {
} catch { inflight.delete(finalFile)
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`);
} }
return finalFile;
} }

View File

@ -1,3 +1,4 @@
// 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';
@ -45,7 +46,7 @@ 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 demoFilePath = await downloadDemoFile( const { path: demoFilePath, existed } = await downloadDemoFile(
match, match,
steamId, steamId,
resolvedDemoPath, resolvedDemoPath,
@ -67,7 +68,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 })); res.end(JSON.stringify({ success: true, path: demoFilePath, existed }));
} 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' });