#!/usr/bin/env node const puppeteer = require('puppeteer'); async function getPlayerCount(roomCode) { const browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox' ] }); const page = await browser.newPage(); // Set a realistic user agent to avoid bot detection 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; let roomValidated = false; // Monitor network requests to see if API call happens page.on('response', async (response) => { const url = response.url(); if (url.includes('ecast.jackboxgames.com/api/v2/rooms')) { if (process.env.DEBUG) { console.error('[NETWORK] Room API called:', url, 'Status:', response.status()); } if (response.status() === 200) { roomValidated = true; } } }); // Listen for console messages that contain the client/welcome WebSocket message page.on('console', async msg => { try { const args = msg.args(); for (const arg of args) { const val = await arg.jsonValue(); const str = typeof val === 'object' ? JSON.stringify(val) : String(val); // Debug: log all console messages that might be relevant if (process.env.DEBUG && (str.includes('welcome') || str.includes('here') || str.includes('opcode'))) { console.error('[CONSOLE]', str.substring(0, 200)); } // Look for the client/welcome message with player data if (str.includes('client/welcome')) { try { let data; if (typeof val === 'object') { data = val; } else { // The string might be "recv <- {...}" so extract the JSON part const jsonStart = str.indexOf('{'); if (jsonStart !== -1) { const jsonStr = str.substring(jsonStart); data = JSON.parse(jsonStr); } } if (data && data.opcode === 'client/welcome' && data.result) { // Look for the "here" object which contains all connected players if (data.result.here) { playerCount = Object.keys(data.result.here).length; if (process.env.DEBUG) { console.error('[SUCCESS] Found player count:', playerCount); } } else if (process.env.DEBUG) { console.error('[DEBUG] client/welcome found but no "here" object. Keys:', Object.keys(data.result)); } } } catch (e) { if (process.env.DEBUG) { console.error('[PARSE ERROR]', e.message); } } } } } catch (e) { // Ignore errors } }); try { if (process.env.DEBUG) console.error('[1] Navigating to jackbox.tv...'); await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 }); // Wait for the room code input to be ready if (process.env.DEBUG) console.error('[2] Waiting for form...'); await page.waitForSelector('input#roomcode', { timeout: 10000 }); // Type the room code using the input ID (more reliable) // Use the element.type() method which properly triggers React events if (process.env.DEBUG) console.error('[3] Typing room code:', roomCode); const roomInput = await page.$('input#roomcode'); await roomInput.type(roomCode.toUpperCase(), { delay: 50 }); // Reduced delay from 100ms to 50ms // Wait for room validation (the app info appears after validation) if (process.env.DEBUG) { console.error('[4] Waiting for room validation...'); const roomValue = await page.evaluate(() => document.querySelector('input#roomcode').value); console.error('[4] Room code value:', roomValue); } // Actually wait for the validation to complete - the game name label appears try { await page.waitForFunction(() => { const labels = Array.from(document.querySelectorAll('div, span, label')); return labels.some(el => { const text = el.textContent; return text.includes('Trivia') || text.includes('Party') || text.includes('Quiplash') || text.includes('Fibbage') || text.includes('Drawful') || text.includes('Murder'); }); }, { timeout: 5000 }); if (process.env.DEBUG) console.error('[4.5] Room validated successfully!'); } catch (e) { if (process.env.DEBUG) console.error('[4.5] Room validation timeout - continuing anyway...'); } // Type the name using the input ID // This will trigger the input event that enables the Play button if (process.env.DEBUG) console.error('[5] Typing name...'); const nameInput = await page.$('input#username'); await nameInput.type('Observer', { delay: 30 }); // Reduced delay from 100ms to 30ms // Wait a moment for the button to enable and click immediately if (process.env.DEBUG) console.error('[6] Waiting for Play button...'); await page.waitForFunction(() => { const buttons = Array.from(document.querySelectorAll('button')); const playBtn = buttons.find(b => { const text = b.textContent.toUpperCase(); return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled; }); return playBtn !== undefined; }, { timeout: 5000 }); // Reduced timeout from 10s to 5s if (process.env.DEBUG) console.error('[7] Clicking Play...'); await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')); const playBtn = buttons.find(b => { const text = b.textContent.toUpperCase(); return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled; }); if (playBtn) { playBtn.click(); } else { throw new Error('Play button not found or still disabled'); } }); // Wait for the WebSocket player count message (up to 5 seconds) if (process.env.DEBUG) console.error('[8] Waiting for player count message...'); for (let i = 0; i < 10 && playerCount === null; i++) { await new Promise(resolve => setTimeout(resolve, 500)); } } finally { await browser.close(); } if (playerCount === null) { throw new Error('Could not get player count from WebSocket'); } return playerCount; } // Main const roomCode = process.argv[2]; if (!roomCode) { console.error('Usage: node jackbox-count-v3.js '); process.exit(1); } getPlayerCount(roomCode.toUpperCase()) .then(count => { console.log(count); process.exit(0); }) .catch(err => { console.error('Error:', err.message); process.exit(1); });