Greasy Fork

来自缓存

Greasy Fork is available in English.

VRChat链接解析

【v8.6更新】仅在视频页面显示按钮,非视频页自动隐藏,减少干扰。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
    }
})();