// ==UserScript== // @name YT-DLP Download Helper // @namespace https://workbuddy.ai // @version 1.0.0 // @description 把当前视频页 URL 推送给本地 yt-dlp HTTP 服务,由服务端真正拉取视频。多平台通用。 // @author WorkBuddy // @license MIT // @match *://*.youtube.com/* // @match *://*.youtu.be/* // @match *://*.bilibili.com/* // @match *://*.iqiyi.com/* // @match *://*.v.qq.com/* // @match *://*.youku.com/* // @match *://*.mgtv.com/* // @match *://*.miguvideo.com/* // @match *://*.pptv.com/* // @match *://*.ixigua.com/* // @match *://*.cctv.com/* // @match *://*.cntv.cn/* // @match *://*.twitter.com/* // @match *://*.x.com/* // @match *://*.vimeo.com/* // @match *://*.tiktok.com/* // @match *://*.douyin.com/* // @match *://*.facebook.com/* // @match *://*.instagram.com/* // @match *://*.twitch.tv/* // @match *://*.dailymotion.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @connect 127.0.0.1 // @connect localhost // @connect * // @run-at document-idle // ==/UserScript== (function () { "use strict"; // ---- 配置 ---- const DEFAULT_SERVER = "http://127.0.0.1:8080"; const FORMATS = [ { key: "best", label: "最佳画质 (视频+音频)" }, { key: "1080p", label: "1080p" }, { key: "720p", label: "720p" }, { key: "480p", label: "480p" }, { key: "audio", label: "仅音频 (MP3)" }, ]; let SERVER = (GM_getValue("ytdlp_server") || DEFAULT_SERVER).replace(/\/+$/, ""); let pollTimer = null; // 全局快捷键:Alt+Shift+D 直接下载当前视频(在输入框内不触发) const HOTKEY = { ctrl: false, alt: true, shift: true, meta: false, key: "D" }; const HOTKEY_LABEL = "Alt+Shift+D"; // ---- 工具:发起请求 ---- function api(method, path, body, cb) { GM_xmlhttpRequest({ method: method, url: SERVER + path, headers: { "Content-Type": "application/json" }, data: body ? JSON.stringify(body) : undefined, timeout: 15000, onload: function (r) { let obj; try { obj = JSON.parse(r.responseText); } catch (e) { obj = { error: r.responseText || ("HTTP " + r.status) }; } cb(obj, r.status); }, onerror: function (e) { cb({ error: "网络错误:本地服务未启动或被拦截 (" + (e.error || "") + ")" }, 0); }, ontimeout: function () { cb({ error: "请求超时:本地服务无响应" }, 0); }, }); } function currentVideoUrl() { // 去掉 hash,保留干净的页面地址 return location.href.split("#")[0]; } // ---- 样式 ---- GM_addStyle(` #ytdlp-root { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; font-family: -apple-system, "Segoe UI", Roboto, "Microsoft YaHei", sans-serif; font-size: 13px; } #ytdlp-fab { width: 46px; height: 46px; border-radius: 50%; background: #ff4d4f; color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.3); font-weight: 700; font-size: 16px; user-select: none; } #ytdlp-panel { display: none; width: 300px; background: #fff; border: 1px solid #e5e5e5; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.25); padding: 14px; color: #222; } #ytdlp-panel.open { display: block; } #ytdlp-panel h3 { margin: 0 0 10px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; } #ytdlp-panel h3 .x { cursor: pointer; color: #999; font-weight: 400; } #ytdlp-panel label { display: block; margin: 8px 0 3px; color: #666; } #ytdlp-panel input, #ytdlp-panel select { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 13px; } #ytdlp-send { margin-top: 12px; width: 100%; padding: 9px; border: 0; border-radius: 8px; cursor: pointer; background: #ff4d4f; color: #fff; font-weight: 600; font-size: 14px; } #ytdlp-send:disabled { background: #ffb3b3; cursor: default; } #ytdlp-status { margin-top: 10px; padding: 8px; border-radius: 6px; background: #f5f5f5; min-height: 18px; white-space: pre-wrap; word-break: break-all; line-height: 1.5; } #ytdlp-status.err { background: #fff1f0; color: #cf1322; } #ytdlp-status.ok { background: #f6ffed; color: #389e0d; } #ytdlp-hint { margin-top: 8px; font-size: 12px; color: #999; } #ytdlp-hint b { color: #ff4d4f; } #ytdlp-bar { height: 6px; width: 100%; background: #f0f0f0; border-radius: 4px; margin-top: 8px; overflow: hidden; display: none; } #ytdlp-bar > i { display: block; height: 100%; width: 0%; background: #52c41a; transition: width .4s; } #ytdlp-foot { margin-top: 10px; font-size: 12px; color: #999; display: flex; justify-content: space-between; } #ytdlp-foot a { color: #1677ff; cursor: pointer; text-decoration: none; } `); // ---- UI ---- const root = document.createElement("div"); root.id = "ytdlp-root"; root.innerHTML = `

