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.

@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.

@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.

@vinivosh
Copy link

vinivosh commented Apr 1, 2024

Brazilian Portuguese version:

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

    video.querySelector('#primary button[aria-label="Menu de ações"]').click();

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

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

@guilhermecgs
Copy link

Brazilian Portuguese version:

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

    video.querySelector('#primary button[aria-label="Menu de ações"]').click();

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

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

👍

@macaua-23
Copy link

Thanks a lot @vinivosh & @colejd. It works.

@ArthurYdalgo
Copy link

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.

probably because the requests were timing out

@SbMan1
Copy link

SbMan1 commented Apr 26, 2024

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

Do you happen to still have this code as this would be very helpful. I just noticed I have 4,800 Watched Later and heard if I hit 5k I can't add anymore. I also msg'd js6pak on his forked page. I'll just do Date Added ( Oldest ) to make it the easiest, if I delete ones I don't want then oh well. Thanks for your time.

@BIRTAX38
Copy link

BIRTAX38 commented May 1, 2024

Polska wersja:

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

    video.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();
    }
}, 400);

@jeduardobras
Copy link

Brazilian Portuguese version:

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

    video.querySelector('#primary button[aria-label="Menu de ações"]').click();

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

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

Works for the European Portuguese version of YouTube as well. Thanks!

@John-nata
Copy link

John-nata commented Aug 14, 2024

Hi guys!
Just wanna share my userscript (inspired by @colejd's script) to helps you tidy up your YT playlists : YT-Playlist-Cleaner

Here are the key features :
-Customisable settings for deletion criteria
-Auto-scroll functionality to process large playlists
-Improved user interface with progress bar and status updates
-Pause and resume functionality
-Configurable delays between deletions to avoid rate limiting

screenshot

Tbh, I received some helpful suggestions from Claude AI to optimize some parts of my script before uploading it online.
Let me know what you think!

@Someone117
Copy link

I created a similar script that works by just clicking the 3 dots and then on the "Remove from playlist" button. By default, this script removes 100 videos, but that is also adjustable

This should work for all languages, so paste the script into your console and run.

YT might change their UI and you may have different screen resolution, so you may need to adjust the click x and y for each. You may also need to increase the delay if videos don't load fast enough (you can also run it multiple times).

function simulateClick(x, y) {
    // What acually does the clicking
    var event = new MouseEvent('click', {
        view: window,
        bubbles: true,
        cancelable: true,
        clientX: x,
        clientY: y
    });
    document.elementFromPoint(x, y).dispatchEvent(event);
}

function performClicks() {
    let count = 0;

    function clickSequence() {
        // change this number to remove more videos
        if (count >= 100) {
            console.log("Completed 100 click sequences.");
            return;
        }

        // change this when the UI changes, this should be the location of the 3 dots
        simulateClick(1872, 256);
        console.log(`Click sequence ${count + 1} pt 1`);

        setTimeout(function() {
            // change this when the UI changes, this should be the location of the "Remove From Playlist" button
            simulateClick(1784, 333);
            console.log(`Click sequence ${count + 1} pt 2`);

            count++;

            // Wait 100ms, you may want to set this to more time to let the next video load
            setTimeout(clickSequence, 100);
        // Wait 100ms, you may want to set this to more time to let the next video load
        }, 100);
    }

    // Start
    clickSequence();
}

// Execute the click sequences
performClicks();

This code prints your mouse location to the console so you can tailor the script

document.addEventListener('mousemove', function(event) {
    let x = event.clientX;
    let y = event.clientY;
    console.log(`Mouse position: X: ${x}, Y: ${y}`);
});

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