您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto scan and download Reddit videos on iOS Safari with touch-friendly UI and fallback download methods
当前为
// ==UserScript== // @name Reddit Video Saver for iOS Safari // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Auto scan and download Reddit videos on iOS Safari with touch-friendly UI and fallback download methods // @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'; // Helper utilities 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 { // Simple toast fallback for iOS Safari const toast = document.createElement("div"); toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #ff4500; color: white; padding: 12px 16px; border-radius: 8px; 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); `; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } }, openLinkToSave(url) { // Open video URL in new tab - user long press to save media window.open(url, '_blank'); this.notify('Opened video in new tab - long press the video to save.'); } }; // Main video extractor and UI manager const scraper = { videoData: null, // Store extracted info from current page // Extract video URL(s) from Reddit JSON API or DOM async scanForVideo() { this.videoData = null; try { const postId = this.getPostIdFromUrl(); if(!postId) return null; // Fetch Reddit JSON API data about the post const apiUrl = `https://www.reddit.com/comments/${postId}.json`; const response = await fetch(apiUrl); const json = await response.json(); if(!json || !json[0] || !json[0].data.children.length) return null; const postData = json[0].data.children[0].data; // Check if Reddit video is available if(postData.secure_media?.reddit_video) { const videoUrl = postData.secure_media.reddit_video.fallback_url; const title = postData.title || 'reddit_video'; this.videoData = { url: videoUrl, title: utils.sanitizeFilename(title) }; return this.videoData; } } catch(e) { // Fallback: try find video element in DOM const videoEl = document.querySelector('video'); if(videoEl && (videoEl.src || videoEl.currentSrc)) { this.videoData = { url: videoEl.currentSrc || videoEl.src, title: 'reddit_video' }; return this.videoData; } } }, getPostIdFromUrl() { const match = window.location.href.match(/\/comments\/([a-z0-9]+)/i); return match ? match[1] : null; }, // Create UI container for the two fixed buttons 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: 4px; left: 50%; transform: translateX(-50%); background: rgba(255, 69, 0, 0.95); border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); z-index: 100000; display: flex; gap: 12px; padding: 8px 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -webkit-user-select:none; `; document.body.appendChild(container); return container; }, // Build scan and download buttons with event handlers buildButtons() { const container = this.createUi(); container.innerHTML = ''; // clear old buttons // Scan button: scans current page for video const scanBtn = document.createElement('button'); scanBtn.textContent = 'Scan Current Page for Media'; this.styleButton(scanBtn); scanBtn.onclick = async () => { scanBtn.disabled = true; scanBtn.textContent = 'Scanning...'; await this.scanForVideo(); if(this.videoData && this.videoData.url) { utils.notify('Media found on page!'); } else { utils.notify('No media found on this page.'); } scanBtn.textContent = 'Scan Current Page for Media'; scanBtn.disabled = false; }; container.appendChild(scanBtn); // Save button: saves video currently detected const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save Media on Page'; this.styleButton(saveBtn); saveBtn.onclick = async () => { if(!this.videoData || !this.videoData.url) { utils.notify('No media detected yet! Tap "Scan" first.'); return; } saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; // Try GM_download if available, fallback open tab if(typeof GM_download === 'function') { try { GM_download({ url: this.videoData.url, name: this.videoData.title + '.mp4', onerror: () => { utils.notify('Download failed, opening video in new tab.'); utils.openLinkToSave(this.videoData.url); }, onload: () => utils.notify('Download started.') }); } catch(e) { utils.openLinkToSave(this.videoData.url); } } else { utils.openLinkToSave(this.videoData.url); } setTimeout(() => { saveBtn.textContent = 'Save Media on Page'; saveBtn.disabled = false; }, 1500); }; container.appendChild(saveBtn); }, styleButton(btn) { btn.style.cssText = ` background-color: white; color: rgb(255, 69, 0); font-weight: 700; font-size: 14px; padding: 8px 12px; border-radius: 8px; border: none; cursor: pointer; user-select:none; min-width: 120px; box-shadow: 0 2px 8px rgba(255,69,0,0.4); transition: background-color 0.3s ease; `; 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'); }, // Set periodic rescans for SPA navigation or content changes setRescanOnNavigation() { let lastUrl = location.href; new MutationObserver(() => { if(location.href !== lastUrl) { lastUrl = location.href; this.scanForVideo(); } }).observe(document.body, {childList: true, subtree: true}); }, init() { this.buildButtons(); this.scanForVideo(); this.setRescanOnNavigation(); } }; // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => scraper.init()); } else { scraper.init(); } })();