Get all video titles from a YouTube Playlist page

Published October 27, 2019, by Tommy George

The other night, a friend was setting up a YouTube playlist for a party. For reasons we don’t have to go into here, she also wanted a text based list of all the video titles, sorted, for easy reference.

I thought this was a good opportunity to do a fun little Javascript snippet, and show her how to open Dev Tools and run it.

TL;DR: This is what we ended up with:

console.log(Array.from(document.querySelectorAll('.ytd-playlist-video-list-renderer #video-title')).map((el) => {return el.textContent.trim()}).sort().join("\n"))

Or shorter (see $$ later):

$$('.ytd-playlist-video-list-renderer #video-title').map((el)=>{return el.textContent.trim()}).sort().join('\n')

The process / finding what we needed

Opening dev tools in my browser, I wanted to confirm that each video title in the playlist had a shared class name, or some way to use a CSS selector to get at them.

Example of YouTube Playlist HTML

Sure enough, each does have a shared identifier. Strangely, they all have an id of video-title. This is really weird to me, because that’s not how that should be used, according to the html spec:

When specified on HTML elements, the id attribute value must be unique amongst all the IDs in the element's tree…

There should only ever be a single element on the page with a given ID value. At least that's how I learned it, and read it. :shrug:

I didn’t bother investigating why the folks at YouTube went this route instead of just using a class name. So that’s a mystery for another day, I suppose.

One thing we ran into was that for some pages, using a selector limited to that ID value gave unexpected results: sometimes it included video titles that were not in the playlist, and not visible on the page. I suspect there are just visibly hidden recommended videos or something.

So then we just needed to get a class name or selector for the actual list container, to limit our result set a bit!

One thing to note: this snippet only reads what’s currently on the page. YouTube doesn’t automatically load every video for long playlists. You may have to manually scroll the list down a few times until all of the items are loaded, before running this snippet!

Once we target the right part of the page by limiting our selector a bit, and make sure the entire list is loaded, we can run the snippet.

How it works

If you’re not used to looking at this stuff, this isn’t going to be that obvious.

It starts actually with document.querySelectorAll('.ytd-playlist-video-list-renderer #video-title'). That selects all HTML Elements with an ID of video-title that are found inside an element with a class of ytd-playlist-video-list-renderer.

That’s where the actual playlist items are rendered. Again, it’s odd that there would be more than one element on the page with the same ID — but that’s what we’re working with!

Next, we need to loop over each of those and get the title text. But since querySelectorAll doesn’t return an array, we can’t just use map(). So, first we wrap that in Array.from(), which accepts the NodeList returned from querySelectorAll and turns it into a proper array, with methods like map, etc.

map() returns a new array, based on passing each item into a callback function. Here, we're using Arrow Function syntax to create that callback inline:

(el) => {return el.textContent.trim()}).sort().join("\n")

This takes an element, el, and returns it’s textContent value (a string) having run trim(), which cuts off any extra white space before and after the title text. (Try running without trim() to see why this is necessary to clean up this data.

Before we return what map has for us, we’ll use “chaining” to apply sort to the returned array. That’s how we get our list of strings in alphabetical order!

Then we continue the “chain” and apply join() to the array. This returns a string that joins each element of the Array with whatever string we pass in — in this case, a “new line” character (\n). So the final return value is a string, with each title on its own line.

Possible Improvements