Export Your YouTube Subscriptions to RSS in 2025

YouTube removed RSS subscription feeds years ago, but your subscriptions still have individual RSS feeds available. This script automates extracting all your YouTube subscriptions and converting them into RSS feed URLs that work with any RSS reader.

What This Script Does

The YouTube RSS Extractor script runs directly in your browser and:

  1. Automatically navigates to your YouTube subscriptions page
  2. Scrolls to load all your subscribed channels
  3. Extracts the unique channel ID for each subscription
  4. Generates downloadable files with RSS feed URLs
  5. Creates an OPML file for easy import into RSS readers

How to Use It

  1. Open YouTube - Go to youtube.com and make sure you're logged in
  2. Open Developer Console - Press F12 (or right-click → Inspect) and click the "Console" tab
  3. Paste and Run - Copy the entire script below, paste it into the console, and press Enter
  4. Wait for Extraction - The script will show a progress dialog as it processes your subscriptions
  5. Download Your Feeds - Once complete, download either:
    • OPML file - Import directly into any RSS reader
    • TXT file - Plain text list of all feed URLs
    • Copy URLs - Copy all feeds to clipboard

What You Get

Each YouTube channel has an RSS feed at:

https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID

The script finds all your channel IDs and packages them into standard RSS formats that work with Feedly, Inoreader, NewsBlur, or any other RSS reader.

Benefits of RSS for YouTube

  • Chronological feed - Videos appear in order of upload, not algorithm-sorted
  • No ads - RSS readers don't show YouTube ads
  • No Shorts - Easy to filter out Shorts in your RSS reader
  • Offline reading - See what's new without opening YouTube
  • Multi-platform - Use any RSS reader on any device

Privacy & Security Note

This script runs entirely in your browser. It doesn't send your data anywhere - it only reads your subscriptions from YouTube's page and generates files locally on your computer. The script is open source and readable, so you can verify exactly what it does.

The Script

Copy the code below and follow the instructions above:

(async () => {
    // Suppress YouTube's internal errors
    const originalError = console.error;
    console.error = (...args) => {
        if (args[0]?.toString().includes('postMessage') || 
            args[0]?.toString().includes('requestStorageAccessFor') ||
            args[0]?.toString().includes('doubleclick')) return;
        originalError.apply(console, args);
    };

    // Create Trusted Types policy if needed
    let trustedPolicy = null;
    if (window.trustedTypes && trustedTypes.createPolicy) {
        trustedPolicy = trustedTypes.createPolicy('ytRssExtractor', {
            createHTML: (string) => string
        });
    }

    // Helper to safely set HTML
    const safeSetHTML = (element, html) => {
        if (trustedPolicy) {
            element.innerHTML = trustedPolicy.createHTML(html);
        } else {
            // Fallback to textContent and DOM manipulation
            element.textContent = '';
            const temp = document.createElement('div');
            temp.innerHTML = html;
            while (temp.firstChild) {
                element.appendChild(temp.firstChild);
            }
        }
    };

    // Create modern dialog UI
    const dialog = document.createElement("dialog");
    dialog.style.cssText = `
        padding: 30px;
        border-radius: 12px;
        border: none;
        box-shadow: 0 10px 40px rgba(0,0,0,0.2);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        max-width: 500px;
        background: white;
        z-index: 99999;
    `;

    const container = document.createElement("div");
    container.style.cssText = "display: flex; flex-direction: column; gap: 20px;";

    const title = document.createElement("h2");
    title.textContent = "📺 YouTube RSS Extractor";
    title.style.cssText = "margin: 0; color: #333; font-size: 24px;";

    const statusLabel = document.createElement("div");
    statusLabel.style.cssText = "color: #666; font-size: 14px;";
    statusLabel.textContent = "Initializing...";

    const progressContainer = document.createElement("div");
    progressContainer.style.cssText = "display: flex; flex-direction: column; gap: 8px;";

    const progressBar = document.createElement("progress");
    progressBar.style.cssText = "width: 100%; height: 8px;";

    const progressText = document.createElement("div");
    progressText.style.cssText = "font-size: 12px; color: #999; text-align: center;";
    progressText.textContent = "0%";

    const stats = document.createElement("div");
    stats.style.cssText = `
        display: none;
        padding: 15px;
        background: #f5f5f5;
        border-radius: 8px;
        font-size: 14px;
        color: #555;
        max-height: 200px;
        overflow-y: auto;
    `;

    const buttonContainer = document.createElement("div");
    buttonContainer.style.cssText = "display: none; gap: 10px; margin-top: 10px; flex-wrap: wrap;";

    const createButton = (text, bgColor) => {
        const btn = document.createElement("button");
        btn.textContent = text;
        btn.style.cssText = `
            padding: 10px 20px;
            background: ${bgColor};
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            font-weight: 600;
            transition: opacity 0.2s;
        `;
        btn.onmouseover = () => btn.style.opacity = '0.9';
        btn.onmouseout = () => btn.style.opacity = '1';
        return btn;
    };

    const downloadOPMLBtn = createButton("📥 Download OPML", "#ff0000");
    const downloadTxtBtn = createButton("📄 Download TXT", "#065fd4");
    const copyBtn = createButton("📋 Copy URLs", "#606060");
    const closeBtn = createButton("✕ Close", "#333");

    // Assemble dialog
    progressContainer.appendChild(progressBar);
    progressContainer.appendChild(progressText);
    buttonContainer.appendChild(downloadOPMLBtn);
    buttonContainer.appendChild(downloadTxtBtn);
    buttonContainer.appendChild(copyBtn);
    buttonContainer.appendChild(closeBtn);
    container.appendChild(title);
    container.appendChild(statusLabel);
    container.appendChild(progressContainer);
    container.appendChild(stats);
    container.appendChild(buttonContainer);
    dialog.appendChild(container);

    // Add to page
    document.body.appendChild(dialog);

    // Show dialog
    try {
        dialog.showModal();
    } catch (e) {
        dialog.style.cssText += `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 99999;
            display: block;
        `;
    }

    const channels = [];
    let failedChannels = [];

    try {
        // Step 1: Check if we're on the subscriptions page
        statusLabel.textContent = "Checking current page...";

        const isSubscriptionsPage = window.location.pathname.includes('/feed/channels') || 
                                   window.location.pathname.includes('/feed/subscriptions');

        if (!isSubscriptionsPage) {
            statusLabel.textContent = "Navigating to subscriptions page...";
            window.location.href = 'https://www.youtube.com/feed/channels';

            // Store flag to auto-run after navigation
            sessionStorage.setItem('autoRunRSSExtractor', 'true');

            dialog.close();
            dialog.remove();
            alert('Navigating to subscriptions page. The script will continue automatically.');
            return;
        }

        // Check if we should auto-run
        if (sessionStorage.getItem('autoRunRSSExtractor') === 'true') {
            sessionStorage.removeItem('autoRunRSSExtractor');
            statusLabel.textContent = "Auto-continuing extraction...";
        }

        // Wait for page to fully load
        await new Promise(resolve => setTimeout(resolve, 2000));

        // Step 2: Find the container with subscriptions
        statusLabel.textContent = "Looking for subscriptions...";

        // Try multiple selectors for different YouTube layouts
        const containerSelectors = [
            '#contents',
            '#items',
            'ytd-section-list-renderer #contents',
            'ytd-item-section-renderer #contents',
            '#primary #contents'
        ];

        let contentContainer = null;
        for (const selector of containerSelectors) {
            const element = document.querySelector(selector);
            if (element && element.children.length > 0) {
                contentContainer = element;
                break;
            }
        }

        if (!contentContainer) {
            // Try scrolling once to trigger loading
            window.scrollTo(0, 1000);
            await new Promise(resolve => setTimeout(resolve, 2000));

            for (const selector of containerSelectors) {
                const element = document.querySelector(selector);
                if (element && element.children.length > 0) {
                    contentContainer = element;
                    break;
                }
            }
        }

        if (!contentContainer) {
            throw new Error("Could not find subscriptions container. Please make sure you're on youtube.com/feed/channels");
        }

        // Step 3: Scroll to load all subscriptions
        statusLabel.textContent = "Loading all subscriptions (this may take a moment)...";

        let previousCount = 0;
        let currentCount = 0;
        let noChangeCounter = 0;
        const maxNoChange = 3;

        while (noChangeCounter < maxNoChange) {
            previousCount = currentCount;

            // Scroll to bottom
            window.scrollTo(0, document.documentElement.scrollHeight);

            // Wait for content to load
            await new Promise(resolve => setTimeout(resolve, 1500));

            // Count current channels
            const currentChannels = document.querySelectorAll([
                'ytd-channel-renderer',
                'ytd-browse[page-subtype="channels"] ytd-grid-renderer ytd-channel-renderer',
                '#main-link.channel-link',
                'a[href^="/@"]#endpoint'
            ].join(', '));

            currentCount = currentChannels.length;
            progressText.textContent = `Found ${currentCount} channels so far...`;

            if (currentCount === previousCount) {
                noChangeCounter++;
            } else {
                noChangeCounter = 0;
            }

            // Check for loading spinners
            const hasSpinner = document.querySelector('#spinnerContainer.active') || 
                             document.querySelector('ytd-continuation-item-renderer') ||
                             document.querySelector('.spinner');

            if (!hasSpinner && noChangeCounter >= maxNoChange) {
                break;
            }
        }

        // Step 4: Find all channel elements
        statusLabel.textContent = "Extracting channel information...";

        // Use multiple strategies to find channels
        const channelSelectors = [
            'ytd-channel-renderer',
            '#main-link.channel-link',
            'a#main-link[href*="/channel/"]',
            'a#main-link[href*="/@"]',
            'a#endpoint[href^="/@"]',
            'a#avatar-link[href*="/channel/"]',
            'a#avatar-link[href*="/@"]'
        ];

        const channelElements = new Set();
        for (const selector of channelSelectors) {
            document.querySelectorAll(selector).forEach(el => {
                if (el.href && (el.href.includes('/channel/') || el.href.includes('/@'))) {
                    channelElements.add(el);
                }
            });
        }

        const uniqueChannels = Array.from(channelElements);

        if (uniqueChannels.length === 0) {
            throw new Error("No channels found. Please scroll down manually to load some channels and try again.");
        }

        statusLabel.textContent = `Found ${uniqueChannels.length} unique channels. Extracting details...`;
        progressBar.max = uniqueChannels.length;
        progressBar.value = 0;

        // Process channels in batches to avoid overwhelming
        const batchSize = 5;
        const processedUrls = new Set();

        for (let i = 0; i < uniqueChannels.length; i++) {
            const element = uniqueChannels[i];
            progressBar.value = i + 1;
            progressText.textContent = `${Math.round((i + 1) / uniqueChannels.length * 100)}% (${i + 1}/${uniqueChannels.length})`;

            try {
                const channelUrl = element.href;

                // Skip if already processed
                if (processedUrls.has(channelUrl)) {
                    continue;
                }
                processedUrls.add(channelUrl);

                // Try to get channel name
                let channelName = "Unknown Channel";

                // Look for channel name in various places
                const nameSelectors = [
                    'yt-formatted-string.ytd-channel-name',
                    '#channel-title',
                    '#text.ytd-channel-name',
                    '#text',
                    '.ytd-channel-name',
                    '#title'
                ];

                // First try within the channel element
                let nameElement = null;
                const parentRenderer = element.closest('ytd-channel-renderer');

                if (parentRenderer) {
                    for (const selector of nameSelectors) {
                        nameElement = parentRenderer.querySelector(selector);
                        if (nameElement && nameElement.textContent) break;
                    }
                }

                if (nameElement && nameElement.textContent) {
                    channelName = nameElement.textContent.trim();
                }

                statusLabel.textContent = `Processing: ${channelName}...`;

                // Try to extract channel ID directly from URL first
                let channelId = null;

                // Direct channel ID in URL
                const channelIdMatch = channelUrl.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/);
                if (channelIdMatch) {
                    channelId = channelIdMatch[1];
                } else {
                    // Need to fetch the channel page for @handle URLs
                    try {
                        const response = await fetch(channelUrl, {
                            credentials: 'include',
                            headers: {
                                'Accept': 'text/html,application/xhtml+xml',
                            }
                        });

                        if (response.ok) {
                            const html = await response.text();

                            // Try multiple patterns to find channel ID
                            const patterns = [
                                /"channelId":"(UC[a-zA-Z0-9_-]{22})"/,
                                /"browseId":"(UC[a-zA-Z0-9_-]{22})"/,
                                /"externalChannelId":"(UC[a-zA-Z0-9_-]{22})"/,
                                /channel_id=(UC[a-zA-Z0-9_-]{22})/,
                                /<link rel="canonical" href="[^"]*\/channel\/(UC[a-zA-Z0-9_-]{22})/
                            ];

                            for (const pattern of patterns) {
                                const match = html.match(pattern);
                                if (match) {
                                    channelId = match[1];
                                    break;
                                }
                            }
                        }
                    } catch (fetchError) {
                        console.warn(`Failed to fetch ${channelUrl}:`, fetchError);
                    }
                }

                if (!channelId) {
                    console.error(`Could not find channel ID for ${channelName} (${channelUrl})`);
                    failedChannels.push(`${channelName} - Could not extract ID`);
                    continue;
                }

                channels.push({
                    id: channelId,
                    name: channelName,
                    url: channelUrl,
                    feedUrl: `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`
                });

            } catch (error) {
                console.error(`Error processing channel:`, error);
                failedChannels.push(`Error processing channel`);
            }

            // Small delay every batch to avoid rate limiting
            if ((i + 1) % batchSize === 0) {
                await new Promise(resolve => setTimeout(resolve, 500));
            }
        }

        // Step 5: Display results
        statusLabel.textContent = "✅ Extraction complete!";
        progressContainer.style.display = "none";

        // Show statistics using DOM manipulation instead of innerHTML
        stats.style.display = "block";
        stats.textContent = ''; // Clear content

        // Create stats content with DOM
        const statsTitle = document.createElement('strong');
        statsTitle.textContent = '📊 Results:';
        stats.appendChild(statsTitle);
        stats.appendChild(document.createElement('br'));

        const successText = document.createTextNode(`✓ Successfully extracted: `);
        const successCount = document.createElement('strong');
        successCount.textContent = channels.length.toString();
        const successLabel = document.createTextNode(' channels');
        stats.appendChild(successText);
        stats.appendChild(successCount);
        stats.appendChild(successLabel);
        stats.appendChild(document.createElement('br'));

        if (failedChannels.length > 0) {
            const failedText = document.createTextNode(`✗ Failed: `);
            const failedCount = document.createElement('strong');
            failedCount.textContent = failedChannels.length.toString();
            const failedLabel = document.createTextNode(' channels');
            stats.appendChild(failedText);
            stats.appendChild(failedCount);
            stats.appendChild(failedLabel);
            stats.appendChild(document.createElement('br'));

            if (failedChannels.length > 0) {
                stats.appendChild(document.createElement('br'));
                const details = document.createElement('details');
                const summary = document.createElement('summary');
                summary.textContent = 'Show failed channels';
                details.appendChild(summary);

                const failedList = document.createElement('small');
                failedList.textContent = failedChannels.join(', ');
                details.appendChild(failedList);
                stats.appendChild(details);
            }
        }

        // Show action buttons
        buttonContainer.style.display = "flex";

        // Helper function to escape XML
        const escapeXML = (str) => {
            if (!str) return '';
            return str.replace(/[<>&'"]/g, (c) => ({
                '<': '<',
                '>': '>',
                '&': '&',
                "'": ''',
                '"': '"'
            }[c]));
        };

        // OPML download
        downloadOPMLBtn.onclick = () => {
            const opmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
    <head>
        <title>YouTube Subscriptions RSS Export</title>
        <dateCreated>${new Date().toISOString()}</dateCreated>
        <ownerName>YouTube RSS Extractor</ownerName>
    </head>
    <body>
        <outline text="YouTube Subscriptions" title="YouTube Subscriptions">
${channels.map(ch => `            <outline type="rss" text="${escapeXML(ch.name)}" title="${escapeXML(ch.name)}" xmlUrl="${ch.feedUrl}" htmlUrl="${ch.url}"/>`).join('\n')}
        </outline>
    </body>
</opml>`;

            const blob = new Blob([opmlContent], { type: 'application/xml' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `youtube_subscriptions_${new Date().toISOString().split('T')[0]}.opml`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            // Flash button to show success
            const originalText = downloadOPMLBtn.textContent;
            downloadOPMLBtn.textContent = "✓ Downloaded!";
            setTimeout(() => {
                downloadOPMLBtn.textContent = originalText;
            }, 2000);
        };

        // TXT download
        downloadTxtBtn.onclick = () => {
            const txtContent = `YouTube RSS Feeds Export
Generated: ${new Date().toISOString()}
Total Channels: ${channels.length}

${'='.repeat(50)}

${channels.map(ch => `Channel: ${ch.name}
Feed URL: ${ch.feedUrl}
Channel URL: ${ch.url}
${'-'.repeat(50)}`).join('\n\n')}`;

            const blob = new Blob([txtContent], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `youtube_rss_feeds_${new Date().toISOString().split('T')[0]}.txt`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            // Flash button to show success
            const originalText = downloadTxtBtn.textContent;
            downloadTxtBtn.textContent = "✓ Downloaded!";
            setTimeout(() => {
                downloadTxtBtn.textContent = originalText;
            }, 2000);
        };

        // Copy URLs
        copyBtn.onclick = async () => {
            const urls = channels.map(ch => ch.feedUrl).join('\n');
            try {
                await navigator.clipboard.writeText(urls);
                const originalText = copyBtn.textContent;
                copyBtn.textContent = "✓ Copied!";
                setTimeout(() => {
                    copyBtn.textContent = originalText;
                }, 2000);
            } catch (err) {
                // Fallback method
                const textarea = document.createElement('textarea');
                textarea.value = urls;
                textarea.style.position = 'fixed';
                textarea.style.opacity = '0';
                document.body.appendChild(textarea);
                textarea.select();
                document.execCommand('copy');
                document.body.removeChild(textarea);

                const originalText = copyBtn.textContent;
                copyBtn.textContent = "✓ Copied!";
                setTimeout(() => {
                    copyBtn.textContent = originalText;
                }, 2000);
            }
        };

        // Close button
        closeBtn.onclick = () => {
            dialog.close();
            dialog.remove();
            console.error = originalError; // Restore original console.error
        };

        // Log summary to console
        console.log(`
========================================
YouTube RSS Extractor - Summary
========================================
✓ Extracted: ${channels.length} channels
✗ Failed: ${failedChannels.length} channels

Feed URLs:
${channels.map(ch => ch.feedUrl).join('\n')}
========================================
        `);

    } catch (error) {
        console.error('YouTube RSS Extractor Error:', error);
        statusLabel.textContent = `❌ Error: ${error.message}`;
        statusLabel.style.color = '#ff0000';

        // Add a small text below for console notice
        const consoleNote = document.createElement('small');
        consoleNote.textContent = 'Check console for details';
        consoleNote.style.cssText = 'color: #999; display: block; margin-top: 5px;';
        statusLabel.appendChild(consoleNote);

        buttonContainer.style.display = 'flex';
        progressContainer.style.display = 'none';

        closeBtn.onclick = () => {
            dialog.close();
            dialog.remove();
            console.error = originalError; // Restore original console.error
        };
    }

    // Auto-run check for page navigation
    if (sessionStorage.getItem('autoRunRSSExtractor') === 'true' && 
        window.location.pathname.includes('/feed/channels')) {
        // Script has navigated to the correct page, will auto-continue
    }
})();