Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Last active April 14, 2024 17:06
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 DinoChiesa/f3f2aa34d7d581782a0b8572fad99278 to your computer and use it in GitHub Desktop.
Save DinoChiesa/f3f2aa34d7d581782a0b8572fad99278 to your computer and use it in GitHub Desktop.
Remove video shorts from YT Watch History
// orig: https://gist.github.com/miketromba/334282421c4784d7d9a191ca25095c09
// Paste the script into your console on this page: https://myactivity.google.com/product/youtube
const ENABLED = true;
const MIN_DURATION_MS = 1000 * 60 * 1.5; // 1:30 mins
const CHECK_FOR_CONFIRM_INTERVAL = 2000;
let CYCLE_INTERVAL = 1800; // Amount of time in MS to wait between deletion (more likely to fail with a small number)
let wantCycling = true;
const VERY_LONG_DURATION = MIN_DURATION_MS * 10;
const alreadyRemoved = [];
const durationString= (vidElement) => {
const elt = vidElement.querySelector('[aria-label="Video duration"]'),
tc = elt && elt.textContent;
return tc && tc.trim();
};
const duration = (vidElement) => {
const dString = durationString(vidElement);
if ( ! dString) {
// unknown, something else is wrong
return VERY_LONG_DURATION;
}
if(dString.split(':').length > 2) { // The video is > 1hr long
return VERY_LONG_DURATION;
}
// less than an hour
let [mins, secs] = dString.split(':');
if ( ! mins || !secs){
return VERY_LONG_DURATION;
}
[mins, secs] = [mins, secs].map(stringNum => parseInt(stringNum, 10));
const durationMS = (mins * 60 * 1000) + (secs * 1000);
return durationMS;
};
const getIdentifiers = (videoElement) =>
[...videoElement.getElementsByTagName('a')].map(anchor => anchor.textContent.trim());
const vidUniquifier = (videoName, channelName) => `${videoName}|${channelName}`;
const isPreviouslyRemoved = (videoElement) => {
const [videoName, channelName] = getIdentifiers(videoElement);
return alreadyRemoved.includes(vidUniquifier(videoName, channelName));
};
const isShort = (videoElement) => {
try {
return duration(videoElement) < MIN_DURATION_MS;
} catch(e){
console.log(`Exception while examining video: ${e}`);
return false;
}
}
async function deleteNext() {
// Extract next item from page
const nextItem = await getNextItem();
if (nextItem) {
// Find name and author of video & log it to console
try {
const [videoName, channelName] = getIdentifiers(nextItem);
const dString = durationString(nextItem);
console.log(`deleteNext(): Will delete: ${videoName} by ${channelName} (${dString})...`);
// Find the next menu button for the item & click it
if (ENABLED) {
const deleteButton = nextItem.getElementsByTagName('button')[0];
console.log(`deleteNext: DELETE: click...`);
deleteButton.click();
alreadyRemoved.push(vidUniquifier(videoName, channelName));
}
else {
console.log(`deleteNext: DELETE: NOT CLICKing...`);
}
} catch(e){
console.log(`deleteNext(): while examining, exc: ${e}`);
}
}
else {
console.log(`deleteNext(): no item found.`);
}
if (wantCycling) {
// Wait [DELETE_INTERVAL] ms
setTimeout(() => {
// For the FIRST video, the YT UI pops up a dialog that the user must click through,
// to confirm the delete. Subsequent videos do not require this.
// Get the next delete button on the page & click it
const nextMenu = nextItem.querySelector('[aria-label="Activity options menu"]');
if (nextMenu) {
const nextDeleteButton = nextMenu.querySelector('[aria-label="Delete activity item"]');
if ( nextDeleteButton) {
nextDeleteButton.click();
}
setTimeout(deleteNext, CYCLE_INTERVAL);
}
else {
// wait a bit, and look again
setTimeout(deleteNext, CYCLE_INTERVAL - CHECK_FOR_CONFIRM_INTERVAL);
}
}, CHECK_FOR_CONFIRM_INTERVAL);
}
}
function getNextItem() {
const items = document.querySelectorAll('div[role="listitem"]');
console.log(`Found ${items.length} candidate items...`);
const nextItemToDelete = Array.from(items).find((v) => isShort(v) && !isPreviouslyRemoved(v));
return nextItemToDelete; // possibly null
}
// This will start up the script
deleteNext()
@DinoChiesa
Copy link
Author

DinoChiesa commented Apr 13, 2024

Automate the selective deletion of items in your Youtube watch history

This script will enable you to automate the deletion selected videos from your YouTube watch history. The logic here will delete either ads, or shorts that run less than 1:30. If you are a coder, you can adjust the filter to select other items to delete.

There is no API, as far as I can tell, in the YT Data API, to allow deletion of items in the watch history. That part of the YT Data API was removed in 2016 or 2017. That means there is no "blessed" way of building a program to manage your own YT watch history. There may be a separate API published by Google for the "myactivity" page, which covers YT, web and app activity, location activity, and maybe more (not sure). But I didn't look into that. Lacking an official API, this script can help do what you might want.

To use this script

  1. visit https://myactivity.google.com/product/youtube
  2. open the browser developer tools (open it with Shift+Ctrl+I)
  3. paste the code here into the console.

It should start to run immediately. It deletes one item in your watch history, every 4+ seconds or so.

Why would you want to do this?

If you have indulged in watching a long series of funny TikTok-style shorts, that can cause your YouTube feed to promote more shorts. If you want to avoid that, you may want to remove a bunch of your history. But not the long videos!

FAQs

  • Can you make it a Tampermonkey script? Yes - I did that here: https://gist.github.com/DinoChiesa/e33b13b81178dccc6e48cba01dea15d3 . You can use either approach.

  • Can you implement some other selection filters, like keyword filters? I could, but I'm leaving that as an exercise for you.

  • Why does it wait a few seconds between each delete? The YT UI inserts a user experience to notify the user that a delete is occurring, and even gives the user a chance to cancel the delete. This script waits a few seconds in order to allow that experience to occur.

  • The YT UI shows only 100 videos. Will the script need to be restarted after it deletes all shorts in the first 100 displayed? No. The page for YT is designed to reload when only a few videos are displayed. This brings more videos into the list, which this script can then scan as candidates for deletion.

  • Why did you make the script also delete ads? I don't know, I just didn't want the list of ads cluttering my watch history.

  • When I turn off "ENABLED" the script doesn't run properly. Why? I didn't test that part thoroughly. I didn't care too much about it, so I neglected it.

Inspired by and Derived from https://gist.github.com/miketromba/334282421c4784d7d9a191ca25095c09

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment