Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save astamicu/eb351ce10451f1a51b71a1287d36880f to your computer and use it in GitHub Desktop.
Save astamicu/eb351ce10451f1a51b71a1287d36880f to your computer and use it in GitHub Desktop.
Script to remove all videos from Youtube Watch Later playlist

UPDATED 22.11.2022

It's been two years since the last update, so here's the updated working script as per the comments below.

Thanks to BryanHaley for this.

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Action menu"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

Non-english users will need to change "Action menu" and "Remove from" to what YouTube uses for their localization.

@bryancasler
Copy link

bryancasler commented Oct 7, 2021

Just discovered a native way to remove ALL watched videos!

I had 5,000 videos in my watch later playlist and it took about 10 seconds before it was down to 1,675 videos; I refreshed the page after 10 seconds in excitement. When I tested it again with only a handful of watched videos it did it within a few seconds and no manual refresh was necessary.

  • Go here on desktop: https://www.youtube.com/playlist?list=WL
  • Click the three horizontal dots under "Watch later" and next to the "Shuffle Play" icon
  • Click "Remove watched videos"
  • Click "Remove" on the popup to confirm their removal

@jzisser9
Copy link

jzisser9 commented Oct 7, 2021

Hi Bryan,

Thanks for your response, however two problems with this approach:

  1. Its availability is inconsistent. It's available in the browser on desktop, and sometimes on mobile, but not always - either that or they took the feature down there.
  2. It only removes videos you've already watched. This thread is about removing all videos from the WL playlist.

@LucasPlacentino
Copy link

@blackmailer75 This worked on 2021.10.16 for deleting all of my remaining 400 "Unavailable" or "Deleted" videos from my Watch Later playlist. Thank you!

Unfortunately, this way don't work in my case. After clearing all "normal" videos from WH list I use following script to delete Deleted and Private videos: setInterval(function() { document.querySelector('#primary button[class="style-scope yt-icon-button"]').click(); var things = document.evaluate('//span[contains(text(),"Remove from")]',document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null); for (var i = 0; i < things.snapshotLength; i++) { things.snapshotItem(i).click(); } }, 1000);

@bryancasler
Copy link

bryancasler commented Oct 19, 2021

@jzisser9 I only have one phone to test, but it has shown up 100% of the time on the mobile Youtube app, not sure about the Youtube web app. And yes, you're right it only removes watched videos but still, could be a HUGE time saver before running any of these scripts.

@sabafr001
Copy link

Works well! :)

me too it works

@p-r-i-t-h-v-i
Copy link

p-r-i-t-h-v-i commented Nov 2, 2021

Yeah, the script above works but there's also a better idea which is https://myaccount.google.com/u/2/youtubeoptions?rapt=AEjHL4O3TGX89Dxo2QJFPCy3eel_5Ocuh2jpX-mltnYqkpd3HWW4ugu4KWoVdr8ji5JMNJLQF5J3f5-Gh410ePHn9xvxtjL3FQ and all of your youtube playlists, history & liked videos ever will be deleted.

@ibrag8998
Copy link

Thanks! Worked for me, but for ~200 videos, now yt says "this function is temporary unavailable".

@Amit-pl
Copy link

Amit-pl commented Nov 17, 2021

Thank you, it works great, Good job, Andrei !

@DShip91
Copy link

DShip91 commented Feb 15, 2022

Is there a version of the script that only removes unavailable and deleted videos?

@youurayy
Copy link

youurayy commented Mar 6, 2022

This worked for me. Make sure to leave that Chrome tab open on the foreground, even if not active Chrome window. When the list is massive, sometimes close & open of a new tab is needed.

setInterval(() => {
  document.querySelector('#primary button[aria-label="Action menu"]').click();
  document.evaluate('//span[contains(text(),"Remove from")]', document, null, 
   XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0).click();
}, 500);

@FarisPalayi
Copy link

Thanks for this script man. Saved me a lot of time. :)

@TakshPSingh
Copy link

Does anyone have any guesses for what youtube's rate limit is? I have to wait for a few minutes after every ~300 videos that I delete.

@sthompson-celerity
Copy link

