-
-
Save Cozy19/40502f9b34e670b1662c556ec575610b to your computer and use it in GitHub Desktop.
Listen to a youtube stream in puppeteer (headless chrome/chromium). By default plays Chillcow's channel, works on linux and windows, clicks on all the popup crap.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// first: `npm init -- yes && npm i chalk puppeteer-core` | |
// then: `node . --help` | |
const puppeteer = require('puppeteer-core'); | |
const chalk = require('chalk'); | |
// --- | |
const ifHasParam = (s = '') => process.argv.includes(s); | |
const paramValueOf = (s = '') => (process.argv.find(x => x.startsWith(`${s}=`)) || '').split('=')[1]; | |
const DEFAULT_VIDEO_ID = '5qap5aO4i9A'; // ChillCow; his other channel is "DWcJFNfaw9c" | |
const DEBUG = ifHasParam('--debug'); | |
const SHOW_HEAD = ifHasParam('--head'); | |
const MAX_TRIES = parseInt(paramValueOf('--iteration'), 10) || 20; | |
const VIDEO_ID = paramValueOf('--vid'); | |
const URL = `https://www.youtube.com/watch?v=${VIDEO_ID || DEFAULT_VIDEO_ID}`; | |
const ALSA_DEVICE = paramValueOf('--alsa'); | |
let BROWSER_PATH = paramValueOf('--browser');; | |
if (!BROWSER_PATH && process.platform === 'linux') BROWSER_PATH = '/usr/bin/chromium'; | |
if (!BROWSER_PATH && process.platform === 'win32') BROWSER_PATH = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; | |
// --- | |
if (ifHasParam('--help')) { | |
console.info('Params (all optional):\n--help\n--debug\n--head\n--vid=YOUTUBE_vID' + | |
'\n--iteration=N\n--browser=/usr/bin/chromium\n--alsa=ALSA_DEVICE_ID'); | |
process.exit(); | |
} | |
let pup = { browser: null, page: null }; | |
const selectors = { | |
pageBody: 'body', | |
gdprButton: 'button[aria-label^="Agree"]', | |
introAgreeButton: '#introAgreeButton', | |
playButton: 'button[aria-label="Play"]', | |
noThanksButton: '#button[aria-label="No thanks"]', | |
dismissBullshitButton: '#dismiss-button paper-button', | |
consentIframe: 'iframe[src*="consent"]', | |
consentForm: 'form[action*="consent"]', | |
skipAdButton: 'button[class*="skip-button"]' | |
}; | |
const print = (...args) => console.info(chalk.cyan(...args)); | |
const sleep = (t = 0) => new Promise((resolve) => { setTimeout(resolve, t); }); | |
async function waitForSelector(sel = '', timeout = 2000) { | |
if (DEBUG) print(`Waiting for ${chalk.blue(sel)}`); | |
if (selectors[sel]) sel = selectors[sel]; | |
let success = true; | |
try { success = await pup.page.waitForSelector(sel, { timeout }); } catch (err) { success = false; } | |
return success; | |
} | |
const _clicked = {}; | |
async function click(sel = '', times = 1) { | |
if (DEBUG) print(`Clicking "${chalk.blue(sel)}"`); | |
if (selectors[sel]) sel = selectors[sel]; | |
if (_clicked[sel] >= times) return; | |
let success = true; | |
try { await pup.page.click(sel); } catch (err) { success = false; } | |
if (success) _clicked[sel] = (_clicked[sel] || 0) + 1; | |
return success; | |
} | |
async function acceptConsent() { | |
let hasConsentFrame = await waitForSelector('consentIframe'); | |
if (!hasConsentFrame) return; | |
const frame = pup.page.frames().find((frame, i) => frame.url().includes('consent')); | |
if (frame) { | |
let success = true; | |
try { await frame.waitForSelector(selectors.consentForm, { timeout: 2000 }); } catch (err) { success = false; } | |
if (success) { | |
if (DEBUG) print('Submitting consent in iframe') | |
await frame.$eval(selectors.consentForm, form => form.submit()); | |
} | |
} | |
} | |
async function quit(code = 0, message = '') { | |
if (message) print(message); | |
await pup.browser.close(); | |
process.exit(code); | |
} | |
async function handleExit() { | |
await quit(0, 'Closing chrome, good bye!'); | |
} | |
// --- | |
(async () => { | |
process.on('SIGINT', handleExit); | |
print(`Let's try to play ${chalk.white(URL)}`); | |
const headless = !SHOW_HEAD; | |
const args = [ | |
'--autoplay-policy=no-user-gesture-required', | |
'--disable-background-timer-throttling', | |
'--disable-backgrounding-occluded-windows', | |
'--disable-renderer-backgrounding', | |
'--disable-crash-reporter', | |
// youtube will block playback for headless browsers | |
// for headless detection see: https://intoli.com/blog/making-chrome-headless-undetectable/ | |
'--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"', | |
'--use-gl=egl', // linux headless | |
// linux sound: use `aplay -L` then `speaker-test -Dplug:front -c2` to determine the proper device | |
// and also, please fuck pulseaudio and all the devices it rode in on, yes, even in 2021 | |
ALSA_DEVICE ? `--alsa-output-device=${ALSA_DEVICE}` : '', | |
headless ? '--headless' : '' // headless by itself in pup params is NOT enough of course | |
].filter(x => x); | |
if (DEBUG) print(`Args:\n${args.join('\n')}`); | |
const ignoreDefaultArgs = '--mute-audio'; | |
const browser = pup.browser = await puppeteer.launch({ headless, executablePath: BROWSER_PATH, ignoreDefaultArgs, args }); | |
const page = pup.page = await browser.newPage(); | |
await page.goto(URL); | |
for (let i = 0; i < MAX_TRIES; i++) { // when in doubt, use brute force | |
print(`Iteration ${chalk.white(i + 1)}/${chalk.white(MAX_TRIES)}...`); | |
if (DEBUG) page.screenshot({ path: `screen-${i}.png` }); | |
await sleep(3000); | |
const hasBody = await waitForSelector('pageBody'); | |
if (!hasBody) { print('No body?!'); continue; } | |
await acceptConsent(); | |
await click('playButton'); | |
await click('gdprButton'); | |
await click('dismissBullshitButton'); | |
await click('noThanksButton'); | |
await click('skipAdButton'); | |
await click('introAgreeButton'); | |
} | |
print('Done. Press ctrl+c to exit.'); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment