// ==UserScript== // @name VRChat链接解析 // @namespace http://tampermonkey.net/ // @version 8.6 // @description 【v8.6更新】仅在视频页面显示按钮,非视频页自动隐藏,减少干扰。 // @author AI Assistant // @match *://*/* // @grant GM_setClipboard // @grant GM_notification // @grant GM_xmlhttpRequest // @connect bilibili.com // @connect api.bilibili.com // @connect api.cobalt.tools // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/574772/VRChat%E9%93%BE%E6%8E%A5%E8%A7%A3%E6%9E%90.user.js // @updateURL https://update.greasyfork.icu/scripts/574772/VRChat%E9%93%BE%E6%8E%A5%E8%A7%A3%E6%9E%90.meta.js // ==/UserScript== // --- 下面是我的备注 --- // 修改者:小星星 // 修改时间:2026年4月18日 // 本脚本由千问3.5提供编译支持,我进行测试和bug修改,可公开使用,感谢您的使用 // 该脚本目前测试可解析blbl,X以及其他网站视频,但不可解析个人视频网站或已进行加密后的视频网站资源 // ==/UserScript== (function () { 'use strict'; // --- 1. 创建悬浮按钮 --- const btn = document.createElement('button'); btn.innerText = '🚀 VRC解析'; btn.style.position = 'fixed'; btn.style.zIndex = '999999'; btn.style.padding = '10px 15px'; btn.style.background = '#FB7299'; btn.style.color = 'white'; btn.style.border = '2px solid white'; btn.style.borderRadius = '8px'; btn.style.cursor = 'pointer'; btn.style.boxShadow = '0 4px 10px rgba(0,0,0,0.5)'; btn.style.fontSize = '14px'; btn.style.fontWeight = 'bold'; btn.style.transition = 'opacity 0.2s'; btn.style.display = 'none'; // 初始隐藏 document.body.appendChild(btn); // --- 2. 创建自定义提示框 (Toast) --- const toast = document.createElement('div'); toast.innerText = ''; toast.style.position = 'fixed'; toast.style.top = '10%'; toast.style.left = '40%'; toast.style.transform = 'translate(-50%, -50%)'; toast.style.zIndex = '1000000'; toast.style.padding = '15px 25px'; toast.style.background = 'rgba(251, 114, 153, 0.95)'; toast.style.color = 'white'; toast.style.borderRadius = '12px'; toast.style.boxShadow = '0 4px 15px rgba(251, 114, 153, 0.4)'; toast.style.fontSize = '16px'; toast.style.fontWeight = 'bold'; toast.style.opacity = '0'; toast.style.transition = 'all 0.3s ease-out'; toast.style.pointerEvents = 'none'; document.body.appendChild(toast); // --- 显示提示框的函数 --- function showToast(message) { toast.innerText = message; toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, 2000); } function isVideoPlaying() { const videos = document.querySelectorAll('video'); for (let video of videos) { // 新代码:只要视频加载了元数据(HAVE_METADATA)或更多,就认为它“存在” if (video.readyState >= 1) { return true; } } return false; } // --- 3. 智能显示判断逻辑 (修改版) --- function shouldShowButton() { const url = window.location.href; const hostname = window.location.hostname; // 1. 首先检测是否有视频在播放 if (!isVideoPlaying()) { return false; // 没有视频播放,强制隐藏 } // 2. 如果有视频播放,再检查是否在支持的网站域名下 // Bilibili if (hostname.includes('bilibili.com')) { if (url.match(/(BV|av)\d+/i) || url.includes('/video/')) return true; return false; } // 抖音 if (hostname.includes('douyin.com')) return true; // X / Twitter if (hostname.includes('x.com') || hostname.includes('twitter.com')) { if (url.match(/\/(status|tweet)s?\/(\d+)/i)) return true; return false; } // YouTube if (hostname.includes('youtube.com') || hostname.includes('youtu.be')) { if (url.includes('/watch') || url.includes('/shorts/')) return true; return false; } // 其他所有网站 (排除搜索引擎) const excludeList = ['google', 'baidu', 'bing', 'github', 'stackoverflow']; for (let site of excludeList) { if (hostname.includes(site)) return false; } if (hostname === 'localhost' || hostname === '127.0.0.1') return false; return true; } // --- 4. 按钮显示控制 --- function updateButtonVisibility() { const shouldBeVisible = shouldShowButton(); const isCurrentlyVisible = btn.style.display !== 'none'; if (shouldBeVisible && !isCurrentlyVisible) { const savedPos = localStorage.getItem('vrchat_btn_position'); if (savedPos) { try { const pos = JSON.parse(savedPos); btn.style.left = pos.left + 'px'; btn.style.top = pos.top + 'px'; } catch (e) { btn.style.left = '20px'; btn.style.top = '100px'; } } else { btn.style.left = '20px'; btn.style.top = '100px'; } btn.style.display = 'block'; } else if (!shouldBeVisible && isCurrentlyVisible) { btn.style.display = 'none'; } } // --- 5. 拖拽逻辑 --- let isDragging = false; let startX, startY, initialLeft, initialTop; btn.addEventListener('mousedown', (e) => { if (btn.style.display === 'none') return; isDragging = false; startX = e.clientX; startY = e.clientY; initialLeft = btn.offsetLeft; initialTop = btn.offsetTop; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (Math.abs(e.clientX - startX) > 3 || Math.abs(e.clientY - startY) > 3) { isDragging = true; btn.style.opacity = '0.7'; btn.style.cursor = 'grabbing'; } if (isDragging) { const dx = e.clientX - startX; const dy = e.clientY - startY; let newLeft = initialLeft + dx; let newTop = initialTop + dy; const maxX = window.innerWidth - btn.offsetWidth; const maxY = window.innerHeight - btn.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; } } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); btn.style.opacity = '1'; btn.style.cursor = 'pointer'; if (isDragging) { const finalPos = { left: btn.offsetLeft, top: btn.offsetTop }; localStorage.setItem('vrchat_btn_position', JSON.stringify(finalPos)); } } // --- 6. 核心解析逻辑 --- btn.addEventListener('click', () => { if (isDragging) { isDragging = false; return; } const currentUrl = window.location.href; btn.disabled = true; btn.innerText = '⏳ 解析中...'; // Bilibili if (currentUrl.includes('bilibili.com')) { handleBilibili(currentUrl); } // 抖音 else if (currentUrl.includes('douyin.com')) { handleDouyin(currentUrl); } // X / Twitter else if (currentUrl.includes('x.com') || currentUrl.includes('twitter.com')) { handleTwitter(currentUrl); } // YouTube else if (currentUrl.includes('youtube.com') || currentUrl.includes('youtu.be')) { GM_setClipboard(currentUrl); showToast("小星星提醒您,链接已复制,可以到vrchat粘贴使用了哦=V="); resetBtn(); } // 其他所有网站 else { GM_setClipboard(currentUrl); showToast("小星星提醒您,链接已复制,可以到vrchat粘贴使用了哦=V="); resetBtn(); } }); // --- 7. 页面监听 (增加定时器检测视频状态) --- window.addEventListener('load', updateButtonVisibility); // 除了 URL 变化,我们还需要定时检查视频是否开始播放或暂停 // 每 1 秒检查一次 setInterval(updateButtonVisibility, 1000); let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; updateButtonVisibility(); } }).observe(document, { subtree: true, childList: true }); // --- 8. 解析函数 --- function handleBilibili(url) { let bvId = url.match(/BV\w+/i)?.[0]; let cid = new URLSearchParams(window.location.search).get('cid'); if (!bvId) { alert("未找到BV号"); return resetBtn(); } if (cid) { fetchBiliLink(bvId, cid); } else { GM_xmlhttpRequest({ method: "GET", url: `https://api.bilibili.com/x/web-interface/view?bvid=${bvId}`, onload: function (res) { const json = JSON.parse(res.responseText); if (json.code === 0) { fetchBiliLink(bvId, json.data.cid); } else { alert("获取CID失败"); resetBtn(); } } }); } } function fetchBiliLink(bv, cid) { GM_xmlhttpRequest({ method: "GET", url: `https://api.bilibili.com/x/player/playurl?bvid=${bv}&cid=${cid}&qn=64&type=mp4&platform=html5`, onload: function (res) { const json = JSON.parse(res.responseText); if (json.code === 0) { const videoUrl = json.data.durl[0].url; GM_setClipboard(videoUrl); showToast("小星星提醒您,链接已复制,可以到vrchat粘贴使用了哦=V="); } else { alert("解析失败: " + json.message); } resetBtn(); } }); } function handleDouyin(url) { const apiUrl = `https://api.vvhan.com/api/douyin?url=${encodeURIComponent(url)}`; GM_xmlhttpRequest({ method: "GET", url: apiUrl, timeout: 10000, onload: function (res) { try { const data = JSON.parse(res.responseText); if (data.success && data.videoUrl) { GM_setClipboard(data.videoUrl); showToast("小星星提醒您,链接已复制,可以到vrchat粘贴使用了哦=V="); } else { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); } } catch (e) { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); } resetBtn(); }, onerror: function () { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); resetBtn(); } }); } function handleTwitter(url) { const apiUrl = `https://api.cobalt.tools/api/json`; const requestData = JSON.stringify({ url: url, vCodec: "h264", vQuality: "720" }); GM_xmlhttpRequest({ method: "POST", url: apiUrl, data: requestData, headers: { "Accept": "application/json", "Content-Type": "application/json" }, timeout: 10000, onload: function (res) { try { const json = JSON.parse(res.responseText); if (json && json.url) { GM_setClipboard(json.url); showToast("小星星提醒您,链接已复制,可以到vrchat粘贴使用了哦=V="); } else { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); } } catch (e) { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); } resetBtn(); }, onerror: function () { GM_setClipboard(url); showToast("解析失败,已复制网页链接,请选 Web 模式"); resetBtn(); } }); } function resetBtn() { setTimeout(() => { btn.disabled = false; btn.innerText = '🚀 VRC解析'; }, 1000); } })();