Last active
April 16, 2020 16:54
-
-
Save conundrumer/0cd01dc60f0b129e4cdc148a6e9606af to your computer and use it in GitHub Desktop.
Periodically toggles visibility of a YouTube video.
This file contains hidden or 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
/* yt-autotoggler.js: Periodically toggles visibility of a YouTube video. | |
Usage: | |
Go to video "Details" page and run this script. Requires pop-ups. | |
To stop, focus on main page and press Escape. | |
When running, the main page header should turn violet. | |
When stopped, the headers should turn light blue. | |
If the main page is hidden in a tab, the automation may run slower. | |
If `baseInterval` is less than 4 seconds, YouTube will rate limit with 429 RESOURCE_EXHAUSTED errors. | |
*/ | |
async function run() { | |
const baseInterval = 1000 * 60 * 30 // 30 minutes | |
const jitter = 1000 * 60 * 5 // +- 5 minutes | |
const errorTimeout = 1000 * 60 // something failed to load | |
const offlineTimeout = 1000 * 5 | |
const updateFailTimeout = 1000 * 5 // yellow eye warning | |
const timeout = baseInterval + jitter * (Math.random() * 2 - 1) | |
let win, div | |
let cancelled = false | |
window.addEventListener('keydown', onKeydown, { once: true }) | |
// keep attempting to toggle until successful | |
while (true) { | |
await waitUntilOnline() | |
if (win) { | |
win.close() | |
} | |
// open a duplicate window of this page | |
const openMessage = `[${new Date().toLocaleString()}] Opened!` | |
win = window.open(window.location.href, '_blank', 'toolbar=0,menubar=0,width=900,height=700') | |
if (!win) { | |
alert("yt-autotoggler.js requires pop-ups to be allowed!") | |
return | |
} | |
// indicate script is running by turning header violet | |
document.body.style.setProperty("--ytcp-background-color", "violet") | |
try { | |
// wait until it loads | |
await new Promise((resolve, reject) => { | |
win.onload = resolve | |
setTimeout(() => reject(new Error("Could not load (offline?)")), 1000 * 15) // timeout fail load after 15 seconds | |
}) | |
// add a note when window was opened | |
div = win.document.body.appendChild(win.document.createElement('div')) | |
div.style.position = "fixed" | |
div.style.backgroundColor = 'rgba(255,230,255,0.9)' | |
div.innerHTML = openMessage | |
div.innerHTML += '<br/>' + `[${new Date().toLocaleString()}] Loaded!` | |
console.info(`[${new Date().toLocaleString()}] Toggling visibility!`) | |
const privateSuccess = await setVisibility('PRIVATE', win) | |
if (!privateSuccess) { | |
console.warn(`[${new Date().toLocaleString()}] Failed to make video private! Trying again in ${(updateFailTimeout/1000).toFixed()} seconds`) | |
await cancellableSleep(updateFailTimeout) | |
continue | |
} | |
const publicSuccess = await setVisibility('PUBLIC', win) | |
if (!publicSuccess) { | |
console.warn(`[${new Date().toLocaleString()}] Failed to make video public! Trying again in ${(updateFailTimeout/1000).toFixed()} seconds`) | |
await cancellableSleep(updateFailTimeout) | |
continue | |
} | |
break // success, exit loop | |
} catch (e) { | |
console.error(e) | |
const errorMessage = `[${new Date().toLocaleString()}] Failed to toggle visibility! Trying again at ${new Date(Date.now() + errorTimeout).toLocaleTimeString()}` | |
if (div) { | |
div.innerHTML += '<br/>' + errorMessage | |
} | |
console.warn(errorMessage) | |
await cancellableSleep(errorTimeout) | |
} | |
} | |
let successMessage = `[${new Date().toLocaleString()}] Toggled!` | |
if (!cancelled) { | |
successMessage += ` Next toggle at ${new Date(Date.now() + timeout).toLocaleTimeString()}` | |
} | |
div.innerHTML += '<br/>' + successMessage | |
console.info(successMessage) | |
await cancellableSleep(timeout) | |
await waitUntilOnline() | |
win.close() | |
window.removeEventListener('keydown', onKeydown) | |
run() | |
function onKeydown(e) { | |
if (e.key === 'Escape') { | |
console.info(`[${new Date().toLocaleString()}] Cancel!`) | |
document.body.style.setProperty("--ytcp-background-color", "lightblue") | |
cancelled = true | |
if (win) { | |
win.document.body.style.setProperty("--ytcp-background-color", "lightblue") | |
} | |
} | |
} | |
function cancellableSleep(t = 0) { | |
return new Promise((resolve) => setTimeout(() => { | |
if (!cancelled) { | |
resolve() | |
} | |
}, t)) | |
} | |
async function waitUntilOnline() { | |
let notifiedOffline = false | |
while (!navigator.onLine) { | |
if (!notifiedOffline) { | |
console.warn(`[${new Date().toLocaleString()}] Offline!`) | |
notifiedOffline = true | |
} | |
await cancellableSleep(offlineTimeout) // poll until online again | |
} | |
} | |
} | |
async function setVisibility(status, win) { | |
let visibilityOptionsContainer = win.document.querySelector("ytcp-video-metadata-visibility") | |
// if (Math.random() < 0.5) { throw new Error("Chaos Monkey Testing") } | |
// sometimes visibility options don't get opened so poll for it | |
const attempts = 100 | |
for (let i = 0; i <= attempts; i++) { | |
// open visibility options | |
visibilityOptionsContainer.firstElementChild.click() | |
await sleep() | |
// click Public/Private radio button | |
const radioButton = win.document.querySelector(`paper-radio-button[name=${status}]`) | |
if (radioButton) { | |
radioButton.click() | |
break | |
} else { | |
if (i === attempts) { | |
throw new Error("Failed to select Public/Private radio button!") | |
} | |
await sleep(200) | |
} | |
} | |
await sleep() | |
// close visibility options | |
win.document.querySelector("#save-button").click() | |
await sleep() | |
// click "SAVE" to save changes to video | |
win.document.querySelector("ytcp-button#save").click() | |
await sleep(200) | |
// while saving, editor becomes disabled with "scrim" css class | |
// poll until editor becomes enabled again, which means changes have been saved | |
for (let i = 0; i <= attempts; i++) { | |
if (i === attempts) { | |
throw new Error("Failed to update visibility! (Are you offline?)") | |
} | |
if (win.document.querySelector("#edit > ytcp-video-metadata-editor > div").classList.contains("scrim")) { | |
await sleep(200) | |
} else { | |
break | |
} | |
} | |
// sometimes the update fails with the following message: | |
// Some data is currently unavailable for this video. As a result, this status may be inaccurate. | |
// we can detect it and return fail | |
if (visibilityOptionsContainer.nextElementSibling.textContent !== "") { | |
return false | |
} | |
console.info(`[${new Date().toLocaleString()}] Set visibility to ${status}`) | |
return true | |
} | |
function sleep(t = 0) { | |
return new Promise((resolve) => setTimeout(resolve, t)) | |
} | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment