抢救本家直链

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();

})();