YT-DLP 下载助手

就绪。当前页面:
${currentVideoUrl()}
快捷键 ${HOTKEY_LABEL}:直接下载当前视频
${currentVideoUrl().slice(0, 38)}… 服务面板 ↗
`; document.body.appendChild(root); const fab = root.querySelector("#ytdlp-fab"); const panel = root.querySelector("#ytdlp-panel"); const serverInput = root.querySelector("#ytdlp-server"); const fmtSel = root.querySelector("#ytdlp-fmt"); const extraInput = root.querySelector("#ytdlp-extra"); const sendBtn = root.querySelector("#ytdlp-send"); const statusEl = root.querySelector("#ytdlp-status"); const bar = root.querySelector("#ytdlp-bar"); const barFill = root.querySelector("#ytdlp-bar > i"); const openLink = root.querySelector("#ytdlp-open"); function setStatus(msg, cls) { statusEl.className = "ytdlp-status" + (cls ? " " + cls : ""); statusEl.textContent = msg; } fab.addEventListener("click", () => panel.classList.add("open")); panel.querySelector(".x").addEventListener("click", () => panel.classList.remove("open")); serverInput.addEventListener("change", () => { SERVER = serverInput.value.trim().replace(/\/+$/, ""); GM_setValue("ytdlp_server", SERVER); }); openLink.addEventListener("click", () => window.open(SERVER + "/", "_blank")); function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } function pollTask(taskId) { stopPolling(); pollTimer = setInterval(() => { api("GET", "/api/task/" + taskId, null, (obj) => { if (obj.error) { setStatus(obj.error, "err"); stopPolling(); return; } const st = obj.status; if (st === "downloading" || st === "queued") { const pct = (obj.progress != null) ? obj.progress.toFixed(1) + "%" : (st === "queued" ? "排队中" : "下载中"); setStatus((obj.filename ? obj.filename + "\n" : "") + pct + (obj.log ? "\n" + obj.log : ""), ""); bar.style.display = "block"; barFill.style.width = (obj.progress != null ? obj.progress : 0) + "%"; } else if (st === "finished") { setStatus("✅ 下载完成!\n" + (obj.filename || obj.url), "ok"); bar.style.display = "block"; barFill.style.width = "100%"; stopPolling(); sendBtn.disabled = false; } else if (st === "error") { setStatus("❌ 失败:" + (obj.error || obj.log || "未知错误"), "err"); stopPolling(); sendBtn.disabled = false; } else if (st === "cancelled") { setStatus("已取消", ""); stopPolling(); sendBtn.disabled = false; } }); }, 1500); } sendBtn.addEventListener("click", doSend); function doSend() { SERVER = serverInput.value.trim().replace(/\/+$/, ""); GM_setValue("ytdlp_server", SERVER); const payload = { url: currentVideoUrl(), format: fmtSel.value, extra_args: extraInput.value.trim(), }; sendBtn.disabled = true; setStatus("正在提交到本地服务…", ""); bar.style.display = "none"; api("POST", "/api/download", payload, (obj, code) => { if (obj.error || (code !== 200 && !obj.task_id)) { setStatus("❌ " + (obj.error || ("HTTP " + code)), "err"); sendBtn.disabled = false; return; } setStatus("已加入队列,任务 ID: " + obj.task_id + "\n等待服务拉取…", ""); pollTask(obj.task_id); }); } // 全局快捷键:在视频页直接按 HOTKEY 触发下载 document.addEventListener("keydown", function (e) { if (e.altKey !== !!HOTKEY.alt) return; if (e.ctrlKey !== !!HOTKEY.ctrl) return; if (e.shiftKey !== !!HOTKEY.shift) return; if (e.metaKey !== !!HOTKEY.meta) return; if (e.key.toUpperCase() !== HOTKEY.key.toUpperCase()) return; const t = e.target; if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) return; e.preventDefault(); panel.classList.add("open"); doSend(); }); // URL 变化时刷新提示 let lastUrl = location.href; setInterval(() => { if (location.href !== lastUrl) { lastUrl = location.href; const u = currentVideoUrl(); root.querySelector("#ytdlp-url").textContent = u.slice(0, 38) + "…"; root.querySelector("#ytdlp-url").title = u; if (panel.classList.contains("open")) { setStatus("就绪。当前页面:\n" + u, ""); } } }, 2000); })();