This will only work while you are watching the playlist:
you need to change the value of howManyToDelete to decide how many of the videos you want to delete

howManyToDelete = 5;
loopValue = 0;
new Promise((resolve1, reject) => {
  const loop = function () {
    if (++loopValue > howManyToDelete) {
      resolve1();
    } else {
      document.querySelector('#secondary button[aria-label="Action menu"]').click();
      new Promise((resolve) => setTimeout(() => resolve(), 200))
        .then(() => {
          document.querySelector('ytd-menu-service-item-renderer[aria-selected="false"]').click();
        })
        .then(new Promise((resolve) => setTimeout(() => resolve(), 200)))
        .then(loop)
        .catch((err) => console.log(err));
    }
  };
  loop();
});

@SarahEdu321
Copy link

Thank you, this is working for me at the moment! —SLG

@handictra
Copy link

handictra commented Mar 31, 2022

Works perfect for me
Just suited to german

setInterval(function () {
// Changed "Action menu" to "Aktionsmenü" (DE)
  	document.querySelector('#primary button[aria-label="Aktionsmenü"]').click();
  	var things = document.evaluate(
// ... as well as "Remove from" to "Aus"
    '//span[contains(text(),"Aus")]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < things.snapshotLength; i++) {
    things.snapshotItem(i).click();
  }
},` 1000);

@DewasSquid
Copy link

For all the french here, use this :
setInterval(function () { document.querySelector('#primary button[aria-label="Menu d\'actions"]').click(); var things = document.evaluate( '//span[contains(text(),"Supprimer de")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (var i = 0; i < things.snapshotLength; i++) { things.snapshotItem(i).click(); } }, 1000);

@othyn
Copy link

othyn commented May 3, 2022

It's insane to me that in 2022 this is still the only way to clear YouTube's Watch Later, and with much pain involved because you hit their stupid rate limit after 200 requests.

I have a Pi Hole and other blockers so YouTube can't tell when a video is 'watched'.

I have around 3,000 videos to clear. It always takes hours as it gets this way every few months.

@jzisser9
Copy link

jzisser9 commented May 3, 2022

It's insane to me that in 2022 this is still the only way to clear YouTube's Watch Later, and with much pain involved because you hit their stupid rate limit after 200 requests.

I have a Pi Hole and other blockers so YouTube can't tell when a video is 'watched'.

I have around 3,000 videos to clear. It always takes hours as it gets this way every few months.

Me too. I'm starting to wonder if this can be done through API calls too.

@othyn
Copy link

othyn commented May 3, 2022

Checking the official YouTube API docs, it does seem possible. I'm yet to go through their official examples repo, but by the looks of it you could use the playlistitems list endpoint to grab all the video ID's in the playlist, then loop through them and send a delete request using the playlistitems delete endpoint.

The only limitation, and its a massive one, is that you are limited to 10,000 'units' per day, with playlist deletions costing 50 per request meaning you'd blow through the quota in just 200 video deletions, which is insane and according to YouTube is 'an amount sufficient for the majority of our API users'. In order to get more, they have to audit your project:

The YouTube Data API uses a quota system to ensure that developers use the service as intended and do not create API clients that unfairly reduce service quality or limit access for others.

Projects that enable the YouTube Data API have a default quota allocation of 10,000 units per day, an amount sufficient for the majority of our API users. You can see your quota usage on the Quotas page in the API Console.

If you would like to request additional quota beyond the default allocation, you must first complete an audit to show that your project is in compliance with the YouTube API Services Terms of Service. This gives YouTube visibility into the intended use cases of large projects and ensures that YouTube's API services are being used in a manner that is free from abuse. Visit this link for additional details on complying with YouTube’s Developer Policies.

I remember all this going down with 3rd party YouTube apps on the various mobile app stores, one by one slowly getting shutdown to force everyone to use the native apps and clients, with much more reduced functionality and rife with petty design (UX and software) decisions.

YouTube really, really needs a competitor.

@jzisser9
Copy link

jzisser9 commented May 3, 2022

@othyn seriously, it beggars belief that such a simple function takes this much work to accomplish, with strings attached. For each video on this specific playlist, remove it.

@othyn
Copy link

othyn commented May 3, 2022

👍

@awelch83
Copy link

awelch83 commented May 3, 2022 via email

@Foks256
Copy link

Foks256 commented May 12, 2022

For anyone from Czech Republic, you can use this modified function from a few comments above.

setInterval(() => {
  document.querySelector('#primary button[aria-label="Nabídka akcí"]').click();
  document.evaluate('//span[contains(text(),"Odstranit ze seznamu")]', document, null, 
   XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0).click();
}, 500);

@ibakirov
Copy link

@AwareWulf Thanks for sharing 👍

@Arcadi-fr
Copy link

For all the french here, use this :
setInterval(function () { document.querySelector('#primary button[aria-label="Menu d\'actions"]').click(); var things = document.evaluate( '//span[contains(text(),"Supprimer de")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (var i = 0; i < things.snapshotLength; i++) { things.snapshotItem(i).click(); } }, 1000);

Merci beaucoup @DewasSquid ! Fonctionne au 02 juin 2022

@georgedonnelly
Copy link

This works great, thank you.

@M123-dev
Copy link

For everyone using a different language you have to update:

1: aria-label="HERE"

and

2: '//span[contains(text(),"HERE")]',


The second one is just the content of the "Remove from playlist button", press on the 3 dots and copy the text. Just some words is okay, e.g "Remove from" is totally fine.


Number 1 is a bit more tricky, to find it you have to inspect the 3 dots button (right click => inspect or something simillar)
It looks like this:
(Make sure to inspect the button not the icon, just inspect a bit on the side from the three dots.)

<button id="button" class="style-scope yt-icon-button" aria-label="Aktionsmenü"><yt-icon class="style-scope ytd-menu-renderer"</button>

From there you have to use the content of the aria-label

@jayuboi
Copy link

jayuboi commented Aug 14, 2022

It worked just fine!!
I don't know why youtube does not include this feature, I had thousends of videos on the watch later section and I'm 100% that i'm actually not going to watch none of them :) but I want to use this feature of watch later but know in the proper way.
Anyways thank you very much and greetings from colombia 👍

@artgmrs
Copy link

artgmrs commented Aug 23, 2022

Works perfect for me Just suited to german

setInterval(function () {
// Changed "Action menu" to "Aktionsmenü" (DE)
  	document.querySelector('#primary button[aria-label="Aktionsmenü"]').click();
  	var things = document.evaluate(
// ... as well as "Remove from" to "Aus"
    '//span[contains(text(),"Aus")]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < things.snapshotLength; i++) {
    things.snapshotItem(i).click();
  }
},` 1000);

