-
-
Save js6pak/33bdefdefac09c387f55d08c5b9526fa to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Youtube Playlist Cleanser | |
// @version 2.1.0 | |
// @description Removes watched videos from playlist either by %watched or all | |
// @author js6pak | |
// @include http*://*.youtube.com/* | |
// @include http*://youtube.com/* | |
// @run-at document-idle | |
// @homepageURL https://gist.github.com/js6pak/33bdefdefac09c387f55d08c5b9526fa | |
// @downloadURL https://gist.github.com/js6pak/33bdefdefac09c387f55d08c5b9526fa/raw/youtube-playlist-cleanser.user.js | |
// ==/UserScript== | |
const config = { | |
// Delete if watched more or equal to % of video | |
threshold: 80, | |
// Delay between delete requests (seems to need to be quite high for consistent results) | |
delay: 600, | |
}; | |
const app = document.querySelector("ytd-app"); | |
if (!app) return; | |
const sleep = (timeout) => new Promise((res) => setTimeout(res, timeout)); | |
function waitForElement(selector) { | |
return new Promise((resolve) => { | |
if (document.querySelector(selector)) { | |
return resolve(document.querySelector(selector)); | |
} | |
const observer = new MutationObserver(() => { | |
if (document.querySelector(selector)) { | |
resolve(document.querySelector(selector)); | |
observer.disconnect(); | |
} | |
}); | |
observer.observe(app, { | |
childList: true, | |
subtree: true, | |
}); | |
}); | |
} | |
function createButtons(menu) { | |
const cleanseButton = document.createElement("button"); | |
{ | |
cleanseButton.textContent = "Cleanse"; | |
cleanseButton.style.padding = "10px"; | |
cleanseButton.style.backgroundColor = "#181717"; | |
cleanseButton.style.color = "white"; | |
cleanseButton.style.textAlign = "center"; | |
cleanseButton.style.fontSize = "14px"; | |
cleanseButton.style.border = "0"; | |
cleanseButton.style.cursor = "pointer"; | |
cleanseButton.style.fontFamily = "Roboto, Arial, sans-serif"; | |
cleanseButton.style.borderRadius = "2px"; | |
cleanseButton.style.marginRight = "10px"; | |
cleanseButton.addEventListener("click", function () { | |
cleanse(); | |
}); | |
} | |
const deleteAllButton = document.createElement("button"); | |
{ | |
deleteAllButton.textContent = "Delete all"; | |
deleteAllButton.style.padding = "10px"; | |
deleteAllButton.style.backgroundColor = "#ff0000"; | |
deleteAllButton.style.color = "white"; | |
deleteAllButton.style.textAlign = "center"; | |
deleteAllButton.style.fontSize = "14px"; | |
deleteAllButton.style.border = "0"; | |
deleteAllButton.style.cursor = "pointer"; | |
deleteAllButton.style.fontFamily = "Roboto, Arial, sans-serif"; | |
deleteAllButton.style.marginRight = "10px"; | |
deleteAllButton.addEventListener("click", function () { | |
cleanse(true); | |
}); | |
} | |
menu.prepend(cleanseButton, deleteAllButton); | |
} | |
function* getVideos() { | |
const videos = document.querySelectorAll("ytd-playlist-video-renderer"); | |
for (const video of videos) { | |
const title = video.querySelector("#video-title").innerText; | |
const progress = video.querySelector("ytd-thumbnail-overlay-resume-playback-renderer")?.data.percentDurationWatched ?? 0; | |
const menu = video.querySelector("ytd-menu-renderer"); | |
const menuButton = menu.querySelector("yt-icon-button#button"); | |
yield { | |
container: video, | |
title, | |
progress, | |
menu, | |
menuButton, | |
}; | |
} | |
} | |
async function deleteVideo(video) { | |
video.menuButton.click(); | |
const popup = await waitForElement("ytd-menu-popup-renderer"); | |
Array.from(popup.querySelectorAll("ytd-menu-service-item-renderer")) | |
.find((x) => x.icon === "DELETE") | |
.click(); | |
await sleep(config.delay); | |
} | |
async function cleanse(deleteAll = false) { | |
console.log("Cleansing..."); | |
let deletedCount = 0; | |
for (const video of getVideos()) { | |
console.log(` ${video.title} (${video.progress}%)`); | |
if (deleteAll || video.progress >= config.threshold) { | |
console.log(" Deleting..."); | |
await deleteVideo(video); | |
deletedCount++; | |
} else { | |
console.log(" Skipping because its under threshold"); | |
} | |
} | |
console.log(`Done! Deleted ${deletedCount} videos`); | |
} | |
waitForElement("ytd-playlist-header-renderer ytd-menu-renderer").then((menu) => { | |
createButtons(menu); | |
}); | |
function createQuickDeleteButtons() { | |
for (const video of getVideos()) { | |
const quickDeleteButton = document.createElement("yt-icon-button"); | |
quickDeleteButton.className = "style-scope ytd-menu-renderer"; | |
quickDeleteButton.setAttribute("style-target", "button"); | |
quickDeleteButton.style.marginRight = "10px"; | |
video.menu.insertBefore(quickDeleteButton, video.menuButton); | |
const deleteIcon = document.createElement("yt-icon"); | |
deleteIcon.className = "style-scope ytd-menu-renderer"; | |
deleteIcon.icon = "DELETE"; | |
deleteIcon.style.color = "#F44336"; | |
quickDeleteButton.querySelector("#button").appendChild(deleteIcon); | |
quickDeleteButton.addEventListener("click", function () { | |
console.log("Quick deleting " + video.title); | |
deleteVideo(video); | |
}); | |
} | |
} | |
waitForElement("ytd-playlist-video-renderer ytd-menu-renderer").then(() => { | |
createQuickDeleteButtons(); | |
}); |
@LosAlamosAl It still works just fine for me on Brave + Violentmonkey.
What are you using to run the script?
Did this ever work for you before? If so then there might be some YouTube A/B testing breaking it.
@js6pak First, my bad. I was trying to run this from the Javascript console in Chrome. I had not heard of user scripts. Your Violentmonkey reference turned me on to these. Thanks! Installed the script into Chrome using Violentmonkey. I run either cleanse or delete and it stops after less than 100 videos. I continue to see "Removed from watch later" pop up, but displayed count and thumbnails cease to update. I revisit page and see that deletion had indeed stopped.
Anyway, thanks for responding and enlightening me on user scripts. My ire rests entirely with Google and YouTube. I may try another browser. If I get it working, I'll post here. Thanks for the rapid reply.
I just had over 4,800 Watch Later in my playlist and was wondering if there was a way to what jackcarey did from July 23, 2023. "I modified this to also remove videos published over X days old, and those that are private or deleted. This currently relies on the language though (English). It could be updated to look for 'no_thumbnail' in the thumbnail source though". This looks very nice and I've also never had a script extension in my browser before either. I'll just do Date Added ( Oldest ) to make it the easiest, if I delete ones I don't want then oh well. Thanks for your time.
This script quit working for me recently. I no longer had the Delete and Cleanse buttons appearing on my playlists.
I'm not a JavaScript programmer, but I took a stab at the code. I modified line 142 as follows:
original: waitForElement("ytd-playlist-header-renderer ytd-menu-renderer").then((menu) => {
changed to: waitForElement("ytd-item-section-renderer ytd-menu-renderer").then((menu) => {
Now the buttons appeared in the first item displayed, but they also appeared anywhere there was a list of YouTube videos, so I also changed the include scope comments at the top of the code, lines 6 and 7, as follows:
original:
// @include http*://*.youtube.com/*
// @include http*://youtube.com/*
changed to:
// @include http*://*.youtube.com/playlist*
// @include http*://youtube.com/playlist*
I'll leave it to someone more experienced in JavaScript than I am to do a proper fix if necessary.
Not working. Illegal return at
if (!app) return;
I'm running Chrome version 120.0.6099.71 (Official Build) (arm64) on MacOS 14.2