Watch later: Better remove watched

Delete videos that you watch 90 or more percent (with mobile support)

// ==UserScript==
// @name        Watch later: Better remove watched
// @namespace   shiftgeist
// @icon        https://www.youtube.com/s/desktop/50798525/img/logos/favicon_144x144.png
// @match       *://*.youtube.com/*
// @grant       none
// @version     20250227.6
// @author      shiftgeist
// @description Delete videos that you watch 90 or more percent (with mobile support)
// @license     GNU GPLv3
// ==/UserScript==

(async function () {
  'use strict'

  const debug = window.localStorage.getItem('better-remove-watched-debug') === 'true'
  const threshold = Number(window.localStorage.getItem('better-remove-watched-threshold') || 90)
  const mobile = window.location.href.includes('m.youtube.com')

  const doc = window.document
  const attachmentPoint = mobile
    ? '.playlist-immersive-header-content .amsterdam-playlist-header-metadata-wrapper'
    : '.metadata-buttons-wrapper.ytd-playlist-header-renderer'

  let timeout = null
  let button = null

  function log(...params) {
    if (debug) {
      console.debug('[Clean]', ...params)
    }
  }

  function createButton() {
    if (button) {
      log('remove button first')
      button.remove()
    }

    button = document.createElement('button')
    button.textContent = 'Remove fully watched'
    button.title = 'Visible videos watched 90% or more'
    button.classList =
      'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--overlay yt-spec-button-shape-next--size-m'
    document.querySelector(attachmentPoint).appendChild(button)

    if (mobile) {
      button.style.marginTop = '8px'
    }

    button.addEventListener('click', clickHandler)
    button.addEventListener('auxclick', clickHandler)

    log('Button created')
  }

  function handleDropdownClick() {
    const parent = document.querySelector(
      mobile ? '#content-wrapper' : 'ytd-popup-container tp-yt-iron-dropdown tp-yt-paper-listbox'
    )
    log('handle dropdown click', parent)

    if (parent) {
      (mobile ? parent.children[0].querySelector('button') : parent.children[2]).click()
    } else {
      setTimeout(handleDropdownClick, 100)
    }
  }

  function removeFromWatched(video) {
    log('Removing video', video)
    video.querySelector(mobile ? 'button' : '#button').click()
    handleDropdownClick()
  }

  async function clickHandler(event) {
    log('button clicked')

    const videos = Array.from(document.querySelectorAll(mobile ? 'ytm-playlist-video-renderer' : 'ytd-playlist-video-renderer'))

    log('Found', videos.length, 'videos')

    const videosStarted = videos.filter(v => {
      const watchbar = v.querySelector(mobile ? '.thumbnail-overlay-resume-playback-progress' : '#progress')

      if (!watchbar) return false

      const percent = Number(watchbar.style.width.replace('%', ''))
      const t = v.innerText.replaceAll('\n', '')
      log(t.slice(t.indexOf('Now playing') + 11), percent)

      return percent >= threshold
    })

    log('Found', videos.length, 'watched videos')

    if (videosStarted.length > 0) {
      removeFromWatched(videosStarted[0])
      setTimeout(() => clickHandler(event), 500)
    }
  }

  let checkCount = 0

  function waitForLoad(query, callback) {
    log('wait for load')

    const search = new URLSearchParams(window.location.href)

    if (
      !(
        window.location.href.includes('youtube.com/playlist') &&
        window.location.search.includes('list=WL')
      )
    ) {
      log('Not on watch later playlist')
      return
    }

    if (checkCount > 99) {
      log('Check count > 99')
      return
    }

    if (document.querySelector(query)) {
      checkCount = 0
      callback()
    } else {
      checkCount += 1
      const waitTime = 100 * checkCount * checkCount
      log('time until check is', waitTime)
      timeout = setTimeout(() => waitForLoad(query, callback), waitTime)
    }
  }

  function init() {
    console.debug(
      '[Clean] Config: debug',
      debug,
      'threshold',
      threshold,
      'mobile',
      mobile,
      'attachmentPoint',
      attachmentPoint
    )

    waitForLoad(attachmentPoint, createButton)
  }

  function handlePageChange(event) {
    log('page change event fired', event.type, window.location.href)
    init()

    if (timeout) {
      clearTimeout(timeout)
    }
  }

  window.addEventListener('yt-page-data-updated', handlePageChange)

  init()
})()