Works perfect! Just suited to pt-br

setInterval(function () {
  	document.querySelector('#primary button[aria-label="Menu de ações"]').click();
  	var things = document.evaluate(
    '//span[contains(text(),"Remover")]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < things.snapshotLength; i++) {
    things.snapshotItem(i).click();
  }
}, 1000);

@mlindgren
Copy link

@othyn, unfortunately you can't do this at all with the YouTube API, because it will not return the contents of the Watch Later playlist: https://developers.google.com/youtube/v3/revision_history#september-15,-2016

@MKaczkow
Copy link

Suited for PL:

setInterval(function () {
    document.querySelector('#primary button[aria-label="Menu czynności"]').click();
    var things = document.evaluate(
        '//span[contains(text(),"Usuń z ")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    for (var i = 0; i < things.snapshotLength; i++) {
        things.snapshotItem(i).click();
    }
}, 500);

Action menu changed to Menu czynności
Remove from changed to Usuń z

@Zyzto
Copy link

Zyzto commented Oct 31, 2022

For Arabic:

setInterval(function () {
  document.querySelector('#primary button[aria-label="قائمة الإجراءات"]').click();
  var things = document.evaluate(
    '//span[contains(text(),"إزالة من")]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < things.snapshotLength; i++) {
    things.snapshotItem(i).click();
  }
}, 200);

@bryancasler
Copy link

With the latest redesign of the Watch Later page, none of the previous solutions work. I think we're going to have to search for the textContent of spans, for example: Array.from(document.querySelectorAll('span.style-scope.yt-formatted-string')).find(el => el.textContent === 'Remove from ');

@mimi1597
Copy link

mimi1597 commented Nov 9, 2022

this code worked....:)

@Alpakash
Copy link

Alpakash commented Nov 19, 2022

Thanks worked like a charm! Had to translate the aria-label and text for dutch:

setInterval(function () {
  document.querySelector('#primary button[aria-label="Actiemenu"]').click();
  var things = document.evaluate(
    '//span[contains(text(),"Verwijderen uit")]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < things.snapshotLength; i++) {
    things.snapshotItem(i).click();
  }
}, 1000);

@BryanHaley
Copy link

BryanHaley commented Nov 21, 2022

Here's my take on this

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Action menu"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

Working as of 11/21/2022. Just let it run in the background. Non-english users will need to change "Action menu" and "Remove from" to what YouTube uses for their localization.

@jumiller-cotiviti
Copy link

jumiller-cotiviti commented Nov 30, 2022

If you need it to clear hidden videos this is the tweak to do that (I had like 200) - click options menu and show hidden videos then run this

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#button > yt-icon').click();

    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

@kangdaeki
Copy link

This is a lifesaver for me. Thanks.

@drementer
Copy link

drementer commented Jan 12, 2023

Türkler için

setInterval(() => {
  document.querySelector('#primary button[aria-label="İşlem menüsü"]').click();
  document
    .evaluate(
      '//span[contains(text(),"Daha sonra")]',
      document,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    )
    .snapshotItem(0)
    .click();
}, 100);

Hızlandırılmış hali

setInterval(() => {
  document.querySelector('#primary button[aria-label="İşlem menüsü"]').click();
  document
    .evaluate(
      '//span[contains(text(),"Daha sonra")]',
      document,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    )
    .snapshotItem(0)
    .click();
}, 50);

@MShrimp4
Copy link

Korean version, if someone needs it
한국어 환경에서 유튜브 나중에 볼 동영상 전부 지우기

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="작업 메뉴"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"에서 삭제")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

@LisichkinVlad
Copy link

LisichkinVlad commented Feb 12, 2023

Russian version

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Меню действий"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Удалить из плейлиста")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++)
    {
        things.snapshotItem(i).click();
    }
}, 500);

@niklas-a
Copy link

Swedish version:


setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Åtgärdsmeny"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Ta bort från")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

@colejd
Copy link

colejd commented Mar 2, 2023

YouTube appears to rate limit you after 200ish deletions in a short time - so while the deletions appear to work on the UI side, the videos aren't actually removed from the playlist. Absolutely infuriating.

Here's a modified version of the script that deletes 200 at a time, waiting 5 minutes between each batch to avoid doing any deletions that, uh, don't actually delete. With a 5000-video playlist, this should take 167ish minutes to run. Just put it on overnight or something.

function deleteVideoFromWatchLater() {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];
    video.querySelector('#primary button[aria-label="Action menu"]').click();
    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    for (var i = 0; i < things.snapshotLength; i++) {
        things.snapshotItem(i).click();
    }
}

async function deleteWatchLater() {
  // Fiddle with these if you'd like
  let batchSize = 200; // Number to delete at once before waiting
  let waitBetweenBatchesInMilliseconds = 1000 * 60 * 5; // 5 minutes
  let waitBetweenDeletionsInMilliseconds = 500; // Half a second

  let totalWaitTime = ((5000 / batchSize) * (waitBetweenBatchesInMilliseconds / 1000 / 60)) + (5000 * (waitBetweenDeletionsInMilliseconds / 1000 / 60))
  console.log(`Deletion will take around ${totalWaitTime.toFixed(0)} minutes to run if the playlist is full.`);

  let count = 0;
  while (true) {
    await new Promise(resolve => setTimeout(resolve, waitBetweenDeletionsInMilliseconds));
    deleteVideoFromWatchLater();
    count++;

    if (count % batchSize === 0 && count !== 0) {
      console.log('Waiting for 5 minutes...');
      await new Promise(resolve => setTimeout(waitBetweenBatchesInMilliseconds));
    }
  }
}

deleteWatchLater();

The batch sizes and wait times are guesses, but they worked for me. You may need to run this more than once if any rate limiting happens - just reload the page and run the script again.

@papiforcex
Copy link

papiforcex commented Mar 25, 2023

There's the french version:

To launch it:

const watchLaterCleaner = setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Menu d\'actions"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Supprimer de")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

To stop it (if you want to):

clearInterval(watchLaterCleaner);

@nataliepjlin
Copy link

works well! thanks a lot

@drkbzx
Copy link

drkbzx commented Apr 12, 2023

It works like a charm! TIP: Disable display images on your browser settings, it runs smoother!

@HaBiX02
Copy link

HaBiX02 commented Apr 23, 2023

For Spanish from Spain users the code is:

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Menú de acciones"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Quitar de")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

It is awesome to finally see my list in a manageable size, thank you!

@PETROUNKNOWN
Copy link

Nice

@Adham380
Copy link

Adham380 commented May 5, 2023

I made a version that allows one to remove only watched videos or all.
Simply change the <= 10 with your preferred progress bar (how much of the video you watched) threshold.

var counter = 0;
setInterval(function () {
    let video = document.getElementsByTagName('ytd-playlist-video-renderer')[counter];
    let progress = "0";
    console.log(video.querySelector('#video-title').innerText)
    if(video.querySelector('#progress') !== null && video.querySelector('#progress') !== undefined){
    progress = video.querySelector('#progress').style.getPropertyValue("width")
    console.log(progress)
    } else {
    console.log("not deleted");
    counter++;
    return;
    }
    video.querySelector('#primary button[aria-label="Action menu"]').click();
    
    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

if(parseInt(progress.substring(0, progress.length -1)) >= 50){
    console.log("deleted")
    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
    } else {
    console.log("not deleted")
    counter++;
    }
}, 800);



@AndersMoberg
Copy link

This thread was a good reference when I need similar functionality (many thanks!), so I implemented it into a web extension. But my code was a little bare, and private extensions doesn't seem to be a thing, so just recently I converted it into a Userscript.

https://gist.github.com/AndersMoberg/0a9f996a49d7b3a9e9c01065ce29abf4

If you have a Userscript manager, press the Raw button and you should get the dialogue for installing it.
It only works with Swedish interface for now, but feel free to request your language if you'd like I can put it in.

Again, many thanks for this Gist and comment section

@deleonjenmar0
Copy link

Still works, thanks!

@fluffeon
Copy link

fluffeon commented Jun 1, 2023

It's giving off a "Uncaught TypeError: video.querySelector(...) is null" error and not actually doing anything.

@hi-skittles
Copy link

hi-skittles commented Jun 5, 2023

YouTube appears to rate limit you after 200ish deletions in a short time - so while the deletions appear to work on the UI side, the videos aren't actually removed from the playlist. Absolutely infuriating.

Here's a modified version of the script that deletes 200 at a time, waiting 5 minutes between each batch to avoid doing any deletions that, uh, don't actually delete. With a 5000-video playlist, this should take 167ish minutes to run. Just put it on overnight or something.

function deleteVideoFromWatchLater() {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];
    video.querySelector('#primary button[aria-label="Action menu"]').click();
    var things = document.evaluate(
        '//span[contains(text(),"Remove from")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    for (var i = 0; i < things.snapshotLength; i++) {
        things.snapshotItem(i).click();
    }
}

async function deleteWatchLater() {
  // Fiddle with these if you'd like
  let batchSize = 200; // Number to delete at once before waiting
  let waitBetweenBatchesInMilliseconds = 1000 * 60 * 5; // 5 minutes
  let waitBetweenDeletionsInMilliseconds = 500; // Half a second

  let totalWaitTime = ((5000 / batchSize) * (waitBetweenBatchesInMilliseconds / 1000 / 60)) + (5000 * (waitBetweenDeletionsInMilliseconds / 1000 / 60))
  console.log(`Deletion will take around ${totalWaitTime.toFixed(0)} minutes to run if the playlist is full.`);

  let count = 0;
  while (true) {
    await new Promise(resolve => setTimeout(resolve, waitBetweenDeletionsInMilliseconds));
    deleteVideoFromWatchLater();
    count++;

    if (count % batchSize === 0 && count !== 0) {
      console.log('Waiting for 5 minutes...');
      await new Promise(resolve => setTimeout(waitBetweenBatchesInMilliseconds));
    }
  }
}

deleteWatchLater();

The batch sizes and wait times are guesses, but they worked for me. You may need to run this more than once if any rate limiting happens - just reload the page and run the script again.

the promise is rejected returning a completed state. see below:

message: "Cannot read properties of null (reading 'click')"
stack: "TypeError: Cannot read properties of null (reading 'click')\n

@ArthurYdalgo
Copy link

ArthurYdalgo commented Jun 10, 2023

Not sure how good this is, but worked for me. Since there's no language dependent querySelector, should work for most people (I hope...)
Caveat: It might take a while, but just leave it doing it's thing

It works on this url: https://www.youtube.com/playlist?list=WL

There were over 2000+ videos on my watch later. I tried removing the ones I had already watched using the Youtube's built in feature, but the request would eventually throw me a 502 after a minute or so of waiting.

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function deleteFirstVideo(){   
    let menu = document.querySelectorAll("yt-icon-button[class='dropdown-trigger style-scope ytd-menu-renderer']")[1]; 
    menu.click(); 

    // Tiny delay to give the menu time to render
    await sleep(300); 
    let menuItems = document.querySelector("tp-yt-paper-listbox[class='style-scope ytd-menu-popup-renderer']").children;
    
    // In case of unavailable videos, there's only the remove button
    let button = menuItems[2] ?? menuItems[0]; 
    button.click();  
}

// Just check how much you have in the playlist
let videos_count = 2000;
for (i=0; i < videos_count ; i++){ 
    deleteFirstVideo(); 
    // 1500ms would get me 429 response after a couple of requests
    await sleep(1750);
}

@js6pak
Copy link

js6pak commented Jun 15, 2023

Made my own userscript which doesn't rely on the language and allows you to delete with progress threshold: https://gist.github.com/js6pak/33bdefdefac09c387f55d08c5b9526fa

@jackcarey
Copy link

Made my own userscript which doesn't rely on the language and allows you to delete with progress threshold: https://gist.github.com/js6pak/33bdefdefac09c387f55d08c5b9526fa

I modified this to also remove videos published over X days old, and those that are private or deleted. This currently relies on the language though (English). It could be updated to look for 'no_thumbnail' in the thumbnail source though

@mpr1255
Copy link

mpr1255 commented Aug 3, 2023

This one saves them all out to a json blog FWIW

var videos = document.querySelectorAll('.yt-simple-endpoint.style-scope.ytd-playlist-video-renderer');
var json = [];

videos.forEach(function(video) {
    var url = video.getAttribute('title') + "   " + 'https://www.youtube.com' + video.getAttribute('href') ;
    url = url.split('&list=WL&index=');
    json.push(url[0]);
});

// Create blob and object URL
var blob = new Blob([JSON.stringify(json, null, 2)], {type : 'application/json'});
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'videos.json';

// Append to the document for Firefox compatibility
document.body.appendChild(link);

// Programmatically click the link
link.click();

// Clean up after ourselves
document.body.removeChild(link);

@rip747
Copy link

rip747 commented Aug 26, 2023

Works perfectly!!!! Thank you so much!!!

@JaiganeshKumaran
Copy link

JaiganeshKumaran commented Nov 13, 2023

It worked for some time but then started saying 'this operation cannot be performed'. Not only can I delete stuff now but I cannot even add stuff. Google really wants you to not clear watch later to keep you addicted.

@iacopocarlini
Copy link

Suited for Italian users

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Menu Azione"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"Rimuovi da")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

@ArthurYdalgo
Copy link

It worked for some time but then started saying 'this operation cannot be performed'. Not only can I delete stuff now but I cannot even add stuff. Google really wants you to not clear watch later to keep you addicted.

depending on how long you take between one request and the other, you might get a 429 http error (too many requests in a short time period). try spacing them a little bit. my sweet spot (from a test in june 9th, 2023) was 1750ms between requests. fast enough, but not enough to get blocked by their api

