Watch later: Better remove watched

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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