Skip to content

Instantly share code, notes, and snippets.

@conundrumer
Last active April 16, 2020 16:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save conundrumer/0cd01dc60f0b129e4cdc148a6e9606af to your computer and use it in GitHub Desktop.
Save conundrumer/0cd01dc60f0b129e4cdc148a6e9606af to your computer and use it in GitHub Desktop.
Periodically toggles visibility of a YouTube video.
/* 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