Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DinoChiesa/e33b13b81178dccc6e48cba01dea15d3 to your computer and use it in GitHub Desktop.
Save DinoChiesa/e33b13b81178dccc6e48cba01dea15d3 to your computer and use it in GitHub Desktop.
Tampermonkey script that removes watch history of youtube shorts
// ==UserScript==
// @name autodelete-Youtube-watch-history-shorts - youtube.com
// @namespace youtube
// @description automatically deletes watch history of shorts.
// @match https://myactivity.google.com/product/youtube/
// @grant none
// @version 0.1.0
// @run-at document-end
// @license Apache 2.0
// ==/UserScript==
/* jshint esversion:9 */
(function () {
"use strict";
const DELAY_AFTER_PAGE_LOAD = 1780;
const timerControl = {};
const log = function () {
const origArguments = Array.prototype.slice.call(arguments);
origArguments[0] = "[watch-history] " + origArguments[0];
Function.prototype.apply.apply(console.log, [console, origArguments]);
};
function waitForPredicate(predicate, action, uniquifier) {
uniquifier = uniquifier || Math.random().toString(36).substring(2, 15);
const interval = timerControl[uniquifier],
found = predicate();
if (found) {
action(found);
if (interval) {
clearInterval(interval);
delete timerControl[uniquifier];
}
} else {
if (!interval) {
timerControl[uniquifier] = setInterval(function () {
waitForPredicate(predicate, action, uniquifier);
}, 300);
}
}
}
const DURATION_THRESHOLD_MS = 1000 * 60 * 1.5;
let CHECK_FOR_CONFIRM_INTERVAL = 2800;
// Amount of time in MS to wait between deletion
let CYCLE_INTERVAL = 3200;
const LONG_WAIT_CYCLE_ORIGINAL_INTERVAL = 15000;
let LONGWAIT = LONG_WAIT_CYCLE_ORIGINAL_INTERVAL; // time to wait after exhausting all videos
let wantCycling = true;
const VERY_LONG_DURATION = DURATION_THRESHOLD_MS * 10;
let itemGetter = null;
// a list of items already deleted
let 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 duration, maybe it's an ad.
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 getDescriptors = (videoElement) =>
[...videoElement.getElementsByTagName('a')].map(anchor => anchor.textContent.trim());
const vidUniquifier = (videoName, channelName) => `${videoName}|${channelName}`;
const isPreviouslyRemoved = (videoElement) => {
const [videoName, channelName] = getDescriptors(videoElement);
return alreadyRemoved.includes(vidUniquifier(videoName, channelName));
};
const isAd = (videoElement) => // no duration means ad
videoElement.querySelector('[aria-label="Video duration"]') == null;
const isShort = (videoElement) => {
try {
//debugger;
return duration(videoElement) < DURATION_THRESHOLD_MS;
} catch(e){
log(`Exception while examining video: ${e}`);
return false;
}
};
function deleteOne(ignorePrevious) {
const nextItem = itemGetter.next(ignorePrevious);
if (nextItem) {
try {
const [videoName, channelName] = getDescriptors(nextItem),
dString = durationString(nextItem) || "-no duration-";
log(`deleteOne: Delete: ${videoName} by ${channelName} (${dString})...`);
const deleteButton = nextItem.getElementsByTagName('button')[0];
//console.log(`deleteOne: click...`);
deleteButton.click();
alreadyRemoved.push(vidUniquifier(videoName, channelName));
} catch(e){
log(`deleteOne: while examining, exc: ${e}`);
}
if (wantCycling) {
setTimeout(() => {
// For the FIRST video, the YT UI may pop up a dialog that the user must
// click through, to confirm the delete. Deletion of subsequent videos
// does not cause the confirmation experience to pop-up.
// Get the next delete button on the page & click it
const confirmationMenu = nextItem.querySelector('[aria-label="Activity options menu"]');
if (confirmationMenu) {
const confirmDeleteButton = confirmationMenu.querySelector('[aria-label="Delete activity item"]');
if ( confirmDeleteButton) {
confirmDeleteButton.click();
}
setTimeout(deleteOne, CYCLE_INTERVAL);
}
else {
// wait a bit, and look again
setTimeout(deleteOne, CYCLE_INTERVAL - CHECK_FOR_CONFIRM_INTERVAL);
}
}, CHECK_FOR_CONFIRM_INTERVAL);
}
LONGWAIT = LONG_WAIT_CYCLE_ORIGINAL_INTERVAL;
}
else {
log(`deleteOne: no item found...starting long wait.`);
setTimeout(() => deleteOne(true), LONGWAIT);
LONGWAIT *= 1.5; // exponential backoff for next time
}
}
class ItemGetter {
previousCount = 0;
constructor() {
}
next(ignorePrevious) {
const items = Array.from(document.querySelectorAll('div[role="listitem"]'));
if ( ! ignorePrevious && items.length == this.previousCount) {
// Success of the delete wil remove the item. Getting the same number of items
// means that the prior delete did not succeed. Which is probably because
// deleting is incurring 429, too many requests. So the caller probably needs to wait a bit.
log(`next: same count...`);
return null;
}
const ads = items.filter(isAd),
shorts = items.filter(isShort);
log(`next: ${items.length} videos, ${ads.length} ads, ${shorts.length} shorts...`);
this.previousCount = items.length;
// possibly null
return items.find((v) => (isAd(v) || isShort(v)) && !isPreviouslyRemoved(v));
}
}
itemGetter = new ItemGetter();
setTimeout(function () {
log("tweak running: " + window.location.href);
waitForPredicate(() => itemGetter.next(), function (result) {
log("got first item");
// launch
deleteOne();
setTimeout(() => {
// periodically cleanup, keep only most recent
alreadyRemoved = alreadyRemoved.slice(-45);
}, 75000);
});
}, DELAY_AFTER_PAGE_LOAD);
})();
@DinoChiesa
Copy link
Author

Automate the selective deletion of items in your Youtube watch history

This TM 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. you need the tampermonkey extension installed in your browser
  2. visit https://myactivity.google.com/product/youtube
  3. that's it

It should start to run immediately. It deletes one item in your watch history, every 4+ seconds or so. You can open the console (Shift-Ctrl+I) to view log messages.

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 paste-into-the-console script? Yes - I did that here: https://gist.github.com/DinoChiesa/f3f2aa34d7d581782a0b8572fad99278 . 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. If you have no ads or shorts in the first 100 videos of your history, you may need to scroll down to allow the script to start.

  • 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.

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