Skip to content

Instantly share code, notes, and snippets.

@qoomon
Last active August 1, 2025 23:32
Show Gist options
  • Select an option

  • Save qoomon/a3f71ff2b2a18ee297c9435d539d5247 to your computer and use it in GitHub Desktop.

Select an option

Save qoomon/a3f71ff2b2a18ee297c9435d539d5247 to your computer and use it in GitHub Desktop.
Clean YouTube Watch Later Videos
// Version 2.0.1
// This script will remove all videos from watch later list
//
// Usage
//
// #1 go to https://www.youtube.com/playlist?list=WL
// #2 run following script in your browser console
(async function() {
const playlistName = document.querySelector('.metadata-wrapper #container #text')?.textContent || document.querySelector('#text')?.textContent
if(!playlistName) {
alert(`Couldn't determine playlist name!`)
return
}
if(!confirm(`Are you sure to delete ALL videos from ${playlistName}?`)) {
return
}
console.info("start...")
while(true) {
const videos = document.querySelectorAll('#primary ytd-playlist-video-renderer')
if(videos.length == 0) break
for (let videoElement of videos) {
const videoTitle = videoElement.querySelector('a#video-title')
console.info(`Remove Video\n`
+ ` Title: ${videoTitle.innerText}\n`
+ ` URL: ${videoTitle.href}`)
const actionMenuButton = videoElement.querySelector('#menu #button')
console.debug("click actionMenuButton", actionMenuButton)
actionMenuButton.click()
const removeButton = await untilDefined(() => document.evaluate(
`//tp-yt-paper-listbox/ytd-menu-service-item-renderer[./tp-yt-paper-item/yt-formatted-string/span[text() = '${playlistName}']]`,
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue)
console.debug("click removeButton", removeButton)
removeButton.click()
await sleep(200)
}
}
console.info("done!")
// === util functions ========================================================
async function sleep (timeout) {
return new Promise(res => setTimeout(res, timeout))
}
async function untilDefined(factory, checkInterval = 100) {
while (true) {
const value = factory()
if (value != null) return value
await sleep(checkInterval)
}
}
})();
@chekoopa

Copy link
Copy Markdown

Do you know how to deal with 400 error? It seems to work, only to fall into chain of errors, even though videos disappear, they actually aren't deleted and are shown again after a refresh.

@qoomon

qoomon commented Aug 30, 2021

Copy link
Copy Markdown
Author

@chekoopa unfortunately I've no idea. This script just simulate clicks so I have no influence on the requests itself.

@xiaolitongxue666

Copy link
Copy Markdown

Thanks a lot , if you web is in Chinese , just add

	'zh-Hans-CN': '从稍后观看中移除',

It's works ~

@qoomon

qoomon commented Sep 23, 2021

Copy link
Copy Markdown
Author

@xiaolitongxue666 I'll add your translation entry

@xiaolitongxue666

Copy link
Copy Markdown

@xiaolitongxue666 I'll add your translation entry

OK 👌 !~

@dextersnp

Copy link
Copy Markdown

Very nice! Works like a charm (pt-br), very very tks!

@sandorex

sandorex commented Feb 8, 2022

Copy link
Copy Markdown

I made your script into tapermonkey/violentmonkey userscript, use it if you want but updating will currently break it as it will be overwritten by the current gist that is not a valid userscript

I was lazy to add checks if the user is at the proper webpage, TODO i guess?

Details
// ==UserScript==
// @name        Clean Youtube Watch Later
// @homepageURL https://gist.github.com/qoomon/a3f71ff2b2a18ee297c9435d539d5247
// @downloadURL https://gist.github.com/qoomon/a3f71ff2b2a18ee297c9435d539d5247/raw/7784b40026cadf3d2ede89358fd554e298c17bd1/youtube_clean_watch_later_videos.js
// @match       *://https://www.youtube.com/playlist
// @grant       GM_registerMenuCommand
// @noframes
// @version     1.0
// @author      qoomon
// @description Removes all videos from your youtube watch later playlist
// ==/UserScript==
//
// Script adapted into userscript by Sandorex
//

GM_registerMenuCommand('Clean Watch Later', function() {
  if (!confirm("This will erase your playlist permanently, are you sure you want to proceed?"))
    return;
  
  // FOR STANDALONE USE COPY THIS BLOCK
  (async function() {
    const REMOVE_BUTTON_TEXT_MAP = {
      'en': 'Remove from Watch later',
      'en-GB': 'Remove from Watch later',
      'de-DE': 'Aus "Später ansehen" entfernen',
      'pt-BR': 'Remover de Assistir mais tarde',
      'zh-Hans-CN': '从稍后观看中移除',
    }

    const sleep = (timeout) => new Promise(res => setTimeout(res, timeout))
    const untilDefined = async (factory, timeout = 100) => {
      while (true) {
        let value = factory()
        if (value != null) return value
        await sleep(timeout)
      }
    }

    console.info("start...")
    while(true) {
      let videos = document.querySelectorAll('#primary ytd-playlist-video-renderer')
      if(videos.length == 0) break

      for (let videoElement of videos) {
        let videoTitle = videoElement.querySelector('a#video-title')
        console.info("remove: " + videoTitle.innerText)
        console.info("        " + videoTitle.href)

        let actionMenu = videoElement.querySelector('#menu')
        let actionMenuButton = actionMenu.querySelector('#button')

        console.debug("click actionMenuButton", actionMenuButton)
        actionMenuButton.click()
        let languageCode = document.documentElement.lang
        let removeButtonText = REMOVE_BUTTON_TEXT_MAP[languageCode] || REMOVE_BUTTON_TEXT_MAP[languageCode.split('-')[0]]
        let removeButton = await untilDefined(() => document.evaluate(
          `//ytd-popup-container/tp-yt-iron-dropdown//tp-yt-paper-item[contains(., "${removeButtonText}")]`,
          document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue);

        console.debug("click removeButton", removeButton)
        removeButton.click()
        await sleep(200)
      }
    } 
    console.info("done!")
  })();
  // FOR STANDALONE USE COPY THIS BLOCK
})

Also i think this script could be adapted to work on any playlist without too much effort but that may be going too far, i may make a separate version that does that if i need it

EDIT: I did make it if anyone is interested, it should work for all playlists and languages but i haven't tested anything but english

@qoomon

qoomon commented Feb 8, 2022

Copy link
Copy Markdown
Author

@sandorex thanks a lot I"ll have a look.

@imapkru

imapkru commented Feb 27, 2022

Copy link
Copy Markdown

Thank you so much for this code. It worked like a genius. I tried to modify it to delete "Liked videos" by replacing watch later with liked videos. It appears to be running but my total liked videos still show as 5000. Any suggestions?

@idadzie

idadzie commented Feb 28, 2022

Copy link
Copy Markdown

@qoomon Thanks for this script. It works perfectly. 👍🏽

@qoomon

qoomon commented Feb 28, 2022

Copy link
Copy Markdown
Author

@imapkru you need to replace "Remove from Watch Later" with "Remove from Liked videos" e.g.

const REMOVE_BUTTON_TEXT_MAP = {
    'en': 'Remove from Liked videos'
  }

and run script at https://www.youtube.com/playlist?list=LL

@imapkru

imapkru commented Mar 1, 2022 via email

Copy link
Copy Markdown

@Hanan-Neor

Copy link
Copy Markdown

For me 'en': 'Remove from Watch later', with lowercase ' L ' instead of 'Watch Later' warked. (just use the written text of the button)

@qoomon

qoomon commented Mar 3, 2022

Copy link
Copy Markdown
Author

This script is now case-insensitive 💯

@Mattia-Zanini

Copy link
Copy Markdown

I got this error:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'click')
at :45:20

@qoomon

qoomon commented Mar 14, 2022

Copy link
Copy Markdown
Author

@Mattia-Zanini should be fixed.

@Mattia-Zanini

Mattia-Zanini commented Mar 14, 2022

Copy link
Copy Markdown

I'm an idiot, because i forgot to change REMOVE_BUTTON_TEXT, this code doesn't create any error, it's perfect, thx

@SteffeSteffe1988

Copy link
Copy Markdown

So i tried adding a line for Swedish:

// swe-SE: 'Ta bort från Titta senare'

But all I'm getting is "wait for removeButton VM229:48" a few thousand times over and over.

@qoomon

qoomon commented Mar 31, 2022

Copy link
Copy Markdown
Author

@SteffeSteffe1988 you need to replace following line

  • const REMOVE_BUTTON_TEXT = 'Remove from Watch later' // en with
    with
  • const REMOVE_BUTTON_TEXT = 'Ta bort från Titta senare' // swe-SE

@SteffeSteffe1988

Copy link
Copy Markdown

@SteffeSteffe1988 you need to replace following line

  • const REMOVE_BUTTON_TEXT = 'Remove from Watch later' // en with
    with
  • const REMOVE_BUTTON_TEXT = 'Ta bort från Titta senare' // swe-SE

Of course I missed something that obvious.
Thank you! Now it works great for people using Sweish in Chrome!

@qoomon

qoomon commented Mar 31, 2022

Copy link
Copy Markdown
Author

@SteffeSteffe1988 Glad to hear that, I'll add Swedish values as coment

@udovichenko

Copy link
Copy Markdown

Thank you so much, still works in 2023!
This is my fork with prompting the limit of videos to be deleted and randomized delay:
https://gist.github.com/udovichenko/295357dc6b504e4ce3dd540ad6c368a4

@othyn

othyn commented Apr 28, 2023

Copy link
Copy Markdown

YouTube appear to have changed the title document selector for me, so the script breaks when trying to locate the playlist title on line 16:

const playlistName = document.querySelector("#title a").text

Looking at the latest commits in sandorex's fork we can see the new selector that needs to be used as YouTube have changed things around.

Replacing it with something like the following should work, so replace this:

  const playlistName = document.querySelector("#title a").text

  if(!confirm(`Are you sure to delete ALL videos from ${playlistName}?`)) {
    return
  }

with this:

  let playlistName = document.querySelector('.metadata-wrapper #container #text')?.textContent || null;

  let confirmationText = 'Are you sure to delete ALL videos from ' + (!playlistName ? 'this playlist' : playlistName) + '?';

  if (!confirm(confirmationText)) {
    return
  }

I still don't get why they don't add this functionality into the native YouTube experience, its been years and it must be a really common issue once playlists fill up.

@qoomon

qoomon commented Apr 28, 2023

Copy link
Copy Markdown
Author

thx @othyn, I fixed it

@sandorex

sandorex commented May 1, 2023

Copy link
Copy Markdown

I've been testing my fork for few months that does not require lang specific options and modifications, could use more people trying it, also it's a Violentmonkey script
https://gist.github.com/sandorex/6bdd51ef467a079c87f19ba469a8bc7c
Feel free to take the code if you want

@qoomon

qoomon commented May 2, 2023

Copy link
Copy Markdown
Author

@sandorex thanks a lot, I've updated my script to your provided solution to get rid of language specific configuration.

@sandorex

sandorex commented Aug 8, 2023

Copy link
Copy Markdown

It seems youtube did something so the script broke a bit, change

const playlistName = document.querySelector('.metadata-wrapper #container #text')?.textContent

to

const playlistName = document.querySelector('#text')?.textContent

The id stayed the same for a while now so im hopeful it wont need more fixing for a while 😄

@qoomon

qoomon commented Aug 9, 2023

Copy link
Copy Markdown
Author

@sandorex thx a lot, however for me it' works as expected with document.querySelector('.metadata-wrapper #container #text')?.textContent

@sandorex

Copy link
Copy Markdown

Weird it works for me now too, it did not work yesterday for some reason

@sutkovyi

sutkovyi commented Apr 3, 2025

Copy link
Copy Markdown

thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment