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

185 lines
6.7 KiB
JavaScript
Executable File

#!/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 <ROOM_CODE>');
process.exit(1);
}
getPlayerCount(roomCode.toUpperCase())
.then(count => {
console.log(count);
process.exit(0);
})
.catch(err => {
console.error('Error:', err.message);
process.exit(1);
});