Last active
April 26, 2024 12:41
-
-
Save js6pak/33bdefdefac09c387f55d08c5b9526fa to your computer and use it in GitHub Desktop.
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
// ==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(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.