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:
How to Use It
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
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
}
})();