Export every video in a YouTube playlist (title, channel, duration and link) to a CSV or text file, straight from your browser. Works with your own playlists, Watch Later, Liked videos and any public playlist, including very long ones.
Updated for YouTube's 2026 layout (old snippets that look for ytd-playlist-video-renderer no longer find most lists).
// 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 the playlist on YouTube (the page with the list of videos,
youtube.com/playlist?list=…). - 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 until the whole list is loaded, then shows a box: click Download (or Copy).
Why old playlist snippets stopped working
In 2026 YouTube moved most playlists to a new layout. Snippets that look for ytd-playlist-video-renderer now find nothing on public playlists, while Watch Later still uses the old one. This script reads both layouts and was tested on a 2,785-video list.
FAQ
Does it work with long playlists?
Yes. It scrolls until the whole list is loaded. We tested it on a 2,785-video list.
Why are a few videos missing?
Deleted and private videos are hidden by YouTube itself ("unavailable videos are hidden"), so they can't be exported.
