#!/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', '--disable-web-security'] }); const page = await browser.newPage(); let playerCount = null; // Listen for console API calls (this catches console.log with formatting) page.on('console', async msg => { try { // Get all args for (const arg of msg.args()) { const val = await arg.jsonValue(); const str = JSON.stringify(val); if (process.env.DEBUG) console.error('[CONSOLE]', str.substring(0, 200)); // Check if this is the welcome message if (str && str.includes('"opcode":"client/welcome"') && str.includes('"here"')) { const data = JSON.parse(str); if (data.result && data.result.here) { playerCount = Object.keys(data.result.here).length; if (process.env.DEBUG) console.error('[FOUND] Player count:', playerCount); break; } } } } catch (e) { // Ignore } }); try { if (process.env.DEBUG) console.error('[1] Loading page...'); await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 }); // Clear storage and reload to avoid reconnect if (process.env.DEBUG) console.error('[2] Clearing storage...'); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); await page.reload({ waitUntil: 'networkidle2' }); await page.waitForSelector('input[placeholder*="ENTER 4-LETTER CODE"]', { timeout: 10000 }); if (process.env.DEBUG) { console.error('[2.5] Checking all inputs on page...'); const allInputs = await page.evaluate(() => { const inputs = Array.from(document.querySelectorAll('input')); return inputs.map(inp => ({ type: inp.type, placeholder: inp.placeholder, value: inp.value, name: inp.name, id: inp.id, visible: inp.offsetParent !== null })); }); console.error('[2.5] All inputs:', JSON.stringify(allInputs, null, 2)); } if (process.env.DEBUG) console.error('[3] Typing room code...'); // Type room code character by character (with delay to trigger validation) await page.click('input[placeholder*="ENTER 4-LETTER CODE"]'); await page.type('input[placeholder*="ENTER 4-LETTER CODE"]', roomCode, { delay: 100 }); // Wait for room validation to complete (look for loader success message) if (process.env.DEBUG) console.error('[3.5] Waiting for room validation...'); await new Promise(resolve => setTimeout(resolve, 1500)); // Type name character by character - this will enable the Play button if (process.env.DEBUG) console.error('[3.6] Typing name...'); await page.click('input[placeholder*="ENTER YOUR NAME"]'); await page.type('input[placeholder*="ENTER YOUR NAME"]', 'Observer', { delay: 100 }); // Wait a moment for button to fully enable await new Promise(resolve => setTimeout(resolve, 500)); if (process.env.DEBUG) { const fieldValues = await page.evaluate(() => { const roomInput = document.querySelector('input[placeholder*="ENTER 4-LETTER CODE"]'); const nameInput = document.querySelector('input[placeholder*="ENTER YOUR NAME"]'); return { roomCode: roomInput ? roomInput.value : 'NOT FOUND', name: nameInput ? nameInput.value : 'NOT FOUND' }; }); console.error('[3.5] Field values:', fieldValues); } // Find the Play or RECONNECT button (case-insensitive, not disabled) if (process.env.DEBUG) { const buttonInfo = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')); const allButtons = buttons.map(b => ({ text: b.textContent.trim(), disabled: b.disabled, visible: b.offsetParent !== null })); const actionBtn = buttons.find(b => { const text = b.textContent.toUpperCase(); return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled; }); return { allButtons, found: actionBtn ? actionBtn.textContent.trim() : 'NOT FOUND' }; }); console.error('[4] All buttons:', JSON.stringify(buttonInfo.allButtons, null, 2)); console.error('[4] Target button:', buttonInfo.found); } if (process.env.DEBUG) console.error('[5] Clicking Play/Reconnect (even if disabled)...'); await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')); const actionBtn = buttons.find(b => { const text = b.textContent.toUpperCase(); return text.includes('PLAY') || text.includes('RECONNECT'); }); if (actionBtn) { // Remove disabled attribute and click actionBtn.disabled = false; actionBtn.click(); } else { throw new Error('Could not find PLAY or RECONNECT button'); } }); // Wait for navigation/lobby to load if (process.env.DEBUG) console.error('[6] Waiting for lobby (5 seconds)...'); await new Promise(resolve => setTimeout(resolve, 5000)); // Check if we're in the lobby const pageText = await page.evaluate(() => document.body.innerText); const inLobby = pageText.includes('Sit back') || pageText.includes('relax'); if (process.env.DEBUG) { console.error('[7] In lobby:', inLobby); console.error('[7] Page text sample:', pageText.substring(0, 100)); console.error('[7] Page URL:', page.url()); } // Wait for WebSocket message 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, 500)); if (process.env.DEBUG && i % 4 === 0) { console.error(`[8.${i}] Still waiting...`); } } } finally { await browser.close(); } if (playerCount === null) { throw new Error('Could not get player count'); } return playerCount; } const roomCode = process.argv[2]; if (!roomCode) { console.error('Usage: node jackbox-count-v2.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); });