抢救本家直链

把B站视频简介sm号超链接使用的失效短链域名 acg.tv 替换为 nicovideo

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         抢救本家直链
// @namespace    https://memo329.jimdofree.com
// @version      0.2.1
// @description  把B站视频简介sm号超链接使用的失效短链域名 acg.tv 替换为 nicovideo
// @author       永咲みつき
// @author       HIMlaoS_Misa
// @match        *://*.bilibili.com/video/*
// @match        *://*.bilibili.com/list/*
// @grant        none
// @run-at       document-start
// @license      GPLv3
// @supportURL   mailto:[email protected]
// @supportURL   mailto:[email protected]
// ==/UserScript==

(function() {
    'use strict';

    // 日志系统
    const jsName = '抢救本家直链';

    function asMethod(type) {
        const styleMap = {
            debug: '#28D',
            info: '#2A8',
            warn: '#DA0',
            error: '#E46'
        };

        if (!(type in styleMap)) {
            type = 'info';
        }

        const style = `
            background-color: ${styleMap[type]};
            color: #FFF;
            font-weight: bold;
            padding: 1px 6px;
            border-radius: 3px;
            margin-right: 3px;
        `;

        return function(...args) {
            const label = `【${jsName}】 %c${type.toUpperCase()}`;
            console[type](label, style, ...args);
        };
    };

    const logger = {
        debug: asMethod('debug'),
        info: asMethod('info'),
        warn: asMethod('warn'),
        error: asMethod('error')
    };

    // 编辑超链接的函数
    const replaceLinksInElement = (element) => {
        let newHTML = element.innerHTML;
        let count = 0;

        const oldUrlReg = /(<a\s+[^>]*?href\s*=\s*["'])((?:https?:)?\/\/acg\.tv(?:\/)?)([^"'\s>]*)(["'][^>]*>)/g;
        const otherIdReg = /\b(so|nm)([1-9]\d*)\b(?![^<]*<\/a>)/g;

        newHTML = newHTML.replace(oldUrlReg, (match, prefix, domain, id, suffix) => {
            count++;
            logger.info(`已替换 ${id} 的链接`);
            return `${prefix}//www.nicovideo.jp/watch/${id}${suffix}`;
        });

        newHTML = newHTML.replace(otherIdReg, (match) => {
            count++;
            logger.info(`已插入 ${match} 的链接`);
            return `<a href="https://www.nicovideo.jp/watch/${match}" target="_blank">${match}</a>`;
        });

        if (count > 0) {
            element.innerHTML = newHTML;
        }

        return count;
    };

    // 定义防抖
    const _ = {
        debounce: (func, wait) => {
            let timeout;
            return (...args) => {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        }
    };

    // 主替换逻辑(500ms防抖)
    const doDescLinkReplace = _.debounce(() => {
        try {
            const containers = document.querySelectorAll('#app .desc-info-text');
            if (containers.length === 0) {
                logger.error('未找到视频简介');
                return;
            }

            let Count = 0;
            containers.forEach(container => {
                Count += replaceLinksInElement(container);
            });

            if (Count > 0) {
                logger.info(`共处理 ${Count} 个链接`);
            } else {
                logger.info('目前无链接需处理');
            }
        } catch (error) {
            logger.error(`执行处理时出错 - ${error.message}`);
        }
    }, 500);

    // DOM变化观察者
    const initObserver = () => {
        const targetNode = document.querySelector('#app');
        if (!targetNode) {
            logger.error('未找到播放器');
            return null;
        }

        const observer = new MutationObserver(mutations => {
            const relevantChange = mutations.some(mutation => {
                return mutation.type === 'childList' &&
                    (mutation.target.matches?.('.desc-info-text, .desc-info-text *') ||
                     [...mutation.addedNodes].some(n => n.matches?.('.desc-info-text, .desc-info-text *')))
            });

            if (relevantChange) {
                doDescLinkReplace();
            }
        });

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

        return observer;
    };

    // 脚本入口
    logger.info('脚本已激活,正在初始化...');
    let appObserver = null;

    // 脚本初始化
    const init = () => {
        if (appObserver) {
            appObserver.disconnect();
        }
        doDescLinkReplace();
        appObserver = initObserver();
    };

    // 页面加载检测(重试30次 超时15s)
    const checkPageLoad = () => {
        let attempts = 0;
        const maxAttempts = 30;

        const checkInterval = setInterval(() => {
            if (document.querySelector('#app')) {
                clearInterval(checkInterval);
                logger.info('页面已加载');
                init();
            } else if (++attempts >= maxAttempts) {
                clearInterval(checkInterval);
                logger.warn('页面加载超时');
                init();
            }
        }, 500);
    };

    // 启动页面加载检测
    checkPageLoad();

})();