Files
jackboxpartypack-gamepicker/scripts/jackbox-count.js
2025-11-03 13:57:26 -05:00

191 lines
6.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
const puppeteer = require('puppeteer');
async function checkRoomStatus(roomCode) {
try {
const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`);
if (response.ok) {
const data = await response.json();
const roomData = data.body || data;
if (process.env.DEBUG) {
console.error('[API] Room data:', JSON.stringify(roomData, null, 2));
}
return {
exists: true,
locked: roomData.locked || false,
full: roomData.full || false,
maxPlayers: roomData.maxPlayers || 8,
minPlayers: roomData.minPlayers || 0
};
}
return { exists: false };
} catch (e) {
if (process.env.DEBUG) {
console.error('[API] Error checking room:', e.message);
}
return { exists: false };
}
}
async function getPlayerCountFromAudience(roomCode) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
let playerCount = null;
// Enable CDP and listen for WebSocket frames BEFORE navigating
const client = await page.target().createCDPSession();
await client.send('Network.enable');
client.on('Network.webSocketFrameReceived', ({ response }) => {
if (response.payloadData && playerCount === null) {
try {
const data = JSON.parse(response.payloadData);
// Check for bc:room with player count data
let roomVal = null;
if (data.opcode === 'client/welcome' && data.result?.entities?.['bc:room']) {
roomVal = data.result.entities['bc:room'][1]?.val;
}
if (data.opcode === 'object' && data.result?.key === 'bc:room') {
roomVal = data.result.val;
}
if (roomVal) {
// Strategy 1: Game ended - use gameResults
if (roomVal.gameResults?.players) {
playerCount = roomVal.gameResults.players.length;
if (process.env.DEBUG) {
console.error('[SUCCESS] Found', playerCount, 'players from gameResults');
}
}
// Strategy 2: Game in progress - use analytics
if (playerCount === null && roomVal.analytics) {
const startAnalytic = roomVal.analytics.find(a => a.action === 'start');
if (startAnalytic?.value) {
playerCount = startAnalytic.value;
if (process.env.DEBUG) {
console.error('[SUCCESS] Found', playerCount, 'players from analytics');
}
}
}
}
} catch (e) {
// Ignore parse errors
}
}
});
try {
if (process.env.DEBUG) console.error('[2] Navigating to jackbox.tv...');
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
if (process.env.DEBUG) console.error('[3] Waiting for form...');
await page.waitForSelector('input#roomcode', { timeout: 10000 });
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
if (process.env.DEBUG) console.error('[4] Typing room code:', roomCode);
const roomInput = await page.$('input#roomcode');
await roomInput.type(roomCode.toUpperCase(), { delay: 50 });
await new Promise(resolve => setTimeout(resolve, 2000));
if (process.env.DEBUG) console.error('[5] Typing name...');
const nameInput = await page.$('input#username');
await nameInput.type('CountBot', { delay: 30 });
if (process.env.DEBUG) console.error('[6] Waiting for JOIN AUDIENCE button...');
await page.waitForFunction(() => {
const buttons = Array.from(document.querySelectorAll('button'));
return buttons.some(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
}, { timeout: 10000 });
if (process.env.DEBUG) console.error('[7] Clicking JOIN AUDIENCE...');
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const btn = buttons.find(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
if (btn) btn.click();
});
// Wait for WebSocket messages
if (process.env.DEBUG) console.error('[8] Waiting for player count...');
for (let i = 0; i < 20 && playerCount === null; i++) {
await new Promise(resolve => setTimeout(resolve, 300));
}
} finally {
await browser.close();
}
return playerCount;
}
async function getPlayerCount(roomCode) {
if (process.env.DEBUG) console.error('[1] Checking room status via API...');
const roomStatus = await checkRoomStatus(roomCode);
if (!roomStatus.exists) {
if (process.env.DEBUG) console.error('[ERROR] Room does not exist');
return 0;
}
// If full, return maxPlayers
if (roomStatus.full) {
if (process.env.DEBUG) console.error('[1] Room is FULL - returning maxPlayers:', roomStatus.maxPlayers);
return roomStatus.maxPlayers;
}
// If locked (game in progress), join as audience to get count
if (roomStatus.locked) {
if (process.env.DEBUG) console.error('[1] Room is LOCKED - joining as audience...');
try {
const count = await getPlayerCountFromAudience(roomCode);
if (count !== null) {
return count;
}
} catch (e) {
if (process.env.DEBUG) console.error('[ERROR] Failed to get count:', e.message);
}
// Fallback to maxPlayers if we couldn't get exact count
if (process.env.DEBUG) console.error('[1] Could not get exact count, returning maxPlayers:', roomStatus.maxPlayers);
return roomStatus.maxPlayers;
}
// Not locked (lobby open) - don't join, return minPlayers
if (process.env.DEBUG) console.error('[1] Room is NOT locked (lobby) - returning minPlayers:', roomStatus.minPlayers);
return roomStatus.minPlayers;
}
// Main
const roomCode = process.argv[2];
if (!roomCode) {
console.error('Usage: node jackbox-count.js <ROOM_CODE>');
process.exit(1);
}
getPlayerCount(roomCode.toUpperCase())
.then(count => {
console.log(count);
process.exit(0);
})
.catch(err => {
if (process.env.DEBUG) console.error('Error:', err.message);
console.log(0);
process.exit(0);
});