Export your YouTube subscriptions (CSV or OPML)

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.

export-subscriptions.js · 118 linespaste in the DevTools console on www.youtube.com
// 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, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
  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.

Why it's safe to runAutomated check passed Oct 22, 2026 · tested on a real account Oct 22, 2026
  • 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
  1. Read it: every line is above, with comments.
  2. Search the code for fetch, XMLHttpRequest, cookie, localStorage or eval. None of them appear.
  3. Open the Network tab before running it: you'll only see requests to www.youtube.com.
Did it work for you?

How to use it

  1. Open youtube.com/feed/channels while signed in.
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. 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.

More YouTube tools