Skip to content

Instantly share code, notes, and snippets.

@Dorifor
Last active January 28, 2025 23:26
Show Gist options
  • Save Dorifor/c63bde5341aa64f18879680f183faa33 to your computer and use it in GitHub Desktop.
Save Dorifor/c63bde5341aa64f18879680f183faa33 to your computer and use it in GitHub Desktop.
Get all videos from a Youtube playlist page
// if you also need the data from each video element of this playlist
/**
* @typedef VideoElement
* @property { string } id a#video-title[href] > get ?v
* @property { string } title a#video-title
* @property { string } channel_name yt-formatted-string.ytd-channel-name
* @property { string } channel_url yt-formatted-string.ytd-channel-name > a[href]
* @property { string } thumbnail_url yt-image.ytd-thumbnail>img[src]
* @property { string } duration span.ytd-thumbnail-overlay-time-status-renderer
* @property { string } watch_url a#video-title[href]
* @property { string } playlist_id a#video-title[href] > get ?list
*/
/**
* extract needed data from each Node
* @param { HTMLElement[] } videoNodes
*/
function getVideosData(videoNodes) {
/** @type { VideoElement[] } */
const videos = []
videoNodes.forEach(videoNode => {
const channelElement = videoNode.querySelector('yt-formatted-string.ytd-channel-name')
const videoUrl = new URL(videoNode.querySelector('a#video-title').href)
/** @type { VideoElement } */
const newVideo = {
id: videoUrl.searchParams.get('v'),
title: videoNode.querySelector('a#video-title').textContent,
channel_name: channelElement.textContent,
channel_url: channelElement.querySelector('a').href,
thumbnail_url: videoNode.querySelector('yt-image.ytd-thumbnail>img').getAttribute('src'),
duration: videoNode.querySelector('span.ytd-thumbnail-overlay-time-status-renderer').textContent,
watch_url: videoUrl.href,
playlist_id: videoUrl.searchParams.get('list')
}
videos.push(newVideo);
})
console.table(videos);
}
// if you need to fetch every video of a playlist (from its page) when you don't have access to the API
// how it works :
// 1. scroll to the end if there's a need ( videos > 100)
// 2. it'll load the next videos in the DOM (and if there's still more repeat step 1)
// 3. it stops when there's no more to load
// 4. you get all video DOM elements (line 34)
function isContinuationInsideContents() {
const continuation = document.querySelector('ytd-continuation-item-renderer');
if (continuation == null)
return false;
return continuation.parentNode === document.querySelector('ytd-playlist-video-list-renderer > #contents');
}
function scrollToContinuation() {
const continuation = document.querySelector('ytd-continuation-item-renderer');
continuation.scrollIntoView()
}
function addContinuationCallback() {
const contents = document.querySelector('ytd-playlist-video-list-renderer > #contents');
console.log(`number of videos loaded: ${contents.childElementCount - 1}`)
var obs = new MutationObserver((mutationRecords, observer) => {
if (mutationRecords[0].removedNodes.length <= 0) return;
console.log(`number of videos loaded: ${contents.childElementCount}`)
if (isContinuationInsideContents()) {
scrollToContinuation();
} else {
console.log('FINISHED GATHERING !')
// YOUR VIDEOS ARE HERE
const videoNodes = document.querySelectorAll('ytd-playlist-video-renderer');
observer.disconnect();
}
});
obs.observe(contents, { childList: true, subtree: false });
}
addContinuationCallback();
scrollToContinuation();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment