Get the full list of channels you follow in seconds: CSV for spreadsheets, or OPML to follow them in any RSS reader, NewPipe or FreeTube.
// Responsive Muse · YouTube Subscriptions Exporter · v1.1 · 2026-10-22
// Saves the list of channels you're subscribed to as CSV (Excel, Sheets) or OPML
// (RSS readers, NewPipe, FreeTube). Runs only on youtube.com/feed/channels.
// The script makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
const TOOL = 'Export subscriptions';
const FORMAT = 'csv'; // 'csv' or 'opml'
if (location.hostname !== 'www.youtube.com' || location.pathname !== '/feed/channels') {
alert('Open your subscriptions first (signed in):\nhttps://www.youtube.com/feed/channels');
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 = 'display:block!important;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));
const rows = () => [...document.querySelectorAll('ytd-channel-renderer')];
// 1. Scroll to the end until no new channels 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 your subscriptions… ${n} channels`);
}
// 2. Read every channel (the data YouTube already rendered on this page)
const channels = rows().map(el => {
const text = sel => (el.querySelector(sel)?.textContent || '').trim();
const handle = text('#subscribers');
return {
name: text('#channel-title #text'),
handle: handle.startsWith('@') ? handle : '',
subscribers: text('#video-count').replace(/ subscribers?$/i, ''),
id: el.data?.channelId || '',
url: 'https://www.youtube.com' + (el.querySelector('a#main-link')?.getAttribute('href') || ''),
};
});
if (!channels.length) {
panel.finish('No channels found. YouTube may have changed its layout: please report it on responsive-muse.com.');
return 0;
}
// 3. Build the file and offer it
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = FORMAT === 'opml'
? ['<?xml version="1.0" encoding="UTF-8"?>', '<opml version="1.1">',
'<head><title>YouTube subscriptions</title></head>', '<body><outline text="YouTube" title="YouTube">',
...channels.filter(c => c.id).map(c =>
`<outline type="rss" text="${esc(c.name)}" title="${esc(c.name)}" ` +
`xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=${c.id}" htmlUrl="${esc(c.url)}"/>`),
'</outline></body>', '</opml>'].join('\n')
: ['Channel,Handle,Subscribers,Channel ID,URL',
...channels.map(c => [c.name, c.handle, c.subscribers, c.id, c.url].map(cell).join(','))].join('\n');
deliver(`Found ${many(channels.length, 'channel')}.`, body, `youtube-subscriptions.${FORMAT}`);
return channels.length;
})();
🛡️ Chrome says “Don't paste code…”? That's normal. Type allow pasting in the console, press Enter and paste again. You only need to do it once.
Why does Chrome warn me?
Chrome shows this the first time anyone pastes into the console, to protect people from scams that ask them to paste unknown code. It's right to warn you, and that's why this script is fully visible above: 118 commented lines that send nothing anywhere and never touch your passwords or cookies. Why it's safe.
Rather not use the console? Drag the Export subscriptions button to your bookmarks bar and click it on www.youtube.com. No warning that way.
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. 118 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:
cc1af7648aced7d4…
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/feed/channels while signed in.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Pick CSV or OPML, copy the script, paste it in the console and press Enter. A small box appears on the page with the count: click Download (or Copy).
What you get
The CSV has the channel name, @handle, subscriber count, channel ID and link. The OPML turns each channel into an RSS feed of its new uploads (youtube.com/feeds/videos.xml?channel_id=…), so you can follow YouTube without the algorithm.
FAQ
How do I import the OPML into an RSS reader?
Every reader has an "Import OPML" option (Feedly, Inoreader, NetNewsWire, Thunderbird). Each channel becomes a feed of its new videos.
Isn't Google Takeout enough?
Takeout works but takes hours or days and sends a zip. This gives you the list in a few seconds.
