// ==UserScript== // @name 网页调用MPV播放(视频页面专用版) // @namespace http://tampermonkey.net/ // @version 4.8 // @description 在视频页面左下角添加播放/设置按钮,全屏时自动隐藏,支持油猴菜单打开设置 // @author DeepSeek // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; const DEBUG = true; function log(...args) { if (DEBUG) console.log('[MPV脚本]', ...args); } // 默认匹配规则(仅B站和YouTube) const defaultRules = [ 'https://www.bilibili.com/video/', 'https://www.youtube.com/watch' ]; let config = { mpvProtocol: GM_getValue('mpvProtocol', 'mpv://'), customArgs: GM_getValue('customArgs', ''), urlRules: GM_getValue('urlRules', defaultRules) }; let currentPlayBtn = null; let currentSettingBtn = null; let lastUrl = ''; let observer = null; let urlCheckInterval = null; let settingWindowOpen = false; // 判断当前页面是否匹配任意规则 function isVideoPage() { const currentUrl = window.location.href; if (!config.urlRules || config.urlRules.length === 0) return false; return config.urlRules.some(rule => { if (!rule) return false; if (rule.startsWith('/') && rule.endsWith('/')) { try { const regex = new RegExp(rule.slice(1, -1)); return regex.test(currentUrl); } catch(e) { log('无效的正则表达式:', rule); return false; } } return currentUrl.startsWith(rule); }); } // 全屏状态变化处理 function handleFullscreenChange() { const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); if (currentPlayBtn && currentSettingBtn) { if (isFullscreen) { currentPlayBtn.style.display = 'none'; currentSettingBtn.style.display = 'none'; log('全屏模式,按钮已隐藏'); } else { currentPlayBtn.style.display = 'flex'; currentSettingBtn.style.display = 'flex'; log('退出全屏,按钮已恢复'); } } } // 注册全屏事件监听(兼容各浏览器) function addFullscreenListener() { const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']; events.forEach(event => { document.addEventListener(event, handleFullscreenChange); }); } function createPlayButton() { const btn = document.createElement('button'); btn.textContent = '▶'; btn.title = '用MPV播放当前页面视频'; btn.style.cssText = ` position: fixed; bottom: 20px; left: 20px; z-index: 999999; width: 48px; height: 48px; background: #a855f7; color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 22px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: all 0.2s ease; `; btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.05)'; btn.style.background = '#c084fc'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'scale(1)'; btn.style.background = '#a855f7'; }); btn.addEventListener('click', playWithMPV); return btn; } function createSettingButton() { const btn = document.createElement('button'); btn.textContent = '⚙️'; btn.title = 'MPV播放设置'; btn.style.cssText = ` position: fixed; bottom: 20px; left: 76px; z-index: 999999; background: transparent; border: none; cursor: pointer; font-size: 28px; padding: 6px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; color: #4b5563; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); `; btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.1)'; btn.style.color = '#a855f7'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'scale(1)'; btn.style.color = '#4b5563'; }); btn.addEventListener('click', openSetting); return btn; } function removeButtons() { if (currentPlayBtn && currentPlayBtn.isConnected) currentPlayBtn.remove(); if (currentSettingBtn && currentSettingBtn.isConnected) currentSettingBtn.remove(); currentPlayBtn = null; currentSettingBtn = null; log('按钮已移除'); } function addButtons() { if (!document.body) { log('body未就绪,等待...'); return false; } if (!isVideoPage()) { removeButtons(); return false; } if (currentPlayBtn && currentSettingBtn) { if (!currentPlayBtn.isConnected || !currentSettingBtn.isConnected) { log('按钮存在但不在DOM中,重新添加'); currentPlayBtn = null; currentSettingBtn = null; } else { return true; } } removeButtons(); currentPlayBtn = createPlayButton(); currentSettingBtn = createSettingButton(); document.body.appendChild(currentPlayBtn); document.body.appendChild(currentSettingBtn); // 添加后立即检查当前全屏状态,防止全屏时按钮显示 handleFullscreenChange(); log('按钮已添加到左下角'); return true; } let updateTimer = null; function updateUI() { if (updateTimer) clearTimeout(updateTimer); updateTimer = setTimeout(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; log('URL变化:', currentUrl); } addButtons(); }, 200); } function observePageChanges() { window.addEventListener('popstate', updateUI); window.addEventListener('hashchange', updateUI); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(this, arguments); updateUI(); }; history.replaceState = function() { originalReplaceState.apply(this, arguments); updateUI(); }; if (urlCheckInterval) clearInterval(urlCheckInterval); urlCheckInterval = setInterval(() => { if (window.location.href !== lastUrl) { log('定时器检测到URL变化'); updateUI(); } }, 1000); if (observer) observer.disconnect(); observer = new MutationObserver(() => { if (document.body && isVideoPage() && (!currentPlayBtn || !currentPlayBtn.isConnected)) { log('检测到body变化,尝试重新添加按钮'); addButtons(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); updateUI(); // 注册全屏事件监听 addFullscreenListener(); } function playWithMPV() { try { const pageUrl = window.location.href; let callUrl = config.mpvProtocol + pageUrl; if (config.customArgs) { callUrl += ' ' + config.customArgs; } window.open(callUrl); log('调用MPV:', callUrl); } catch(e) { alert("调用失败: " + e.message + "\n请先完成协议注册!"); } } // 辅助函数:下载文件 function downloadFile(content, filename) { const windowsContent = content.replace(/\r?\n/g, '\r\n'); const blob = new Blob([windowsContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 全局设置窗口函数 function openSetting() { if (settingWindowOpen) return; settingWindowOpen = true; let tempProtocol = config.mpvProtocol; let tempArgs = config.customArgs; let tempRules = [...config.urlRules]; const mask = document.createElement('div'); mask.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000000; display: flex; align-items: center; justify-content: center; `; const win = document.createElement('div'); win.style.cssText = ` background: white; padding: 24px; border-radius: 20px; width: 550px; max-width: 90%; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 35px rgba(0,0,0,0.3); font-family: system-ui, -apple-system, sans-serif; `; // 标题 const title = document.createElement('h2'); title.textContent = '⚙️ MPV播放设置'; title.style.cssText = 'margin-top:0; margin-bottom:16px; font-size:22px; color:#1f2937;'; win.appendChild(title); // 协议前缀 const protocolDiv = document.createElement('div'); protocolDiv.style.marginBottom = '20px'; const protocolLabel = document.createElement('label'); protocolLabel.textContent = 'MPV自定义协议前缀'; protocolLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;'; const protocolInput = document.createElement('input'); protocolInput.type = 'text'; protocolInput.value = tempProtocol; protocolInput.style.cssText = 'width:100%; padding:10px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;'; const protocolHint = document.createElement('p'); protocolHint.textContent = '默认 mpv://,需先注册协议(注册方法见下方)'; protocolHint.style.cssText = 'font-size:12px; color:#6b7280; margin:6px 0 0;'; protocolDiv.appendChild(protocolLabel); protocolDiv.appendChild(protocolInput); protocolDiv.appendChild(protocolHint); win.appendChild(protocolDiv); // 自定义参数 const argsDiv = document.createElement('div'); argsDiv.style.marginBottom = '20px'; const argsLabel = document.createElement('label'); argsLabel.textContent = '自定义MPV启动参数'; argsLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;'; const argsInput = document.createElement('input'); argsInput.type = 'text'; argsInput.value = tempArgs; argsInput.style.cssText = 'width:100%; padding:10px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;'; const argsHint = document.createElement('p'); argsHint.textContent = '注意:当前VBS协议处理脚本不支持额外参数。如需参数,请修改 mpv_protocol_handler.vbs 或在 mpv.conf 中设置。'; argsHint.style.cssText = 'font-size:12px; color:#ef4444; margin:6px 0 0;'; argsDiv.appendChild(argsLabel); argsDiv.appendChild(argsInput); argsDiv.appendChild(argsHint); win.appendChild(argsDiv); // URL匹配规则管理 const rulesDiv = document.createElement('div'); rulesDiv.style.marginBottom = '20px'; const rulesLabel = document.createElement('label'); rulesLabel.textContent = '🌐 视频页面匹配规则(URL前缀或正则)'; rulesLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;'; const rulesContainer = document.createElement('div'); rulesContainer.id = 'rulesContainer'; rulesContainer.style.cssText = 'background:#f9fafb; border:1px solid #e5e7eb; border-radius:12px; padding:8px 12px; max-height:200px; overflow-y:auto; margin-bottom:12px;'; const addDiv = document.createElement('div'); addDiv.style.cssText = 'display:flex; gap:8px; margin-top:4px;'; const newRuleInput = document.createElement('input'); newRuleInput.type = 'text'; newRuleInput.placeholder = '例如: https://www.bilibili.com/video/ 或 /\\/watch\\?v=/'; newRuleInput.style.cssText = 'flex:1; padding:8px 12px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;'; const addRuleBtn = document.createElement('button'); addRuleBtn.textContent = '添加'; addRuleBtn.style.cssText = 'background:#a855f7; color:white; border:none; border-radius:10px; padding:0 16px; cursor:pointer; font-size:14px;'; addDiv.appendChild(newRuleInput); addDiv.appendChild(addRuleBtn); const rulesHint = document.createElement('p'); rulesHint.textContent = '支持URL前缀(字符串开头匹配)或正则表达式(用 / / 包裹)。当前页面URL匹配任意一条即显示按钮。'; rulesHint.style.cssText = 'font-size:12px; color:#6b7280; margin:8px 0 0;'; rulesDiv.appendChild(rulesLabel); rulesDiv.appendChild(rulesContainer); rulesDiv.appendChild(addDiv); rulesDiv.appendChild(rulesHint); win.appendChild(rulesDiv); // 刷新按钮 const refreshBtn = document.createElement('button'); refreshBtn.textContent = '🔄 立即刷新按钮显示'; refreshBtn.style.cssText = 'background:#10b981; color:white; border:none; border-radius:10px; padding:8px 16px; cursor:pointer; font-size:14px; width:100%; margin-bottom:20px;'; refreshBtn.onclick = () => { addButtons(); alert('已尝试重新添加按钮,请检查左下角。如果仍未出现,请刷新页面重试。'); }; win.appendChild(refreshBtn); // 底部按钮(取消/保存) const btnDiv = document.createElement('div'); btnDiv.style.cssText = 'display:flex; gap:12px; justify-content:flex-end; margin-bottom:20px;'; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = 'padding:8px 18px; border:1px solid #cbd5e1; background:white; border-radius:10px; cursor:pointer; font-size:14px;'; const saveBtn = document.createElement('button'); saveBtn.textContent = '保存设置'; saveBtn.style.cssText = 'padding:8px 18px; background:#2563eb; color:white; border:none; border-radius:10px; cursor:pointer; font-size:14px;'; btnDiv.appendChild(cancelBtn); btnDiv.appendChild(saveBtn); win.appendChild(btnDiv); // 协议注册说明(带下载按钮) const regDiv = document.createElement('div'); regDiv.style.cssText = 'padding:14px; background:#f8fafc; border-radius:12px; margin-bottom:24px; font-size:13px; border-left:4px solid #a855f7;'; const regTitle = document.createElement('b'); regTitle.textContent = '🔧 首次使用注册协议(Windows)'; regTitle.style.color = '#1e293b'; regDiv.appendChild(regTitle); const step1 = document.createElement('div'); step1.textContent = '1. 在 MPV 安装目录(与 mpv.exe 相同)下创建两个文件(可点击按钮下载):'; step1.style.marginTop = '8px'; regDiv.appendChild(step1); // 文件1 const file1Container = document.createElement('div'); file1Container.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-top:8px; flex-wrap:wrap; gap:8px;'; const file1Label = document.createElement('span'); file1Label.textContent = '📄 mpv_protocol_handler.vbs'; file1Label.style.fontWeight = 'bold'; const downloadBtn1 = document.createElement('button'); downloadBtn1.textContent = '📥 下载'; downloadBtn1.style.cssText = 'background:#3b82f6; color:white; border:none; border-radius:6px; padding:4px 12px; cursor:pointer; font-size:12px;'; const vbsContent = `' mpv_protocol_handler.vbs Option Explicit On Error Resume Next Dim rawUrl, targetUrl, mpvPath, wshShell Dim regex, matches, protocol, hostPart, pathPart If WScript.Arguments.Count = 0 Then WScript.Quit rawUrl = WScript.Arguments(0) ' 1. 去掉 mpv:// 前缀 If LCase(Left(rawUrl, 6)) = "mpv://" Then targetUrl = Mid(rawUrl, 7) ElseIf LCase(Left(rawUrl, 4)) = "mpv:" Then targetUrl = Mid(rawUrl, 5) Else targetUrl = rawUrl End If ' 2. 所有反斜杠转为正斜杠 targetUrl = Replace(targetUrl, "\\", "/") ' 3. 使用正则表达式匹配并修复 URL Set regex = New RegExp regex.IgnoreCase = True regex.Global = False regex.Pattern = "^(https?)(?:[:/]+)(.*)$" If regex.Test(targetUrl) Then Set matches = regex.Execute(targetUrl) protocol = matches(0).SubMatches(0) hostPart = matches(0).SubMatches(1) hostPart = LTrim(hostPart, "/") targetUrl = LCase(protocol) & "://" & hostPart Else If InStr(targetUrl, ".") > 0 Then If Left(targetUrl, 8) = "https://" Then targetUrl = Mid(targetUrl, 9) If Left(targetUrl, 7) = "http://" Then targetUrl = Mid(targetUrl, 8) targetUrl = "https://" & LTrim(targetUrl, "/") End If End If If Left(targetUrl, 8) = "https://" Then If Mid(targetUrl, 9, 8) = "https://" Then targetUrl = Mid(targetUrl, 9) ElseIf Left(targetUrl, 7) = "http://" Then If Mid(targetUrl, 8, 7) = "http://" Then targetUrl = Mid(targetUrl, 8) End If mpvPath = Left(WScript.ScriptFullName, InStrRev(WScript.ScriptFullName, "\\")) & "mpv.exe" Set wshShell = CreateObject("WScript.Shell") wshShell.CurrentDirectory = Left(WScript.ScriptFullName, InStrRev(WScript.ScriptFullName, "\\")) wshShell.Run """" & mpvPath & """ """ & targetUrl & """", 1, False Set wshShell = Nothing Set regex = Nothing`; downloadBtn1.onclick = () => downloadFile(vbsContent, 'mpv_protocol_handler.vbs'); file1Container.appendChild(file1Label); file1Container.appendChild(downloadBtn1); regDiv.appendChild(file1Container); const vbsCode = document.createElement('code'); vbsCode.textContent = vbsContent; vbsCode.style.cssText = 'background:#e2e8f0; display:block; padding:8px; margin:8px 0; border-radius:8px; font-size:11px; white-space:pre-wrap; overflow-x:auto;'; regDiv.appendChild(vbsCode); // 文件2 const file2Container = document.createElement('div'); file2Container.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-top:8px; flex-wrap:wrap; gap:8px;'; const file2Label = document.createElement('span'); file2Label.textContent = '📄 注册mpv.bat'; file2Label.style.fontWeight = 'bold'; const downloadBtn2 = document.createElement('button'); downloadBtn2.textContent = '📥 下载'; downloadBtn2.style.cssText = 'background:#3b82f6; color:white; border:none; border-radius:6px; padding:4px 12px; cursor:pointer; font-size:12px;'; const batContent = `@echo off title MPV Protocol Manager (VBS) cls echo ====================================== echo MPV Protocol Manager (VBS) echo ====================================== echo 1. Register MPV protocol echo 2. Unregister MPV protocol echo ====================================== set /p choice=Enter your choice (1/2): if "%choice%"=="1" goto register if "%choice%"=="2" goto unregister echo Invalid choice! Enter 1 or 2. pause exit :register echo. echo Checking required files... if not exist "%~dp0mpv.exe" ( echo [ERROR] mpv.exe not found. echo Place this batch file in the same folder as mpv.exe. pause exit /b ) if not exist "%~dp0mpv_protocol_handler.vbs" ( echo [ERROR] mpv_protocol_handler.vbs not found. pause exit /b ) echo Writing registry entries... reg add "HKEY_CLASSES_ROOT\\mpv" /ve /d "URL:mpv Protocol" /f >nul reg add "HKEY_CLASSES_ROOT\\mpv" /v "URL Protocol" /d "" /f >nul reg add "HKEY_CLASSES_ROOT\\mpv\\shell\\open\\command" /ve /d "wscript.exe \"%~dp0mpv_protocol_handler.vbs\" \"%%1\"" /f >nul echo. echo ====================================== echo Registration successful! echo You can now use mpv://URL in your browser. echo Example: mpv://https://www.bilibili.com/video/BV1t5v1e9EG4 echo ====================================== goto end :unregister echo. echo Removing MPV protocol registry entries... reg delete "HKEY_CLASSES_ROOT\\mpv" /f >nul 2>&1 if errorlevel 1 ( echo Registry entry not found or already removed. ) else ( echo Unregistration complete. ) goto end :end echo. echo Press any key to exit... pause >nul`; downloadBtn2.onclick = () => downloadFile(batContent, '注册mpv.bat'); file2Container.appendChild(file2Label); file2Container.appendChild(downloadBtn2); regDiv.appendChild(file2Container); const batCode = document.createElement('code'); batCode.textContent = batContent; batCode.style.cssText = 'background:#e2e8f0; display:block; padding:8px; margin:8px 0; border-radius:8px; font-size:11px; white-space:pre-wrap; overflow-x:auto;'; regDiv.appendChild(batCode); const step2 = document.createElement('div'); step2.textContent = '2. 以管理员身份运行 “注册mpv.bat”,输入 1 并回车,完成注册。'; step2.style.marginTop = '8px'; regDiv.appendChild(step2); const step3 = document.createElement('div'); step3.textContent = '3. 注册成功后,点击页面左下角的紫色播放按钮即可调用 MPV 播放当前视频。'; regDiv.appendChild(step3); const note = document.createElement('div'); note.textContent = '⚠️ 注意:自定义参数功能在此VBS方案中无效,如需添加启动参数请修改 mpv_protocol_handler.vbs 或在 mpv.conf 中配置。'; note.style.marginTop = '8px'; note.style.color = '#dc2626'; regDiv.appendChild(note); win.appendChild(regDiv); mask.appendChild(win); document.body.appendChild(mask); // 渲染规则列表 - 完全避免 innerHTML function renderRules() { while (rulesContainer.firstChild) { rulesContainer.removeChild(rulesContainer.firstChild); } if (tempRules.length === 0) { const emptyDiv = document.createElement('div'); emptyDiv.textContent = '暂无匹配规则,请添加(至少一条才能显示按钮)'; emptyDiv.style.cssText = 'color:#6b7280; text-align:center; padding:12px;'; rulesContainer.appendChild(emptyDiv); return; } tempRules.forEach((rule, idx) => { const itemDiv = document.createElement('div'); itemDiv.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-bottom:1px solid #e5e7eb;'; const ruleSpan = document.createElement('span'); ruleSpan.textContent = rule; ruleSpan.style.cssText = 'font-size:13px; color:#1f2937; word-break:break-all; flex:1; margin-right:8px;'; const delBtn = document.createElement('button'); delBtn.textContent = '删除'; delBtn.style.cssText = 'background:#ef4444; color:white; border:none; border-radius:6px; padding:2px 10px; cursor:pointer; font-size:12px;'; delBtn.onclick = () => { tempRules.splice(idx, 1); renderRules(); }; itemDiv.appendChild(ruleSpan); itemDiv.appendChild(delBtn); rulesContainer.appendChild(itemDiv); }); } renderRules(); addRuleBtn.onclick = () => { let newRule = newRuleInput.value.trim(); if (!newRule) { alert('请输入规则'); return; } if (tempRules.includes(newRule)) { alert('规则已存在'); return; } tempRules.push(newRule); renderRules(); newRuleInput.value = ''; }; const closeMask = () => { mask.remove(); settingWindowOpen = false; }; cancelBtn.onclick = closeMask; saveBtn.onclick = () => { const newProtocol = protocolInput.value.trim(); const newArgs = argsInput.value.trim(); if (tempRules.length === 0) { alert('至少需要一条匹配规则,否则按钮永远不会显示!'); return; } GM_setValue('mpvProtocol', newProtocol); GM_setValue('customArgs', newArgs); GM_setValue('urlRules', tempRules); config.mpvProtocol = newProtocol; config.customArgs = newArgs; config.urlRules = [...tempRules]; addButtons(); alert('✅ 设置保存成功!'); closeMask(); }; mask.onclick = (e) => { if (e.target === mask) closeMask(); }; } // 注册油猴菜单命令 GM_registerMenuCommand('⚙️ MPV播放设置', () => { openSetting(); }); function init() { const oldSiteList = GM_getValue('siteList', null); if (oldSiteList && Array.isArray(oldSiteList) && (!GM_getValue('urlRules', null))) { log('检测到旧版域名列表,正在迁移为URL前缀规则...'); const migratedRules = []; for (const domain of oldSiteList) { if (domain === 'youtube.com') migratedRules.push('https://www.youtube.com/watch'); else if (domain === 'bilibili.com') migratedRules.push('https://www.bilibili.com/video/'); } if (migratedRules.length === 0) { migratedRules.push(...defaultRules); } GM_setValue('urlRules', migratedRules); config.urlRules = migratedRules; log('迁移完成,新规则:', migratedRules); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { observePageChanges(); }); } else { observePageChanges(); } } init(); })();