// ==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); })();