// ==UserScript==
// @name Reddit Video Downloader Pro
// @namespace http://tampermonkey.net/
// @version 2.1.0
// @description Professional Reddit video downloader with multiple extraction methods and intuitive UI
// @author RedditVideoDownloader
// @match https://www.reddit.com/*
// @match https://old.reddit.com/*
// @match https://new.reddit.com/*
// @match https://i.redd.it/*
// @match https://v.redd.it/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CONFIG = {
debug: GM_getValue('debug', false),
autoDetect: GM_getValue('autoDetect', true),
preferredQuality: GM_getValue('preferredQuality', 'highest'),
downloadPath: GM_getValue('downloadPath', 'Downloads/Reddit'),
showNotifications: GM_getValue('showNotifications', true),
buttonStyle: GM_getValue('buttonStyle', 'modern')
};
// Utility functions
const utils = {
log: function(message, type = 'info') {
if (!CONFIG.debug && type === 'debug') return;
const prefix = `[Reddit Video DL] [${type.toUpperCase()}]`;
console.log(`${prefix} ${message}`);
},
notify: function(message, type = 'info') {
if (!CONFIG.showNotifications) return;
if (typeof GM_notification !== 'undefined') {
GM_notification({
title: 'Reddit Video Downloader',
text: message,
timeout: 3000,
onclick: () => window.focus()
});
} else {
// Fallback for mobile Safari
this.showToast(message, type);
}
},
showToast: function(message, type = 'info') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'error' ? '#f44336' : type === 'success' ? '#4CAF50' : '#2196F3'};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10001;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 300px;
word-wrap: break-word;
animation: slideIn 0.3s ease-out;
`;
// Add animation keyframes
if (!document.querySelector('#toast-keyframes')) {
const style = document.createElement('style');
style.id = 'toast-keyframes';
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
}
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s ease-out reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
},
sanitizeFilename: function(filename) {
return filename.replace(/[^\w\s.-]/gi, '_').replace(/\s+/g, '_');
},
formatFileSize: function(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
getPostId: function(url = window.location.href) {
const matches = url.match(/\/comments\/([a-z0-9]+)/i);
return matches ? matches[1] : null;
},
makeRequest: function(url, options = {}) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: options.method || 'GET',
url: url,
headers: options.headers || {},
onload: response => resolve(response),
onerror: error => reject(error),
...options
});
} else {
// Fallback for environments without GM_xmlhttpRequest
fetch(url, options)
.then(response => resolve({
status: response.status,
responseText: response.text(),
response: response
}))
.catch(reject);
}
});
}
};
// Video extraction methods
const extractors = {
// Method 1: Reddit JSON API
extractFromAPI: async function(postId) {
try {
utils.log(`Extracting video from API for post: ${postId}`, 'debug');
const apiUrl = `https://www.reddit.com/comments/${postId}.json`;
const response = await utils.makeRequest(apiUrl);
if (response.status !== 200) {
throw new Error(`API request failed: ${response.status}`);
}
const data = JSON.parse(response.responseText);
const post = data[0].data.children[0].data;
// Check multiple possible video locations
let videoData = null;
// Reddit hosted video
if (post.secure_media?.reddit_video) {
videoData = {
videoUrl: post.secure_media.reddit_video.fallback_url,
audioUrl: post.secure_media.reddit_video.fallback_url.replace('DASH_', 'DASH_audio_'),
duration: post.secure_media.reddit_video.duration,
width: post.secure_media.reddit_video.width,
height: post.secure_media.reddit_video.height,
hasAudio: post.secure_media.reddit_video.has_audio
};
}
// Alternative location
if (!videoData && post.media?.reddit_video) {
videoData = {
videoUrl: post.media.reddit_video.fallback_url,
audioUrl: post.media.reddit_video.fallback_url.replace('DASH_', 'DASH_audio_'),
duration: post.media.reddit_video.duration,
width: post.media.reddit_video.width,
height: post.media.reddit_video.height,
hasAudio: post.media.reddit_video.has_audio
};
}
// Crosspost check
if (!videoData && post.crosspost_parent_list?.length > 0) {
const crosspost = post.crosspost_parent_list[0];
if (crosspost.secure_media?.reddit_video) {
videoData = {
videoUrl: crosspost.secure_media.reddit_video.fallback_url,
audioUrl: crosspost.secure_media.reddit_video.fallback_url.replace('DASH_', 'DASH_audio_'),
duration: crosspost.secure_media.reddit_video.duration,
width: crosspost.secure_media.reddit_video.width,
height: crosspost.secure_media.reddit_video.height,
hasAudio: crosspost.secure_media.reddit_video.has_audio
};
}
}
if (videoData) {
videoData.title = post.title;
videoData.subreddit = post.subreddit;
videoData.author = post.author;
videoData.extractionMethod = 'API';
}
return videoData;
} catch (error) {
utils.log(`API extraction failed: ${error.message}`, 'error');
return null;
}
},
// Method 2: DOM analysis
extractFromDOM: function() {
try {
utils.log('Extracting video from DOM', 'debug');
// Look for video elements
const videoElements = document.querySelectorAll('video');
for (const video of videoElements) {
if (video.src || video.currentSrc) {
const videoUrl = video.src || video.currentSrc;
if (videoUrl.includes('v.redd.it') || videoUrl.includes('reddit.com')) {
return {
videoUrl: videoUrl,
width: video.videoWidth,
height: video.videoHeight,
duration: video.duration,
extractionMethod: 'DOM'
};
}
}
}
// Look for iframe embeds
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
const src = iframe.src;
if (src.includes('v.redd.it')) {
return {
videoUrl: src,
extractionMethod: 'DOM-iframe'
};
}
}
return null;
} catch (error) {
utils.log(`DOM extraction failed: ${error.message}`, 'error');
return null;
}
},
// Method 3: Network monitoring
setupNetworkMonitoring: function() {
if (window.videoUrls) return; // Already monitoring
window.videoUrls = new Set();
// Intercept fetch requests
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0];
if (typeof url === 'string' && (url.includes('v.redd.it') || url.includes('reddit.com') && url.includes('video'))) {
window.videoUrls.add(url);
utils.log(`Network captured: ${url}`, 'debug');
}
return originalFetch.apply(this, args);
};
// Intercept XMLHttpRequest
const originalXHR = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(method, url) {
if (typeof url === 'string' && (url.includes('v.redd.it') || url.includes('reddit.com') && url.includes('video'))) {
window.videoUrls.add(url);
utils.log(`XHR captured: ${url}`, 'debug');
}
return originalXHR.apply(this, arguments);
};
},
// Method 4: URL pattern matching
extractFromURL: function(url = window.location.href) {
try {
// Direct v.redd.it links
if (url.includes('v.redd.it')) {
return {
videoUrl: url,
extractionMethod: 'URL-direct'
};
}
// Extract from Reddit post URL structure
const postId = utils.getPostId(url);
if (postId) {
// Try common v.redd.it patterns
const possibleUrls = [
`https://v.redd.it/${postId}/DASH_720.mp4`,
`https://v.redd.it/${postId}/DASH_480.mp4`,
`https://v.redd.it/${postId}/DASH_360.mp4`
];
return {
videoUrl: possibleUrls[0], // Return highest quality guess
alternativeUrls: possibleUrls,
extractionMethod: 'URL-pattern'
};
}
return null;
} catch (error) {
utils.log(`URL extraction failed: ${error.message}`, 'error');
return null;
}
}
};
// Download manager
const downloader = {
downloadVideo: async function(videoData, options = {}) {
try {
const filename = options.filename || this.generateFilename(videoData);
const url = videoData.videoUrl;
utils.log(`Starting download: ${filename}`, 'info');
utils.notify('Starting video download...', 'info');
if (typeof GM_download !== 'undefined') {
// Use GM_download if available (Tampermonkey)
GM_download(url, filename, {
onerror: (error) => {
utils.log(`Download failed: ${error}`, 'error');
utils.notify('Download failed!', 'error');
},
onload: () => {
utils.log('Download completed successfully', 'success');
utils.notify('Video downloaded successfully!', 'success');
}
});
} else {
// Fallback method for mobile Safari
await this.downloadFallback(url, filename);
}
// Download audio separately if available
if (videoData.hasAudio && videoData.audioUrl && options.includeAudio) {
setTimeout(() => {
const audioFilename = filename.replace(/\.[^/.]+$/, '_audio.mp4');
if (typeof GM_download !== 'undefined') {
GM_download(videoData.audioUrl, audioFilename);
}
}, 1000);
}
} catch (error) {
utils.log(`Download error: ${error.message}`, 'error');
utils.notify('Download failed!', 'error');
}
},
downloadFallback: async function(url, filename) {
try {
// Create download link for mobile Safari
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.target = '_blank';
// For mobile Safari, we need to handle the download differently
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
// Open in new tab for iOS
window.open(url, '_blank');
utils.notify('Video opened in new tab. Long press to save.', 'info');
} else {
// Standard download for other browsers
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
} catch (error) {
utils.log(`Fallback download failed: ${error.message}`, 'error');
// Last resort: copy URL to clipboard
this.copyToClipboard(url);
utils.notify('Video URL copied to clipboard', 'info');
}
},
copyToClipboard: function(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
},
generateFilename: function(videoData) {
const title = videoData.title ? utils.sanitizeFilename(videoData.title) : 'reddit_video';
const subreddit = videoData.subreddit || 'unknown';
const timestamp = new Date().toISOString().slice(0, 10);
return `${subreddit}_${title}_${timestamp}.mp4`.substring(0, 100); // Limit filename length
}
};
// UI components
const ui = {
createDownloadButton: function(videoData) {
const button = document.createElement('button');
button.className = 'reddit-video-dl-btn';
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download Video
`;
button.style.cssText = `
display: inline-flex;
align-items: center;
gap: 6px;
background: linear-gradient(135deg, #FF4500, #FF6B35);
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(255, 69, 0, 0.3);
margin: 4px;
z-index: 1000;
position: relative;
`;
// Hover effects
button.addEventListener('mouseenter', () => {
button.style.transform = 'translateY(-1px)';
button.style.boxShadow = '0 4px 12px rgba(255, 69, 0, 0.4)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'translateY(0)';
button.style.boxShadow = '0 2px 8px rgba(255, 69, 0, 0.3)';
});
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
button.disabled = true;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3" fill="currentColor">
<animateTransform attributeName="transform" type="rotate"
values="0 12 12;360 12 12" dur="1s" repeatCount="indefinite"/>
</circle>
</svg>
Downloading...
`;
try {
await downloader.downloadVideo(videoData);
} finally {
setTimeout(() => {
button.disabled = false;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download Video
`;
}, 2000);
}
});
return button;
},
createQualityMenu: function(videoData) {
if (!videoData.alternativeUrls) return null;
const menu = document.createElement('div');
menu.className = 'reddit-video-quality-menu';
menu.style.cssText = `
position: absolute;
top: 100%;
right: 0;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 8px 0;
min-width: 120px;
z-index: 1001;
display: none;
`;
const qualities = ['720p', '480p', '360p'];
videoData.alternativeUrls.forEach((url, index) => {
const item = document.createElement('div');
item.textContent = qualities[index] || `Quality ${index + 1}`;
item.style.cssText = `
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
color: #333;
`;
item.addEventListener('click', () => {
downloader.downloadVideo({ ...videoData, videoUrl: url });
menu.style.display = 'none';
});
item.addEventListener('mouseenter', () => {
item.style.background = '#f5f5f5';
});
item.addEventListener('mouseleave', () => {
item.style.background = 'transparent';
});
menu.appendChild(item);
});
return menu;
},
insertDownloadButtons: function(videoData) {
// Remove existing buttons
document.querySelectorAll('.reddit-video-dl-btn').forEach(btn => btn.remove());
const button = this.createDownloadButton(videoData);
const qualityMenu = this.createQualityMenu(videoData);
// Find insertion points
const insertionPoints = [
// New Reddit
document.querySelector('[data-testid="post-content"]'),
document.querySelector('div[data-click-id="body"]'),
// Old Reddit
document.querySelector('.usertext-body'),
document.querySelector('.entry'),
// Mobile
document.querySelector('.Post'),
document.querySelector('[data-testid="post_author_link"]')?.parentElement,
// Fallback - any video element
document.querySelector('video')?.parentElement
].filter(Boolean);
if (insertionPoints.length > 0) {
const container = document.createElement('div');
container.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
position: relative;
`;
container.appendChild(button);
if (qualityMenu) {
container.appendChild(qualityMenu);
// Toggle quality menu
button.addEventListener('contextmenu', (e) => {
e.preventDefault();
qualityMenu.style.display = qualityMenu.style.display === 'block' ? 'none' : 'block';
});
}
insertionPoints[0].appendChild(container);
utils.log(`Download button inserted using method: ${videoData.extractionMethod}`, 'debug');
return true;
}
return false;
}
};
// Main functionality
const main = {
init: function() {
utils.log('Initializing Reddit Video Downloader Pro', 'info');
// Setup network monitoring
extractors.setupNetworkMonitoring();
// Initial scan
this.scanForVideos();
// Monitor for navigation changes (SPA)
this.setupNavigationMonitoring();
// Setup mutation observer for dynamic content
this.setupMutationObserver();
},
scanForVideos: async function() {
utils.log('Scanning for videos...', 'debug');
const postId = utils.getPostId();
if (!postId) {
utils.log('No post ID found, skipping scan', 'debug');
return;
}
let videoData = null;
// Try multiple extraction methods
videoData = await extractors.extractFromAPI(postId);
if (!videoData) {
videoData = extractors.extractFromDOM();
}
if (!videoData) {
videoData = extractors.extractFromURL();
}
if (videoData) {
utils.log(`Video found: ${videoData.extractionMethod}`, 'success');
// Insert download button
const inserted = ui.insertDownloadButtons(videoData);
if (!inserted) {
utils.log('Failed to insert download button', 'warn');
}
} else {
utils.log('No video found on this page', 'debug');
}
},
setupNavigationMonitoring: function() {
let currentUrl = window.location.href;
// Monitor URL changes
const urlObserver = new MutationObserver(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
utils.log('Navigation detected, rescanning...', 'debug');
setTimeout(() => {
this.scanForVideos();
}, 1000);
}
});
urlObserver.observe(document.body, {
childList: true,
subtree: true
});
// Also listen for popstate events
window.addEventListener('popstate', () => {
setTimeout(() => {
this.scanForVideos();
}, 1000);
});
},
setupMutationObserver: function() {
const observer = new MutationObserver((mutations) => {
let shouldRescan = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check if new video elements were added
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'VIDEO' || node.querySelector('video')) {
shouldRescan = true;
break;
}
}
}
}
});
if (shouldRescan) {
utils.log('New video content detected, rescanning...', 'debug');
setTimeout(() => {
this.scanForVideos();
}, 500);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => main.init());
} else {
main.init();
}
// Global access for debugging
window.RedditVideoDownloader = {
main,
extractors,
downloader,
ui,
utils,
CONFIG
};
})();