Created
May 2, 2026 16:46
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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