Skip to content

Instantly share code, notes, and snippets.

@leotulipan
Created May 2, 2026 16:46
Show Gist options
  • Select an option

  • Save leotulipan/5c40ed1f44aae7d5bca107d99089eae0 to your computer and use it in GitHub Desktop.

Select an option

Save leotulipan/5c40ed1f44aae7d5bca107d99089eae0 to your computer and use it in GitHub Desktop.
Scraper to turn your YouTube watch later list (private no API) into a csv
function scrapePlaylist() {
// 0. go to https://www.youtube.com/playlist?list=WL and run this from the chrome dev console
// 1. Extract the expected number of videos from the metadata
const bylineItems = document.querySelectorAll('ytd-playlist-byline-renderer .byline-item');
let targetCount = 0;
if (bylineItems.length > 0) {
// Extracts digits from strings like "450 videos" or "1,234 views"
const countText = bylineItems[0].innerText;
targetCount = parseInt(countText.replace(/[^0-9]/g, ''), 10);
}
if (!targetCount || isNaN(targetCount)) {
console.error("Could not find the total video count. Falling back to 100.");
targetCount = 100;
}
console.log(`Start: Attempting to harvest a total of ${targetCount} videos... Please do not scroll.`);
let extractedData = [];
let seenUrls = new Set();
// Safety net: If no new videos are found for 10 cycles (10 seconds), abort loop
let idleCounter = 0;
let lastDataLength = 0;
const scrollInterval = setInterval(() => {
const domVideos = document.querySelectorAll('ytd-playlist-video-renderer');
let lastValidVideo = null;
for (let video of domVideos) {
const titleEl = video.querySelector('#video-title');
// Skip empty/ghost containers
if (!titleEl || titleEl.innerText.trim() === '') continue;
const url = titleEl.href;
// Process only if we haven't seen this URL yet
if (!seenUrls.has(url)) {
const title = titleEl.innerText.trim();
const channelEl = video.querySelector('ytd-channel-name a');
const channel = channelEl ? channelEl.innerText.trim() : 'N/A';
const durationEl = video.querySelector('ytd-thumbnail-overlay-time-status-renderer #text');
const duration = durationEl ? durationEl.innerText.trim() : 'N/A';
const infoSpans = video.querySelectorAll('#video-info span');
let views = 'N/A';
let uploadTime = 'N/A';
if (infoSpans.length >= 3) {
views = infoSpans[0].innerText.trim();
uploadTime = infoSpans[2].innerText.trim();
} else if (infoSpans.length === 1) {
const splitInfo = infoSpans[0].innerText.split('•');
if(splitInfo.length >= 2) {
views = splitInfo[0].trim();
uploadTime = splitInfo[1].trim();
}
}
extractedData.push({ title, channel, duration, views, uploadTime });
seenUrls.add(url);
}
lastValidVideo = video;
}
// Progress display
console.log(`Status: Collected ${extractedData.length} of ${targetCount} videos.`);
// Idle-Check (Safety net for hidden/deleted videos)
if (extractedData.length === lastDataLength) {
idleCounter++;
} else {
idleCounter = 0; // Reset because we found new videos
lastDataLength = extractedData.length;
}
// Abort condition: Target reached OR idle too long at the bottom
if (extractedData.length >= targetCount || idleCounter > 10) {
clearInterval(scrollInterval);
if (idleCounter > 10) {
console.warn(`\nStopped due to timeout! The playlist likely contains hidden/deleted videos. Found ${extractedData.length} videos.`);
} else {
console.log(`\nGoal reached! Successfully harvested all ${targetCount} videos.`);
}
// Generate and download CSV
downloadCSV(extractedData.slice(0, targetCount));
} else {
// Continue scrolling
if (lastValidVideo) {
lastValidVideo.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
const loader = document.querySelector('ytd-continuation-item-renderer');
if (loader) {
setTimeout(() => loader.scrollIntoView({ behavior: 'smooth', block: 'end' }), 300);
}
}
}, 1000);
// Helper function to build and trigger the CSV download
function downloadCSV(videos) {
console.log("Generating CSV file...");
// Header
let csvContent = "Title,Channel,Duration,Views,UploadTime\n";
// Data rows
videos.forEach(v => {
const safeTitle = `"${v.title.replace(/"/g, '""')}"`;
const safeChannel = `"${v.channel.replace(/"/g, '""')}"`;
csvContent += `${safeTitle},${safeChannel},"${v.duration}","${v.views}","${v.uploadTime}"\n`;
});
// Create Blob (with BOM for correct UTF-8 rendering in Excel)
const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", "youtube_playlist_export.csv");
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log("Download should have started!");
}
}
// Start script
scrapePlaylist();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment