您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Scan and save Reddit videos on mobile Safari with native iOS save compatibility and touch-friendly UI
当前为
// ==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(); } })();