Watch later: Better remove watched

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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     20250603.1
// @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 mobile = window.location.href.includes('m.youtube.com')
  const getThreshold = () => Number(window.localStorage.getItem('better-remove-watched-threshold'))

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

  let timeout = null
  let removeButton = null
  let percentButton = null

  function log(...params) {
    if (debug) {
      console.debug('[better-remove-watched]', ...params)
    }
  }

  function baseButton() {
    const button = document.createElement('button')
    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'
    return button
  }

  function createPercentButton(percent) {
    if (percentButton) {
      log('removing percent button first')
      percentButton.remove()
    }

    percent = percent || getThreshold() || 90

    percentButton = baseButton()
    percentButton.textContent = `${percent}%`
    percentButton.title = `Click -10% (remove at ${percent}% watched)`
    percentButton.dataset.percent = percent
    percentButton.addEventListener('click', minusTenPercentHandler)
    percentButton.addEventListener('auxclick', minusTenPercentHandler)
    percentButton.style.marginLeft = '8px'
    percentButton.style.flexGrow = 0
    percentButton.style.padding = '0 24px'
    percentButton.style.borderRadius = '4px 18px 18px 4px'
    document.querySelector(attachmentPoint).appendChild(percentButton)

    window.localStorage.setItem('better-remove-watched-threshold', JSON.stringify(percent))

    log('Remove button created', percentButton)
  }

  function minusTenPercentHandler() {
    const percent = Number(percentButton.dataset.percent)

    if (percent - 10 > 0) {
      createPercentButton(percent - 10)
    } else {
      createPercentButton(100)
    }
  }

  function createRemoveButton() {
    if (removeButton) {
      log('removing remove button first')
      removeButton.remove()
    }

    removeButton = baseButton()
    removeButton.textContent = 'Remove watched'
    removeButton.title = 'Visible videos watched'
    if (mobile) {
      removeButton.style.marginTop = '8px'
    }
    removeButton.style.borderRadius = '18px 4px 4px 18px'
    removeButton.addEventListener('click', e => removeHandler(e, true))
    removeButton.addEventListener('auxclick', e => removeHandler(e, true))
    document.querySelector(attachmentPoint).appendChild(removeButton)

    log('Remove button created', removeButton)
  }

  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 removeHandler(event, cursor = false) {
    log('button clicked')

    if (cursor && removeButton.textContent === 'Stop') {
      location.reload()
      return
    }

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

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

    if (videosStarted.length > 0) {
      if (removeButton.textContent !== 'Stop') {
        removeButton.textContent = 'Stop'
      }

      removeFromWatched(videosStarted[0])
      setTimeout(() => removeHandler(event), 1000)
    } else {
      // finished
      removeButton.parentElement.click()
      removeButton.textContent = 'All videos removed'
      await new Promise(res => setTimeout(res, 400))
      removeButton.textContent += '.'
      await new Promise(res => setTimeout(res, 400))
      removeButton.textContent += '.'
      await new Promise(res => setTimeout(res, 400))
      removeButton.textContent += '.'
      await new Promise(res => setTimeout(res, 400))
      removeButton.textContent = 'Remove watched'
    }
  }

  let checkCount = 0

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

    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() {
    waitForLoad(attachmentPoint, () => {
      createRemoveButton()
      createPercentButton()
    })
  }

  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()
})()