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 = '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;
})();
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:
34c063c4e3461534…
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.
