Save every video in your Watch Later list (title, channel, duration and link) to a CSV or text file. Online playlist exporters can’t do this because the list is private.
// Responsive Muse · YouTube Playlist Exporter · v1.1 · 2026-10-22
// Exports any playlist you can open (your own, Watch Later, Liked videos, public lists,
// or a channel's uploads) to CSV or TXT. Runs only on youtube.com/playlist.
// The script makes no network requests of its own: YouTube loads the rest of the list
// while the script scrolls. It never reads cookies, passwords or tokens.
(async () => {
const TOOL = 'Export playlist';
const FORMAT = 'csv'; // 'csv' (Excel, Google Sheets) or 'txt' (one URL per line)
if (location.hostname !== 'www.youtube.com' || location.pathname !== '/playlist') {
alert('Open a playlist first, for example:\nhttps://www.youtube.com/playlist?list=WL');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`; // 1 video, 2 videos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancel', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
const deliver = (summary, body, file) => {
const save = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
a.download = file;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
};
const copy = () => navigator.clipboard.writeText(body)
.then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
.catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Playlist rows: new layout (yt-lockup-view-model) and old layout (ytd-playlist-video-renderer)
const rows = () => [...document.querySelectorAll('yt-lockup-view-model, ytd-playlist-video-renderer')]
.filter(el => el.querySelector('a[href*="/watch?v="][href*="index="]'));
// 1. Scroll to the end until no new videos appear three times in a row
let last = -1, idle = 0;
while (idle < 3) {
window.scrollTo(0, document.documentElement.scrollHeight);
await sleep(1200);
const n = rows().length;
idle = n === last ? idle + 1 : 0;
last = n;
panel.text(`Loading the playlist… ${n} videos`);
}
// 2. Read every row
const seen = new Set();
const videos = [];
for (const el of rows()) {
const link = new URL(el.querySelector('a[href*="/watch?v="]').href);
const id = link.searchParams.get('v');
const index = link.searchParams.get('index') || '';
if (seen.has(id + index)) continue;
seen.add(id + index);
const text = sel => (el.querySelector(sel)?.textContent || '').trim();
videos.push({
index,
title: el.querySelector('h3')?.getAttribute('title') || text('#video-title'),
channel: text('a[href^="/@"], a[href^="/channel/"], ytd-channel-name a'),
duration: text('.ytBadgeShapeText, ytd-thumbnail-overlay-time-status-renderer #text'),
url: 'https://www.youtube.com/watch?v=' + id,
});
}
if (!videos.length) {
panel.finish('No videos found. YouTube may have changed its layout: please report it on responsive-muse.com.');
return 0;
}
// 3. Build the file and offer it
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = FORMAT === 'txt'
? videos.map(v => v.url).join('\n')
: ['Index,Title,Channel,Duration,URL', ...videos.map(v =>
[v.index, v.title, v.channel, v.duration, v.url].map(cell).join(','))].join('\n');
const name = (document.title.replace(/ - YouTube$/, '') || 'playlist').replace(/[\\/:*?"<>|]+/g, '_');
deliver(`Found ${many(videos.length, 'video')}.`, body, `${name}.${FORMAT}`);
return videos.length;
})();
Bookmarklet: drag the middle button to your bookmarks bar, open www.youtube.com and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.youtube.com itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on www.youtube.com. Anywhere else it stops before doing anything.
- Short and readable. 122 commented lines, not minified or obfuscated.
- Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
- Same code for everyone. SHA-256 of the default version:
7d5d08b0c3126c7b…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to www.youtube.com.
How to use it
- Open youtube.com/playlist?list=WL while signed in.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Copy the script, paste it in the console and press Enter. It scrolls to load the whole list, then shows a box: click Download (or Copy).
Why online tools can’t export Watch Later
Tools that ask for a playlist link read public playlists through YouTube’s API. Watch Later is private and only exists while you’re signed in, so the API returns nothing. This script runs inside your own tab and reads what’s already on your screen.
FAQ
Why can't online playlist exporters read Watch Later?
Watch Later is private and only visible while you're signed in. This script runs inside your own YouTube tab, so it sees what you see.
What columns does the CSV have?
Index, title, channel, duration and the video URL.
