update
This commit is contained in:
parent
db3497138b
commit
ae11e8e17d
117
dist/app/downloadDemoFile.js
vendored
117
dist/app/downloadDemoFile.js
vendored
@ -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;
|
||||
}
|
||||
|
||||
BIN
dist/cs2-demo-downloader-linux
vendored
BIN
dist/cs2-demo-downloader-linux
vendored
Binary file not shown.
BIN
dist/cs2-demo-downloader-macos
vendored
BIN
dist/cs2-demo-downloader-macos
vendored
Binary file not shown.
BIN
dist/cs2-demo-downloader-win.exe
vendored
BIN
dist/cs2-demo-downloader-win.exe
vendored
Binary file not shown.
5
dist/main.js
vendored
5
dist/main.js
vendored
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user