您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a single Download button on Reddit video player in feed, always best MP4
// ==UserScript== // @name Reddit Video Download (Best Quality on Player) // @description Adds a single Download button on Reddit video player in feed, always best MP4 // @version 0.6 // @author GPT // @match https://www.reddit.com/* // @match https://*.reddit.com/* // @grant GM_xmlhttpRequest // @grant GM_download // @license MIT // @namespace https://greasyfork.org/users/1203953 // ==/UserScript== (function () { "use strict"; const BTN_CLASS = "rvdl-btn"; const sanitizeFilename = (s) => (s || "reddit-video") .trim() .replace(/[\\/:*?"<>|]+/g, "-") .replace(/\s+/g, " ") .slice(0, 120) || "reddit-video"; const gmDownload = (url, filename) => new Promise((resolve, reject) => { if (typeof GM_download === "function") { GM_download({ url, name: filename, ontimeout: reject, onerror: reject, onload: resolve, }); } else { fetch(url) .then((r) => r.blob()) .then((blob) => { const a = document.createElement("a"); const link = URL.createObjectURL(blob); a.href = link; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(link); a.remove(); }, 0); resolve(); }) .catch(reject); } }); const parsePackagedMedia = (player) => { const raw = player.getAttribute("packaged-media-json"); if (!raw) return []; try { const text = raw.replace(/"/g, '"'); const json = JSON.parse(text); const perms = json?.playbackMp4s?.permutations || []; return perms .map((p) => ({ url: p?.source?.url, width: p?.source?.dimensions?.width, height: p?.source?.dimensions?.height, })) .filter((x) => x.url) .sort((a, b) => b.width - a.width); } catch (e) { console.warn("[RVDL] parse error", e); return []; } }; const insertButton = (post, player, options, title) => { if (player.dataset.rvdlDone === "1") return; player.dataset.rvdlDone = "1"; const best = options[0]; if (!best) return; // оборачиваем контейнер для позиционирования const host = player.closest("div[slot='post-media-container']") || player.parentElement; if (!host) return; host.style.position = host.style.position || "relative"; const btn = document.createElement("button"); btn.className = BTN_CLASS; btn.textContent = "Download"; btn.title = `Download best quality (${best.width}x${best.height})`; btn.addEventListener("click", async (e) => { e.stopPropagation(); e.preventDefault(); try { const filename = `${sanitizeFilename(title)}_${best.width}x${best.height}.mp4`; await gmDownload(best.url, filename); } catch (err) { alert("Download failed: " + err); } }); host.appendChild(btn); }; const ensureStyle = () => { if (document.getElementById("rvdl-style")) return; const style = document.createElement("style"); style.id = "rvdl-style"; style.textContent = ` .${BTN_CLASS} { position: absolute; top: 8px; right: 8px; background: #000; color: #fff; border: 1px solid #444; border-radius: 6px; padding: 4px 10px; font-size: 13px; font-weight: 500; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; cursor: pointer; opacity: 0.85; } .${BTN_CLASS}:hover { background: #222; opacity: 1; } `; document.head.appendChild(style); }; const scan = () => { document.querySelectorAll("shreddit-post[post-type='video']").forEach((post) => { const player = post.querySelector("shreddit-player-2, shreddit-player"); if (!player) return; const opts = parsePackagedMedia(player); if (!opts.length) return; const title = post.getAttribute("post-title") || post.querySelector('a[slot="title"]')?.textContent || "reddit-video"; insertButton(post, player, opts, title); }); }; const init = () => { ensureStyle(); scan(); const mo = new MutationObserver(scan); mo.observe(document.body, { childList: true, subtree: true }); }; document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", init) : init(); })();