Make AV number, not BV code

Make AV number, not BV code. F**k you BiliBili

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name              Make AV number, not BV code
// @name:en           Make AV number, not BV code
// @name:zh-CN        要AV号,不要BV码
// @namespace         https://pilipili.com/bv10492
// @version           0.1.5
// @description       Make AV number, not BV code. F**k you BiliBili
// @description:en    Make AV number, not BV code. F**k you BiliBili
// @description:zh-CN 要AV号,不要BV码,屑站飞了
// @author            jk1551
// @require           https://unpkg.com/[email protected]/dist/ajaxhook.min.js
// @nocompat          Chrome
// @nocompat          Firefox
// @nocompat          Opera
// @nocompat          Edge
// @match             https://www.bilibili.com/video/*
// @match             http://www.bilibili.com/video/*
// @match             https://www.bilibili.com/bangumi/play/*
// @match             http://www.bilibili.com/bangumi/play/*
// @match             https://acg.tv/*
// @match             http://acg.tv/*
// @match             https://b23.tv/*
// @match             http://b23.tv/*
// @grant             none
// ==/UserScript==

/* jshint esversion: 6 */

const CONFIG = {
    autoJump: false
};

// GM代码构思来自 https://greasyfork.org/zh-CN/scripts/398526-give-me-av-not-bv

