push
This commit is contained in:
commit
b2c00edfca
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
demos
|
||||
node_modules
|
||||
167
dist/app/downloadDemoFile.js
vendored
Normal file
167
dist/app/downloadDemoFile.js
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.downloadDemoFile = downloadDemoFile;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const https_1 = __importDefault(require("https"));
|
||||
const http_1 = __importDefault(require("http"));
|
||||
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
|
||||
*/
|
||||
async function extractBz2Safe(srcPath, destPath) {
|
||||
try {
|
||||
await pipe(fs_1.default.createReadStream(srcPath), (0, unbzip2_stream_1.default)(), fs_1.default.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, dest, onProgress, maxRetries = 3, retryDelay = 3000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let attempt = 0;
|
||||
const tryDownload = () => {
|
||||
const file = fs_1.default.createWriteStream(dest);
|
||||
const client = url.startsWith('https') ? https_1.default : http_1.default;
|
||||
let downloaded = 0;
|
||||
let total = 0;
|
||||
let lastPercent = -1;
|
||||
const request = client.get(url, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
file.close();
|
||||
file.destroy();
|
||||
if ([502, 503, 504].includes(res.statusCode) && attempt < maxRetries) {
|
||||
process.stdout.write(`🔁 Retry ${attempt + 1}/${maxRetries} – HTTP ${res.statusCode}\r`);
|
||||
attempt++;
|
||||
setTimeout(tryDownload, retryDelay);
|
||||
return;
|
||||
}
|
||||
if (attempt >= maxRetries) {
|
||||
console.log(`❌ Max. Versuche erreicht (${maxRetries}), Datei wird übersprungen`);
|
||||
return resolve(false);
|
||||
}
|
||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
||||
}
|
||||
total = parseInt(res.headers['content-length'] || '0', 10);
|
||||
res.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
if (onProgress && total) {
|
||||
const percent = Math.floor((downloaded / total) * 100);
|
||||
if (percent !== lastPercent) {
|
||||
lastPercent = percent;
|
||||
onProgress(percent);
|
||||
}
|
||||
}
|
||||
});
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close((err) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
res.on('error', reject);
|
||||
file.on('error', reject);
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`🔁 Retry ${attempt + 1}/${maxRetries} wegen Verbindungsfehler: ${err.message}`);
|
||||
attempt++;
|
||||
setTimeout(tryDownload, retryDelay);
|
||||
}
|
||||
else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
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';
|
||||
}
|
||||
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 demoUrl = typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')
|
||||
? lastRound.map
|
||||
: undefined;
|
||||
const mapName = lastRound?.reservation?.map ||
|
||||
lastRound?.mapname ||
|
||||
match.watchablematchinfo?.game_map ||
|
||||
'unknownmap';
|
||||
if (!demoUrl) {
|
||||
throw new Error('❌ Keine Demo-URL im Match vorhanden');
|
||||
}
|
||||
const isPremier = !!lastRound?.b_switched_teams;
|
||||
const matchType = isPremier ? 'premier' : 'competitive';
|
||||
const tempDir = path_1.default.join(outputBaseDir, 'temp');
|
||||
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 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);
|
||||
}
|
||||
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
|
||||
try {
|
||||
fs_1.default.unlinkSync(tempFile);
|
||||
console.log(`🧹 Gelöscht: ${tempFileName}`);
|
||||
}
|
||||
catch {
|
||||
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`);
|
||||
}
|
||||
return finalFile;
|
||||
}
|
||||
17
dist/app/fetchMatchFromSharecode.js
vendored
Normal file
17
dist/app/fetchMatchFromSharecode.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fetchMatchFromShareCode = fetchMatchFromShareCode;
|
||||
const csgo_sharecode_1 = require("csgo-sharecode");
|
||||
async function fetchMatchFromShareCode(shareCode, session) {
|
||||
const { csgo } = session;
|
||||
const decoded = (0, csgo_sharecode_1.decodeMatchShareCode)(shareCode);
|
||||
return new Promise((resolve, reject) => {
|
||||
csgo.once('matchList', (matches) => {
|
||||
const match = matches.find((m) => m.matchid === decoded.matchId.toString());
|
||||
if (!match)
|
||||
return reject(new Error('❌ Kein Match gefunden'));
|
||||
resolve(match);
|
||||
});
|
||||
csgo.requestGame(shareCode);
|
||||
});
|
||||
}
|
||||
44
dist/app/steamSession.js
vendored
Normal file
44
dist/app/steamSession.js
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createSteamSession = createSteamSession;
|
||||
const steam_user_1 = __importDefault(require("steam-user"));
|
||||
const globaloffensive_1 = __importDefault(require("globaloffensive"));
|
||||
async function createSteamSession(username, password, mfa) {
|
||||
const client = new steam_user_1.default();
|
||||
const csgo = new globaloffensive_1.default(client);
|
||||
const loginPromise = new Promise((resolve, reject) => {
|
||||
client.logOn({
|
||||
accountName: username,
|
||||
password,
|
||||
twoFactorCode: mfa,
|
||||
});
|
||||
client.once('loggedOn', () => {
|
||||
console.log('✅ Eingeloggt bei Steam');
|
||||
client.setPersona(steam_user_1.default.EPersonaState.Online);
|
||||
client.gamesPlayed(730);
|
||||
});
|
||||
client.on('friendRelationship', (steamID, relationship) => {
|
||||
if (relationship === steam_user_1.default.EFriendRelationship.RequestRecipient) {
|
||||
console.log(`➕ Freundschaftsanfrage von ${steamID.getSteamID64()} erhalten – wird akzeptiert`);
|
||||
client.addFriend(steamID, (err) => {
|
||||
if (err) {
|
||||
console.error(`❌ Fehler beim Akzeptieren von ${steamID.getSteamID64()}:`, err);
|
||||
}
|
||||
else {
|
||||
console.log(`✅ Freund hinzugefügt: ${steamID.getSteamID64()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
csgo.once('connectedToGC', () => {
|
||||
console.log('🎮 Verbunden mit dem Game Coordinator');
|
||||
resolve();
|
||||
});
|
||||
client.once('error', reject);
|
||||
});
|
||||
await loginPromise;
|
||||
return { client, csgo };
|
||||
}
|
||||
BIN
dist/cs2-demo-downloader-linux
vendored
Normal file
BIN
dist/cs2-demo-downloader-linux
vendored
Normal file
Binary file not shown.
BIN
dist/cs2-demo-downloader-macos
vendored
Normal file
BIN
dist/cs2-demo-downloader-macos
vendored
Normal file
Binary file not shown.
BIN
dist/cs2-demo-downloader-win.exe
vendored
Normal file
BIN
dist/cs2-demo-downloader-win.exe
vendored
Normal file
Binary file not shown.
69
dist/main.js
vendored
Normal file
69
dist/main.js
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const yargs_1 = __importDefault(require("yargs"));
|
||||
const helpers_1 = require("yargs/helpers");
|
||||
const steamSession_1 = require("./app/steamSession");
|
||||
const fetchMatchFromSharecode_1 = require("./app/fetchMatchFromSharecode");
|
||||
const downloadDemoFile_1 = require("./app/downloadDemoFile");
|
||||
const http_1 = __importDefault(require("http"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)).options({
|
||||
username: { type: 'string', demandOption: true },
|
||||
password: { type: 'string', demandOption: true },
|
||||
mfa: { type: 'string', demandOption: false },
|
||||
authCode: { type: 'string', demandOption: false },
|
||||
port: { type: 'number', default: 4000, demandOption: false },
|
||||
demoPath: {
|
||||
type: 'string',
|
||||
default: 'demos',
|
||||
demandOption: false,
|
||||
description: 'Zielverzeichnis für heruntergeladene Demos',
|
||||
},
|
||||
}).parseSync();
|
||||
const PORT = argv.port;
|
||||
const resolvedDemoPath = path_1.default.resolve(argv.demoPath);
|
||||
if (!fs_1.default.existsSync(resolvedDemoPath)) {
|
||||
fs_1.default.mkdirSync(resolvedDemoPath, { recursive: true });
|
||||
console.log(`📂 Verzeichnis erstellt: ${resolvedDemoPath}`);
|
||||
}
|
||||
async function start() {
|
||||
const session = await (0, steamSession_1.createSteamSession)(argv.username, argv.password, argv.mfa);
|
||||
console.log(`🚀 Server läuft auf http://localhost:${PORT}`);
|
||||
http_1.default
|
||||
.createServer(async (req, res) => {
|
||||
if (req.method === 'POST' && req.url === '/download') {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => (body += chunk));
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { shareCode, steamId } = JSON.parse(body);
|
||||
console.log(`📦 ShareCode empfangen: ${shareCode}`);
|
||||
const match = await (0, fetchMatchFromSharecode_1.fetchMatchFromShareCode)(shareCode, session);
|
||||
const path = await (0, downloadDemoFile_1.downloadDemoFile)(match, steamId, resolvedDemoPath, (percent) => {
|
||||
process.stdout.write(`📶 Fortschritt: ${percent}%\r`);
|
||||
if (percent === 100) {
|
||||
console.log('✅ Download abgeschlossen');
|
||||
}
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, path }));
|
||||
}
|
||||
catch (err) {
|
||||
console.error('❌ Fehler:', err);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : err }));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
.listen(PORT);
|
||||
}
|
||||
start();
|
||||
2
dist/types/types.js
vendored
Normal file
2
dist/types/types.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
1685
package-lock.json
generated
Normal file
1685
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "cs2-demo-downloader",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc && pkg . --targets node18-win-x64,node18-linux-x64,node18-macos-x64 --out-path dist"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"csgo-sharecode": "^3.1.2",
|
||||
"decompress": "^4.2.1",
|
||||
"decompress-bzip2": "^4.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"globaloffensive": "^3.2.0",
|
||||
"lzma": "^2.3.2",
|
||||
"steam-session": "^1.9.3",
|
||||
"steam-totp": "^2.1.2",
|
||||
"steam-user": "^5.2.1",
|
||||
"unbzip2-stream": "^1.4.3",
|
||||
"undici": "^6.21.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/decompress": "^4.2.7",
|
||||
"@types/globaloffensive": "^2.3.4",
|
||||
"@types/node": "^22.15.16",
|
||||
"@types/steam-user": "^5.0.4",
|
||||
"@types/unbzip2-stream": "^1.4.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"node_modules/lzma/src/lzma_worker.js",
|
||||
"node_modules/@doctormckay/steam-crypto/system.pem"
|
||||
]
|
||||
}
|
||||
}
|
||||
212
src/app/downloadDemoFile.ts
Normal file
212
src/app/downloadDemoFile.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
import { pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import bz2 from 'unbzip2-stream';
|
||||
|
||||
const pipe = promisify(pipeline);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
);
|
||||
} 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,
|
||||
onProgress?: (percent: number) => void,
|
||||
maxRetries = 3,
|
||||
retryDelay = 3000
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let attempt = 0;
|
||||
|
||||
const tryDownload = () => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
|
||||
let downloaded = 0;
|
||||
let total = 0;
|
||||
let lastPercent = -1;
|
||||
|
||||
const request = client.get(url, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
file.close();
|
||||
file.destroy();
|
||||
|
||||
if ([502, 503, 504].includes(res.statusCode!) && attempt < maxRetries) {
|
||||
process.stdout.write(`🔁 Retry ${attempt + 1}/${maxRetries} – HTTP ${res.statusCode}\r`);
|
||||
attempt++;
|
||||
setTimeout(tryDownload, retryDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt >= maxRetries) {
|
||||
console.log(`❌ Max. Versuche erreicht (${maxRetries}), Datei wird übersprungen (HTTP ${res.statusCode})`);
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
||||
}
|
||||
|
||||
total = parseInt(res.headers['content-length'] || '0', 10);
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
if (onProgress && total) {
|
||||
const percent = Math.floor((downloaded / total) * 100);
|
||||
if (percent !== lastPercent) {
|
||||
lastPercent = percent;
|
||||
onProgress(percent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close((err) => {
|
||||
if (err) return reject(err);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
res.on('error', reject);
|
||||
file.on('error', reject);
|
||||
});
|
||||
|
||||
request.on('error', (err) => {
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`🔁 Retry ${attempt + 1}/${maxRetries} wegen Verbindungsfehler: ${err.message}`);
|
||||
attempt++;
|
||||
setTimeout(tryDownload, retryDelay);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryDownload();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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<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 lastRound = match.roundstatsall?.at(-1);
|
||||
const demoUrl =
|
||||
typeof lastRound?.map === 'string' && lastRound.map.endsWith('.bz2')
|
||||
? lastRound.map
|
||||
: undefined;
|
||||
|
||||
const mapName =
|
||||
lastRound?.reservation?.map ||
|
||||
lastRound?.mapname ||
|
||||
match.watchablematchinfo?.game_map ||
|
||||
'unknownmap';
|
||||
|
||||
if (!demoUrl) {
|
||||
throw new Error('❌ Keine Demo-URL im Match vorhanden');
|
||||
}
|
||||
|
||||
const isPremier = !!lastRound?.b_switched_teams;
|
||||
const matchType = isPremier ? 'premier' : 'competitive';
|
||||
|
||||
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 finalDir = path.join(outputBaseDir, steamId, 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}...`);
|
||||
|
||||
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 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;
|
||||
}
|
||||
21
src/app/fetchMatchFromSharecode.ts
Normal file
21
src/app/fetchMatchFromSharecode.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { decodeMatchShareCode } from 'csgo-sharecode';
|
||||
import { createSteamSession } from './steamSession';
|
||||
import { SteamSession } from '../types/types';
|
||||
|
||||
export async function fetchMatchFromShareCode(
|
||||
shareCode: string,
|
||||
session: SteamSession
|
||||
): Promise<any> {
|
||||
const { csgo } = session;
|
||||
const decoded = decodeMatchShareCode(shareCode);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
csgo.once('matchList', (matches) => {
|
||||
const match = matches.find((m) => m.matchid === decoded.matchId.toString());
|
||||
if (!match) return reject(new Error('❌ Kein Match gefunden'));
|
||||
resolve(match);
|
||||
});
|
||||
|
||||
csgo.requestGame(shareCode);
|
||||
});
|
||||
}
|
||||
49
src/app/steamSession.ts
Normal file
49
src/app/steamSession.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import SteamUser from 'steam-user';
|
||||
import GlobalOffensive from 'globaloffensive';
|
||||
import { SteamSession } from '../types/types';
|
||||
|
||||
export async function createSteamSession(
|
||||
username: string,
|
||||
password: string,
|
||||
mfa?: string
|
||||
): Promise<SteamSession> {
|
||||
const client = new SteamUser();
|
||||
const csgo = new GlobalOffensive(client);
|
||||
|
||||
const loginPromise = new Promise<void>((resolve, reject) => {
|
||||
client.logOn({
|
||||
accountName: username,
|
||||
password,
|
||||
twoFactorCode: mfa,
|
||||
});
|
||||
|
||||
client.once('loggedOn', () => {
|
||||
console.log('✅ Eingeloggt bei Steam');
|
||||
client.setPersona(SteamUser.EPersonaState.Online);
|
||||
client.gamesPlayed(730);
|
||||
});
|
||||
|
||||
client.on('friendRelationship', (steamID, relationship) => {
|
||||
if (relationship === SteamUser.EFriendRelationship.RequestRecipient) {
|
||||
console.log(`➕ Freundschaftsanfrage von ${steamID.getSteamID64()} erhalten – wird akzeptiert`);
|
||||
client.addFriend(steamID, (err) => {
|
||||
if (err) {
|
||||
console.error(`❌ Fehler beim Akzeptieren von ${steamID.getSteamID64()}:`, err);
|
||||
} else {
|
||||
console.log(`✅ Freund hinzugefügt: ${steamID.getSteamID64()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
csgo.once('connectedToGC', () => {
|
||||
console.log('🎮 Verbunden mit dem Game Coordinator');
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.once('error', reject);
|
||||
});
|
||||
|
||||
await loginPromise;
|
||||
return { client, csgo };
|
||||
}
|
||||
77
src/main.ts
Normal file
77
src/main.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { createSteamSession } from './app/steamSession';
|
||||
import { fetchMatchFromShareCode } from './app/fetchMatchFromSharecode';
|
||||
import { downloadDemoFile } from './app/downloadDemoFile';
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const argv = yargs(hideBin(process.argv)).options({
|
||||
username: { type: 'string', demandOption: true },
|
||||
password: { type: 'string', demandOption: true },
|
||||
mfa: { type: 'string', demandOption: false },
|
||||
authCode: { type: 'string', demandOption: false },
|
||||
port: { type: 'number', default: 4000, demandOption: false },
|
||||
demoPath: {
|
||||
type: 'string',
|
||||
default: 'demos',
|
||||
demandOption: false,
|
||||
description: 'Zielverzeichnis für heruntergeladene Demos',
|
||||
},
|
||||
}).parseSync();
|
||||
|
||||
const PORT = argv.port;
|
||||
|
||||
const resolvedDemoPath = path.resolve(argv.demoPath);
|
||||
if (!fs.existsSync(resolvedDemoPath)) {
|
||||
fs.mkdirSync(resolvedDemoPath, { recursive: true });
|
||||
console.log(`📂 Verzeichnis erstellt: ${resolvedDemoPath}`);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
|
||||
const session = await createSteamSession(argv.username, argv.password, argv.mfa);
|
||||
|
||||
console.log(`🚀 Server läuft auf http://localhost:${PORT}`);
|
||||
http
|
||||
.createServer(async (req, res) => {
|
||||
if (req.method === 'POST' && req.url === '/download') {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => (body += chunk));
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { shareCode, steamId } = JSON.parse(body);
|
||||
console.log(`📦 ShareCode empfangen: ${shareCode}`);
|
||||
|
||||
const match = await fetchMatchFromShareCode(shareCode, session);
|
||||
const path = await downloadDemoFile(
|
||||
match,
|
||||
steamId,
|
||||
resolvedDemoPath,
|
||||
(percent: number) => {
|
||||
process.stdout.write(`📶 Fortschritt: ${percent}%\r`);
|
||||
if (percent === 100) {
|
||||
console.log('✅ Download abgeschlossen');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, path }));
|
||||
} catch (err) {
|
||||
console.error('❌ Fehler:', err);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : err }));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
.listen(PORT);
|
||||
}
|
||||
|
||||
start();
|
||||
21
src/types/types.ts
Normal file
21
src/types/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import SteamUser from 'steam-user';
|
||||
import GlobalOffensive from 'globaloffensive';
|
||||
|
||||
export type DecodedShareCode = {
|
||||
matchId: bigint;
|
||||
reservationId: bigint;
|
||||
tvPort: number;
|
||||
};
|
||||
|
||||
export type DownloadParams = {
|
||||
decoded: DecodedShareCode;
|
||||
username: string;
|
||||
password: string;
|
||||
authCode?: string;
|
||||
outputPath: string;
|
||||
};
|
||||
|
||||
export type SteamSession = {
|
||||
client: SteamUser;
|
||||
csgo: GlobalOffensive;
|
||||
};
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user