Greasy Fork

Greasy Fork is available in English.

B站BV号极速转换工具

用于将B站BV号转换为超链接

// ==UserScript==
// @name         B站BV号极速转换工具
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  用于将B站BV号转换为超链接
// @author       WEN枫树
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      bilibili.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 匹配BV号的正则表达式
    const bvRegex = /BV1[a-zA-Z0-9]{2}4[a-zA-Z0-9]1[a-zA-Z0-9]7[a-zA-Z0-9]{2}/g;

    // 缓存配置(7天有效期)
    const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7天

    // 请求并发控制
    const MAX_CONCURRENT_REQUESTS = 5;
    let activeRequests = 0;
    let requestQueue = [];

    // 添加自定义样式
    function addCustomStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .bv-container {
                display: inline;
                white-space: normal;
            }
            .bv-link {
                color: #00a1d6 !important;
                text-decoration: none !important;
                font-weight: bold;
                cursor: pointer;
                transition: all 0.2s ease;
                display: inline;
            }
            .bv-link:hover {
                text-decoration: underline !important;
                color: #00c8ff !important;
            }
            .bv-title {
                color: #a9a9b3;
                font-style: italic;
                margin-left: 4px;
                display: inline;
                white-space: normal;
            }
            .loading {
                color: #777;
            }
            .cached {
                color: #4ade80;
            }
            .invalid-indicator {
                color: #ff6b6b;
                font-style: italic;
                display: inline;
                margin-left: 5px;
            }
            .invalid-indicator::before {
                content: "(!";
            }
            .invalid-indicator::after {
                content: ")";
            }
        `;
        document.head.appendChild(style);
    }

    // 获取缓存标题
    function getCachedTitle(bv) {
        const cache = GM_getValue(bv);
        if (cache && cache.expire > Date.now()) {
            return cache.title;
        }
        return null;
    }

    // 保存标题到缓存
    function saveToCache(bv, title) {
        GM_setValue(bv, {
            title: title,
            expire: Date.now() + CACHE_TTL
        });
    }

    // 主处理函数
    function processTextNode(node) {
        if (!node.textContent.match(bvRegex)) return;
        if (isInBVContainer(node) || isInPre(node)) return;

        const parent = node.parentNode;
        if (parent.tagName === 'A' || parent.classList.contains('processed-bv')) return;

        // 创建文档片段来保存替换内容
        const fragment = document.createDocumentFragment();
        const text = node.textContent;
        let lastIndex = 0;
        let match;

        while ((match = bvRegex.exec(text)) !== null) {
            // 添加匹配前的文本
            if (match.index > lastIndex) {
                fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
            }

            // 处理BV号
            const bv = match[0];
            const container = createBVContainer(bv);
            fragment.appendChild(container);

            lastIndex = bvRegex.lastIndex;
        }

        // 添加剩余文本
        if (lastIndex < text.length) {
            fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
        }

        // 替换原始节点
        parent.replaceChild(fragment, node);
    }

    // 创建BV容器元素
    function createBVContainer(bv) {
        const container = document.createElement('span');
        container.className = 'bv-container processed-bv';
        container.dataset.bv = bv;

        const link = document.createElement('a');
        link.href = `https://www.bilibili.com/video/${bv}`;
        link.target = '_blank';
        link.className = 'bv-link';
        link.textContent = bv;
        container.appendChild(link);

        // 检查缓存
        const cachedTitle = getCachedTitle(bv);
        if (cachedTitle) {
            const titleSpan = document.createElement('span');
            titleSpan.className = 'bv-title cached';
            titleSpan.textContent = `(${cachedTitle})`;
            container.appendChild(titleSpan);
        } else {
            const titleSpan = document.createElement('span');
            titleSpan.className = 'bv-title loading';
            titleSpan.textContent = '(加载中...)';
            container.appendChild(titleSpan);

            // 添加到处理队列
            requestQueue.push({ bv, container });
        }

        return container;
    }

    // 处理请求队列
    function processRequestQueue() {
        // 先处理可见区域的请求
        const visibleRequests = [];
        const invisibleRequests = [];

        requestQueue.forEach(item => {
            if (isElementInViewport(item.container)) {
                visibleRequests.push(item);
            } else {
                invisibleRequests.push(item);
            }
        });

        // 处理可见请求
        processBatch(visibleRequests);

        // 处理不可见请求(延迟处理)
        if (invisibleRequests.length > 0) {
            setTimeout(() => {
                processBatch(invisibleRequests);
            }, 1500);
        }

        // 清空队列
        requestQueue = [];
    }

    // 批量处理请求
    function processBatch(requests) {
        const batchSize = 5;
        const batches = [];

        for (let i = 0; i < requests.length; i += batchSize) {
            batches.push(requests.slice(i, i + batchSize));
        }

        batches.forEach((batch, index) => {
            // 分批处理,避免一次性发起过多请求
            setTimeout(() => {
                batch.forEach(item => {
                    fetchVideoTitle(item.bv, item.container);
                });
            }, index * 500); // 每批间隔500ms
        });
    }

    // 获取视频标题
    function fetchVideoTitle(bv, container) {
        // 检查是否已有标题
        if (container.querySelector('.cached') || container.querySelector('.invalid-indicator')) {
            return;
        }

        // 并发控制
        if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
            setTimeout(() => fetchVideoTitle(bv, container), 200);
            return;
        }

        activeRequests++;

        const apiUrl = `https://api.bilibili.com/x/web-interface/view?bvid=${bv}`;
        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            timeout: 3000,
            onload: function(res) {
                activeRequests--;
                if (res.status === 200) {
                    try {
                        const data = JSON.parse(res.responseText);
                        if (data.code === 0) {
                            const title = data.data.title;
                            saveToCache(bv, title); // 保存到缓存
                            updateTitle(bv, container, title);
                        } else {
                            markAsInvalid(container);
                        }
                    } catch (e) {
                        markAsInvalid(container);
                    }
                } else {
                    markAsInvalid(container);
                }
            },
            onerror: () => {
                activeRequests--;
                markAsInvalid(container);
            },
            ontimeout: () => {
                activeRequests--;
                markAsInvalid(container);
            }
        });
    }

    // 检查元素是否在可视区域
    function isElementInViewport(el) {
        const rect = el.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }

    // 更新标题显示
    function updateTitle(bv, container, title) {
        const titleSpan = container.querySelector('.bv-title');
        if (titleSpan) {
            titleSpan.textContent = `(${title})`;
            titleSpan.classList.remove('loading');
            titleSpan.classList.add('cached');
        }
    }

    // 标记为无效BV号
    function markAsInvalid(container) {
        const titleSpan = container.querySelector('.bv-title');
        if (titleSpan) {
            const invalidSpan = document.createElement('span');
            invalidSpan.className = 'invalid-indicator';
            invalidSpan.textContent = '该BV号已失效';
            container.replaceChild(invalidSpan, titleSpan);
        }
    }

    // 检查是否在已处理的容器内
    function isInBVContainer(node) {
        let parent = node.parentNode;
        while (parent) {
            if (parent.classList && parent.classList.contains('bv-container')) {
                return true;
            }
            parent = parent.parentNode;
        }
        return false;
    }

    // 检查是否在pre标签内
    function isInPre(node) {
        let parent = node.parentNode;
        while (parent) {
            if (parent.tagName === 'PRE') {
                return true;
            }
            parent = parent.parentNode;
        }
        return false;
    }

    // 扫描文本节点
    function scanTextNodes(root) {
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
        const nodes = [];
        let node;

        while ((node = walker.nextNode())) {
            if (node.textContent.match(bvRegex)) {
                nodes.push(node);
            }
        }

        return nodes;
    }

    // 初始化函数
    function init() {
        addCustomStyles();

        // 初始扫描
        const textNodes = scanTextNodes(document.body);
        textNodes.forEach(processTextNode);

        // 处理请求队列
        processRequestQueue();

        // 监听DOM变化
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            const textNodes = scanTextNodes(node);
                            textNodes.forEach(processTextNode);

                            // 处理新添加的请求
                            if (requestQueue.length > 0) {
                                processRequestQueue();
                            }
                        }
                    });
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 滚动时加载新出现的BV号标题
        window.addEventListener('scroll', () => {
            document.querySelectorAll('.bv-container .loading').forEach(container => {
                if (isElementInViewport(container)) {
                    const bvContainer = container.closest('.bv-container');
                    const bv = bvContainer.dataset.bv;
                    fetchVideoTitle(bv, bvContainer);
                }
            });
        });
    }

    // 延迟执行以避免阻塞页面加载
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 500);
    }
})();