@Cero-Pointer
Copy link

Updated for german users:

setInterval(function () {
    video = document.getElementsByTagName('ytd-playlist-video-renderer')[0];

    video.querySelector('#primary button[aria-label="Aktionsmenü"]').click();

    var things = document.evaluate(
        '//span[contains(text(),"entfernen")]',
        document,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );

    for (var i = 0; i < things.snapshotLength; i++) 
    {
        things.snapshotItem(i).click();
    }
}, 500);

@LosAlamosAl
Copy link

Sadly, as of 1/8/2024, none of these (English versions) work. Various errors (POST errors at YouTube, "Something went wrong", "This functionality currently not available; please try again later"). A common theme is that they work for a while (maybe 100 or so videos) then they die.

If you get stuck with continuously running Javascript as I did:

  • in the JavaScrip console window type debugger; throw 1
  • then from the Chrome "Window" menu --> "Task Manager"; find the Watch Later tab and kill it.

FYI, I'm running Chrome version 120.0.6099.71 (Official Build) (arm64) on MacOS 14.2

@jzisser9
Copy link

jzisser9 commented Jan 9, 2024

@LosAlamosAl if I had to guess, it's YouTube rate-limiting at work. Especially if it's a nice round number like 100.

@LosAlamosAl
Copy link

@jzisser9 Almost certainly, since it's about the same number (+- 10) with the user script by @js6pak. I cranked it down to about 5 seconds per deletion--same results.

@x94fujo6rpg
Copy link

x94fujo6rpg commented Feb 15, 2024

They somehow managed to break this thing again.
They seemed rolling back years of everything you'd already watched and then shoved it all back in.
My home is full of videos from the WL list like 80% of them.
Even after using the [remove watched] there are still over 1000 videos in the list.
And still tons of videos that I'm pretty sure I've already watched.

(async function fkYoutubeWatchLater() {
	let list = document.querySelectorAll("#contents.ytd-playlist-video-list-renderer>ytd-playlist-video-renderer");

	if (list.length > 0) {
		for (let item of list) {
			let target,
				target_index = 3,
				title;

			item.querySelector("#button").click();
			await sleep(500);

			target = document.querySelector("tp-yt-paper-listbox#items.style-scope.ytd-menu-popup-renderer");
			if (!target) {
				continue;
			}

			title = item.querySelector("#meta #video-title").textContent.trim();
			console.log(`removing video: ${title}`);
			target = target.childNodes[target_index];
			target.click();

			await sleep(1750);
		}

		setTimeout(() => fkYoutubeWatchLater(), 5000);
	} else {
		return;
	}

	function sleep(ms = 500) {
		return new Promise(resolve => setTimeout(resolve, ms));
	}
})();

@x94fujo6rpg
Copy link

x94fujo6rpg commented Feb 15, 2024

Sadly, as of 1/8/2024, none of these (English versions) work. Various errors (POST errors at YouTube, "Something went wrong", "This functionality currently not available; please try again later"). A common theme is that they work for a while (maybe 100 or so videos) then they die.

If you get stuck with continuously running Javascript as I did:

  • in the JavaScrip console window type debugger; throw 1
  • then from the Chrome "Window" menu --> "Task Manager"; find the Watch Later tab and kill it.

FYI, I'm running Chrome version 120.0.6099.71 (Official Build) (arm64) on MacOS 14.2

I think the issue is you have to scroll down to load more video.
And it will stop loading after a while somehow. (like they know what you're doing)

My script started randomly stopping at around 100 after successfully delete 700+ videos in one go.

@AttiliaTheHun
Copy link

Not sure if somebody mentioned it already, but you can click on your profile icon and select "Change language" (select English) and then there is no need for localized versions of the script. Next time you open youtube you simply change the language back, if it doesn't happen automatically.

@mpr1255
Copy link

mpr1255 commented Feb 15, 2024 via email

@derharry
Copy link

derharry commented Mar 11, 2024

Thanks for the script :-) I set the seconds down to 200ms - that speeded it up for me.
I was looking for hours for the button to remove everything at once, but it seems that Google removed it.

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