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