(() => {
    if (typeof BigInt !== "function") {
        console.warn("Your browser does not support BigInt. AV/BV conversion is disabled.");
        return;
    }

    const bv2av = (() => {
        // https://www.zhihu.com/question/381784377/answer/1099438784
        const table = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF";
        const tr = Object.create(null);

        for (let i = 0; i < 58; i += 1) {
            tr[table[i]] = BigInt(i);
        }

        const s = [11, 10, 3, 8, 4, 6/* ←今ここ */, 2, 9, 5, 7/* 最大値 */];
        // 不用BigInt的话后面做位运算就爆炸了
        const xor = BigInt(177451812);
        const add = BigInt(8728348608);
        const pow58 = [];

        // 58进制下2^32的最大位数(=6),也就是在此之前变动的最多位数,也就是s表的长度
        // 以后用随机av号,超过27个二进制位的时候,就可以把上面的封印解除啦
        const highestDigit = 6;

        for (let i = 0; i < highestDigit; i += 1) {
            pow58.push(BigInt(Math.pow(58, i)));
        }

        /**
         * @param s {string}
         */
        function list(s) {
            return s.split("");
        }

        /**
         * @param x {string} BV string; including the beginning "BV" (e.g. "BV17x411w7KC")
         */
        function dec(x) {
            let r = BigInt(0);

            for (let i = 0; i < highestDigit; i += 1) {
                r += tr[x[s[i]]] * pow58[i];
            }

            return Number((r - add) ^ xor);
        }

        /**
         * @param x {number} Exsiting AV number
         */
        function enc(x) {
            x = BigInt(x);
            x = (x ^ xor) + add;

            const r = list("BV1  4 1 7  ");

            for (let i = 0; i < highestDigit; i += 1) {
                const index = (x / pow58[i]) % BigInt(58);
                r[s[i]] = table[index];
            }

            return r.join("");
        }

        return {
            enc: enc,
            dec: dec
        };
    })();

    /**
     * @param avNumber {number | string | bigint}
     */
    function getAvUrl(avNumber) {
        return "/video/av" + avNumber.toString();
    }

    /**
     * @param avNumber {number | string | bigint}
     */
    function getAvText(avNumber) {
        return "av" + avNumber.toString();
    }

    /**
     * @param avNumber {number | string}
     */
    function appendOrUpdateAvNumberLink(avNumber) {
        const avInfoLabels = document.getElementsByClassName("video-data");
        // 第一行:分区和日期
        const infoBlock1 = avInfoLabels[0];

        // 投稿日期span
        // 直接塞到和infoBlock1同级会导致谜之排版错误,只好借壳咯(其实是Vue的锅
        const info1 = infoBlock1.children[1];

        // 我放弃创建新的元素了……干脆直接黑魔法换个a
        const avreg = /av\d+/;
        const oldContent = info1.textContent;
        
        if (!avreg.test(oldContent)) {
            const avUrl = getAvUrl(avNumber);
            const avText = getAvText(avNumber);
            info1.outerHTML = `<a href="${avUrl}" title="${avText}" target="_blank">${oldContent}\u00a0${avText}</a>`;
            // 以下两行本来是调整样式,让其显示跟原来差不多的,但是Vue似乎劫持了样式设定,所以没出效果也就算了
            info1.style.color = "#999";
            info1.style.height = "16px";
        }
    }

    /**
     * @param avNumber {number | string}
     */
    function modifyBangumiAvNumberLink(avNumber) {
        const pubs = document.getElementsByClassName("pub-wrapper");

        if (!pubs || pubs.length === 0) {
            return;
        }

        const avLink = pubs[0].querySelector("a.av-link");

        if (!avLink) {
            return;
        }

        avLink.textContent = getAvText(avNumber);
        avLink.href = getAvUrl(avNumber);
    }

    (() => {
        const bvUrlRE = /\/video\/(BV[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)/;
        const bvmatch = bvUrlRE.exec(window.location.href);

        if (bvmatch) {
            console.log("F**k You BV Number!");

            if (CONFIG.autoJump) {
                const bvstr = bvmatch[1];
                const avNumber = bv2av.dec(bvstr);
                const url = getAvUrl(avNumber);
                window.location.href = url;
            }
        }
    })();

    let isContentRefreshed = false;
    let avNumber = Number.NaN;
    let pageType = Number.NaN;

    const PageType = {
        video: 0,
        bangumi: 1
    };

    // TODO: 处理 HTML5 History 相关

    const bvidRE = /bvid=(BV[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)/;
    const aidRE = /aid=(\d+)/;
    const avidRE = /avid=(\d+)/;

    ah.proxy({
        //请求发起前进入
        onRequest: (config, handler) => {
            let match;

            if (config.url.indexOf("api.bilibili.com/x/web-interface/view") > 0) {
                match = bvidRE.exec(config.url);

                if (match) {
                    // https://api.bilibili.com/x/web-interface/view?cid=167028524&bvid=BV1TE411A7VJ
                    avNumber = bv2av.dec(match[1]);
                    isContentRefreshed = true;
                    pageType = PageType.video;
                } else {
                    match = aidRE.exec(config.url);

                    if (match) {
                        // https://api.bilibili.com/x/web-interface/view?cid=167028524&aid=97836354
                        avNumber = Number.parseInt(match[1]);
                        isContentRefreshed = true;
                        pageType = PageType.video;
                    }
                }
            } else if (config.url.indexOf("api.bilibili.com/pgc/player/web/playurl") > 0) {
                match = avidRE.exec(config.url);

                if (match) {
                    // https://api.bilibili.com/pgc/player/web/playurl?cid=122062626&qn=0&type=&otype=json&avid=70455036&ep_id=286039&fourk=1&fnver=0&fnval=16&session=xxx
                    avNumber = Number.parseInt(match[1]);
                    isContentRefreshed = true;
                    pageType = PageType.bangumi;
                }
            }

            handler.next(config);
        },

        //请求成功后进入
        onResponse: (response, handler) => {
            if (response.config.url.indexOf("api.bilibili.com") > 0) {
                if (isContentRefreshed) {
                    switch (pageType) {
                        case PageType.video: {
                            if (!Number.isNaN(avNumber)) {
                                setTimeout(() => appendOrUpdateAvNumberLink(avNumber));
                            }

                            break;
                        }
                        case PageType.bangumi: {
                            if (!Number.isNaN(avNumber)) {
                                setTimeout(() => modifyBangumiAvNumberLink(avNumber));
                            }

                            break;
                        }
                        default:
                            break;
                    }
                }
            }

            handler.next(response);
        }
    });
})();