Greasy Fork is available in English.
【v8.6更新】仅在视频页面显示按钮,非视频页自动隐藏,减少干扰。
// ==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
// ==/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);
}
})();