Bilibili 干净链接

去除bilibili链接中不需要的参数,如spm_id_from/from_sourse/from/等,还地址栏以清白干净

As of 2022-07-23. See the latest version.

// ==UserScript==
// @name         Bilibili 干净链接
// @namespace    Motoori Kashin
// @version      2.0.5
// @description  去除bilibili链接中不需要的参数,如spm_id_from/from_sourse/from/等,还地址栏以清白干净
// @author       Motoori Kashin
// @match        *://*.bilibili.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {

    class UrlFormat {
        /** 去除参数和锚的基链接 */
        base = undefined;
        /** 查询参数转化的对象 */
        searchParams = new Proxy({}, {
            get: (t, p) => t[p] ? decodeURIComponent(t[p]) : t[p], set: (t, p, v) => {
                t[p] = v ? encodeURIComponent(v) : v;
                return true
            }
        });
        /** 锚 */
        hash = undefined;
        /** 锚中的参数 */
        hashParams = new Proxy({}, {
            get: (t, p) => t[p] ? decodeURIComponent(t[p]) : t[p], set: (t, p, v) => {
                t[p] = v ? encodeURIComponent(v) : v;
                return true
            }
        });
        /** 所有参数(包括锚中的参数)。 */
        params() {
            return new Proxy({ ...this.searchParams, ...this.hashParams }, {
                set: (t, p, v) => {
                    t[p] = v;
                    (Reflect.has(this.hashParams, p) ? this.hashParams : this.searchParams)[p] = v ? encodeURIComponent(v) : v;
                    return true;
                },
                deleteProperty: (t, p) => {
                    delete t[p];
                    delete (Reflect.has(this.hashParams, p) ? this.hashParams : this.searchParams)[p];
                    return true;
                }
            });
        }
        /**
         * 格式化URL
         * @param url URL字符串
         */
        constructor(url) {
            try {
                // 原生URL处理函数要求太严格,无法处理自定义链接
                url = new URL(url).href;
            } catch (e) { }
            const one = url.split("#"); // 分离锚
            const two = one[0].split("?"); // 分离参数
            this.base = two[0]; // 分离基链接
            one.shift();
            two.shift();
            // 参数转对象
            if (two[0]) {
                two[0].split("&").forEach(d => {
                    const arr = d.split("=");
                    this.searchParams[arr.shift()] = arr.join("=");
                });
            }
            // 锚处理
            if (one[0] || one[0] === "") {
                const three = one[0].join("#").split("?");
                this.hash = three[0];
                three.shift();
                // 锚参数转对象
                if (three[0]) {
                    three[0].split("&").forEach(d => {
                        const arr = d.split("=");
                        this.searchParams[arr.shift()] = arr.join("=");
                    });
                }
            }
        }
        /** 拼合链接 */
        toJSON() {
            const base = []; // 基栈
            this.base && base.push(this.base); // 基链接
            // 参数
            const searchParams = Object.entries(this.searchParams).reduce((s, d) => {
                d[1] !== null && d[1] !== undefined && s.push(d.join("="));
                return s;
            }, []).join("&");
            searchParams && base.push(searchParams);
            const searchParam = base.join("?"); // 含参基链
            const hash = []; // 锚栈
            this.hash && hash.push(this.hash);
            const hashParams = Object.entries(this.hashParams).reduce((s, d) => {
                d[1] !== null && d[1] !== undefined && s.push(d.join("="));
                return s;
            }, []).join("&");
            hashParams && hash.push(hashParams);
            const hashParam = hash.join("?"); // 含参锚
            const result = []; // 结果栈
            searchParam && result.push(searchParam);
            hashParam && result.push(hashParam);
            if (this.hash === "") result.push(""); // 空锚
            return result.join("#");
        }
    }
    /** 垃圾参数 */
    const paramsSet = new Set([
        'spm_id_from',
        'from_source',
        'msource',
        'bsource',
        'seid',
        'source',
        'session_id',
        'visit_id',
        'sourceFrom',
        'from_spmid',
        'share_source',
        'share_medium',
        'share_plat',
        'share_session_id',
        'share_tag',
        'unique_k',
        "csource",
        "vd_source"
    ]);
    /** 节点监听暂存 */
    const nodelist = [];
    /**
     * 清理url
     * @param str 原url
     * @returns 新url
     */
    function clean(str) {
        const url = new UrlFormat(str);
        const params = url.params();
        paramsSet.forEach(d => {
            Reflect.deleteProperty(params, d);
        });
        // 非参数型bv号转化为av号;
        return url.toJSON()
    }
    /** 地址备份 */
    let locationBackup;
    /** 处理地址栏 */
    function cleanLocation() {
        const { href } = location;
        if (href === locationBackup) return;
        replaceUrl(locationBackup = clean(href));
    }
    /** 处理href属性 */
    function anchor(list) {
        list.forEach(d => {
            if (!d.href) return;
            d.href.includes("bilibili.tv") && (d.href = d.href.replace("bilibili.tv", "bilibili.com")); // tv域名失效
            d.href = clean(d.href);
        });
    }
    /** 检查a标签 */
    function click(e) { // 代码copy自B站spm.js
        var f = e.target;
        for (; f && "A" !== f.tagName;) {
            f = f.parentNode
        }
        if ("A" !== (null == f ? void 0 : f.tagName)) {
            return
        }
        anchor([f]);
    }
    /**
     * 修改当前URL而不出发重定向
     * **无法跨域操作!**
     * @param url 新URL
     */
    function replaceUrl(url) {
        window.history.replaceState(window.history.state, "", url);
    }
    cleanLocation(); // 及时处理地址栏
    // 处理注入的节点
    let timer = 0;
    observerAddedNodes((node) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            cleanLocation();
            anchor(document.querySelectorAll("a"));
        });
    });
    // 处理点击事件
    window.addEventListener("click", click, !1);
    // 处理右键菜单
    window.addEventListener("contextmenu", click, !1);
    // 页面载入完成
    document.addEventListener("load", ()=>anchor(document.querySelectorAll("a")), !1);
    /**
     * 注册节点添加监听
     * **监听节点变动开销极大,如非必要请改用其他方法并且用后立即销毁!**
     * @param callback 添加节点后执行的回调函数
     * @returns 注册编号,用于使用`removeObserver`销毁监听
     */
    function observerAddedNodes(callback) {
        try {
            if (typeof callback === "function") nodelist.push(callback);
            return nodelist.length - 1;
        } catch (e) { console.error(e) }
    }
    const observe = new MutationObserver(d => d.forEach(d => {
        d.addedNodes[0] && nodelist.forEach(async f => {
            try {
                f(d.addedNodes[0])
            } catch (e) { console.error(d) }
        })
    }));
    observe.observe(document, { childList: true, subtree: true });
    window.open = ((__open__) => {
        return (url, name, params) => {
            __open__(clean(url), name, params)
        }
    })(window.open)
    window.navigation?.addEventListener('navigate', e => {
     const newURL = clean(e.destination.url)
     if(e.destination.url!=newURL) {
         e.preventDefault(); // 返回前还是先阻止原事件吧
         if(newURL == window.location.href) return // 如果清理后和原来一样就直接返回
         // 否则就处理清理后的链接
         window.history.replaceState(window.history.state, "", newURL)
         return
     }
    });
})();