// ==UserScript==
// @name Reddit Video Saver iOS Safari Enhanced
// @namespace http://tampermonkey.net/
// @version 3.0.0
// @description Scan and save Reddit videos on mobile Safari with native iOS save compatibility and touch-friendly UI
// @author ChatGPT-Pro
// @match https://www.reddit.com/*
// @grant GM_download
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const utils = {
sanitizeFilename(name) {
return name.replace(/[^\w\s.-]/g, '_').slice(0, 80);
},
notify(msg) {
if(typeof GM_notification !== 'undefined') {
GM_notification({title: 'Reddit Video Saver', text: msg, timeout: 3000});
} else {
const toast = document.createElement("div");
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #ff4500;
color: white;
padding: 12px 16px;
border-radius: 12px;
z-index: 99999;
font-size: 16px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 5px 15px rgba(0,0,0, 0.3);
max-width: 90vw;
word-wrap: break-word;
`;
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
},
openLinkToSave(url) {
// iOS Safari: open in new tab, instruct long-press to save
window.open(url, '_blank', 'noopener');
this.notify('Opened video in new tab - long press the video to save.');
}
};
const scraper = {
videoData: null,
getPostIdFromUrl() {
const match = window.location.href.match(/\/comments\/([a-z0-9]+)/i);
return match ? match[1] : null;
},
fetchRedditJson(postId) {
return new Promise((resolve, reject) => {
if(typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'GET',
url: `https://www.reddit.com/comments/${postId}.json`,
headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' },
onload: res => {
if(res.status === 200) {
try {
resolve(JSON.parse(res.responseText));
} catch(e) {
reject(e);
}
} else reject(new Error(`Status ${res.status}`));
},
onerror: err => reject(err)
});
} else {
// fallback to fetch but may have CORS issues on iOS Safari
fetch(`https://www.reddit.com/comments/${postId}.json`)
.then(r => r.json())
.then(resolve)
.catch(reject);
}
});
},
async scanForVideo() {
this.videoData = null;
console.log("[Reddit Saver] Scanning for video...");
const postId = this.getPostIdFromUrl();
if(!postId) {
console.warn("[Reddit Saver] No post ID found in URL.");
return null;
}
try {
const json = await this.fetchRedditJson(postId);
if(!json || !json[0]?.data?.children?.length) throw new Error('Invalid JSON structure');
const post = json[0].data.children[0].data;
// Try secure_media or media reddit_video entries
let videoUrl = post.secure_media?.reddit_video?.fallback_url || post.media?.reddit_video?.fallback_url;
// If crosspost exists, check there
if(!videoUrl && post.crosspost_parent_list?.length) {
const cross = post.crosspost_parent_list[0];
videoUrl = cross?.secure_media?.reddit_video?.fallback_url || cross?.media?.reddit_video?.fallback_url;
}
if(videoUrl) {
this.videoData = {
url: videoUrl,
title: utils.sanitizeFilename(post.title || 'reddit_video'),
};
console.log("[Reddit Saver] Video found:", this.videoData);
utils.notify('Video detected on this Reddit post.');
return this.videoData;
}
} catch(e) {
console.error("[Reddit Saver] Fetch or parse error:", e);
}
// Try fallback: select visible video in DOM
const videoEl = document.querySelector('video');
if(videoEl && (videoEl.src || videoEl.currentSrc)) {
this.videoData = {
url: videoEl.currentSrc || videoEl.src,
title: 'reddit_video_dom',
};
console.log("[Reddit Saver] Video found in DOM:", this.videoData);
utils.notify('Video found via DOM scanning.');
return this.videoData;
}
utils.notify('No video found on current page.');
return null;
},
createUi() {
let container = document.getElementById('redditsaver-ui');
if(container) return container;
container = document.createElement('div');
container.id = 'redditsaver-ui';
container.style.cssText = `
position: fixed; top: 0; left: 0; right: 0;
background: rgba(255, 69, 0, 0.95);
padding: 10px 0;
z-index: 9999999999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex; justify-content: center; gap: 15px;
user-select: none;
-webkit-user-select: none;
`;
document.body.appendChild(container);
return container;
},
addButtons() {
const container = this.createUi();
container.innerHTML = '';
const scanBtn = document.createElement('button');
scanBtn.textContent = 'Scan Current Page for Media';
this.styleButton(scanBtn);
scanBtn.ontouchstart = scanBtn.onclick = async (e) => {
e.preventDefault();
scanBtn.disabled = true;
scanBtn.textContent = 'Scanning...';
await this.scanForVideo();
scanBtn.textContent = 'Scan Current Page for Media';
scanBtn.disabled = false;
};
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save Media on Page';
this.styleButton(saveBtn);
saveBtn.ontouchstart = saveBtn.onclick = async (e) => {
e.preventDefault();
if(!this.videoData || !this.videoData.url) {
utils.notify('No media detected yet! Tap "Scan" first.');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
try {
if(typeof GM_download === 'function') {
GM_download({
url: this.videoData.url,
name: this.videoData.title + '.mp4',
onerror: () => {
utils.notify('Download error, opening video in new tab...');
utils.openLinkToSave(this.videoData.url);
},
onload: () => utils.notify('Download started.')
});
} else {
utils.openLinkToSave(this.videoData.url);
}
} catch(e) {
console.error(e);
utils.openLinkToSave(this.videoData.url);
}
setTimeout(() => {
saveBtn.textContent = 'Save Media on Page';
saveBtn.disabled = false;
}, 1500);
};
container.appendChild(scanBtn);
container.appendChild(saveBtn);
},
styleButton(btn) {
btn.style.cssText = `
background-color: white;
color: rgb(255, 69, 0);
font-weight: 700;
font-size: 16px;
padding: 12px 18px;
border-radius: 12px;
border: none;
cursor: pointer;
min-width: 150px;
box-shadow: 0 4px 15px rgba(255,69,0,0.5);
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
user-select:none;
`;
btn.addEventListener('touchstart', () => btn.style.backgroundColor = 'rgba(255,69,0,0.1)');
btn.addEventListener('touchend', () => btn.style.backgroundColor = 'white');
btn.addEventListener('mouseenter', () => btn.style.backgroundColor = 'rgba(255,69,0,0.15)');
btn.addEventListener('mouseleave', () => btn.style.backgroundColor = 'white');
},
rescanOnNavigation() {
let lastURL = location.href;
new MutationObserver(() => {
if(location.href !== lastURL) {
lastURL = location.href;
console.log("[Reddit Saver] URL changed, rescanning...");
this.scanForVideo();
}
}).observe(document.body, {childList: true, subtree: true});
window.addEventListener('popstate', () => {
setTimeout(() => this.scanForVideo(), 500);
});
},
init() {
console.log("[Reddit Saver] Initializing UI and scanning...");
this.addButtons();
this.scanForVideo();
this.rescanOnNavigation();
}
};
if(document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => scraper.init());
} else {
scraper.init();
}
})();