Greasy Fork is available in English.

哔哩哔哩(B站|Bilibili)收藏夹Fix

修复 哔哩哔哩(www.bilibili.com) 失效的收藏。(可查看av号、简介、标题、封面)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         哔哩哔哩(B站|Bilibili)收藏夹Fix
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  修复 哔哩哔哩(www.bilibili.com) 失效的收藏。(可查看av号、简介、标题、封面)
// @author       Mr.Po
// @match        https://space.bilibili.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js
// @resource iconError https://cdn.jsdelivr.net/gh/Mr-Po/bilibili-favorites-fix/media/error.png
// @resource iconSuccess https://cdn.jsdelivr.net/gh/Mr-Po/bilibili-favorites-fix/media/success.png
// @resource iconInfo https://cdn.jsdelivr.net/gh/Mr-Po/bilibili-favorites-fix/media/info.png
// @connect      biliplus.com
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_getResourceURL
// ==/UserScript==

/*jshint esversion: 8 */
(function() {
    'use strict';

    /**
     * 失效收藏标题颜色(默认为灰色)。
     * @type {String}
     */
    const invalTitleColor = "#999";

    /**
     * 是否启用调试模式。
     * 启用后,浏览器控制台会显示此脚本运行时的调试数据。
     * @type {Boolean}
     */
    const isDebug = false;

    /**
     * 重试延迟[秒]。
     * @type {Number}
     */
    const retryDelay = 5;

    /**
     * 每隔 space [毫秒]检查一次,是否有新的收藏被加载出来。
     * 此值越小,检查越快;过小会造成浏览器卡顿。
     * @type {Number}
     */
    const space = 2000;

    /******************************************************/

    /**
     * 收藏夹地址正则
     * @type {RegExp}
     */
    const favlistRegex = /https:\/\/space\.bilibili\.com\/\d+\/favlist.*/;

    /**
     * 处理收藏
     */
    function handleFavorites() {

        const flag = favlistRegex.test(window.location.href);

        if (flag) { // 当前页面是收藏地址

            // 失效收藏节点集
            const $lis = $("ul.fav-video-list.content li.small-item.disabled");

            if ($lis.size() > 0) {

                console.info(`${$lis.size()}个收藏待修复...`);

                $lis.each(function(i, it) {

                    const bv = $(it).attr("data-aid");

                    const aid = bv2aid(bv);

                    // 多个超链接
                    const $as = $(it).find("a");

                    $as.attr("href", `https://www.biliplus.com/video/av${aid}/`);
                    $as.attr("target", "_blank");

                    addCopyAVCodeButton($(it), aid);

                    fixTitleAndPic($(it), $($as[1]), aid);

                    // 移除禁用样式
                    $(it).removeClass("disabled");
                    $as.removeClass("disabled");
                });

                showDetail($lis);
            }
        }
    }

    function addOperation($item, name, fun) {

        const $ul = $item.find(".be-dropdown-menu").first();

        const lastChild = $ul.children().last();

        // 未添加过扩展
        if (!lastChild.hasClass('be-dropdown-item-extend')) {
            lastChild.addClass("be-dropdown-item-delimiter");
        }

        const $li = $(`<li class="be-dropdown-item be-dropdown-item-extend">${name}</li>`);

        $li.click(fun);

        $ul.append($li);
    }

    function addCopyAVCodeButton($item, aid) {

        addOperation($item, "复制av号", function() {

            GM_setClipboard(`av${aid}`, "text");

            tipSuccess("av号复制成功!");
        });
    }

    function addCopyInfoButton($item, content) {

        addOperation($item, "复制简介", function() {

            GM_setClipboard(content, "text");

            tipSuccess("简介复制成功!");
        });
    }

    /**
     * 标记失效的收藏
     * @param  {$节点}  $it 当前收藏Item
     * @param  {$节点}  $a  标题链接
     */
    function signInval($it, $a) {

        // 收藏时间
        const $pubdate = $it.find("div.meta.pubdate");

        // 增加 删除线
        $pubdate.attr("style", "text-decoration:line-through");

        // 增加 删除线 + 置(灰)
        $a.attr("style", `text-decoration:line-through;color:${invalTitleColor};`);
    }

    /**
     * 绑定重新加载
     * @param  {$节点}  $a  标题链接
     * @param  {函数}   fun 重试方法
     */
    function bindReload($a, fun) {

        $a.text("->手动加载<-");

        $a.click(function() {

            $(this).unbind("click");

            $a.text("Loading...");

            fun();
        });
    }

    /**
     * 再次尝试加载
     * @param  {$节点}	$a  		标题链接
     * @param  {数字}	aid  		AV号
     * @param  {布尔}	delayRetry 	延迟重试
     * @param  {函数}	fun   		重试方法
     */
    function retryLoad($a, aid, delayRetry, fun) {

        console.warn(`查询:av${aid},请求过快!`);

        if (delayRetry) { // 延迟绑定

            $a.text(`请求过快,${retryDelay}秒后再试!`);

            setTimeout(bindReload, retryDelay * 1000, $a, fun);

            countdown($a, retryDelay);

        } else { // 首次,立即绑定

            $a.attr("href", "javascript:void(0);");

            bindReload($a, fun);
        }
    }

    /**
     * 重新绑定倒计时
     * @param  {$节点}  	$a  	标题链接
     * @param  {数字} 	second 	秒
     */
    function countdown($a, second) {

        if ($a.text().indexOf("请求过快") === 0) {

            $a.text(`请求过快,${second}秒后再试!`);

            if (second > 1) {
                setTimeout(countdown, 1000, $a, second - 1);
            }
        }
    }

    /**
     * 修复收藏
     * @param  {$节点}	$it 	当前收藏Item
     * @param  {$节点}  	$a  	标题链接
     * @param  {数字}   	aid 	av号
     * @param  {字符串} 	title   标题
     * @param  {字符串} 	pic     海报
     * @param  {字符串} 	history 历史归档,若无时,使用空字符串
     */
    function fixFavorites($it, $a, aid, title, pic, history) {

        // 设置标题
        $a.text(title);
        $a.attr("title", $a.text());

        // 多个超链接
        const $as = $it.find("a");
        $as.attr("href", `https://www.biliplus.com/${history}video/av${aid}/`);

        signInval($it, $a);

        // 判断海报链接是否有效,有效时进行替换
        isLoad(pic, function() {
            const $img = $it.find("img");
            $img.attr("src", pic);
        });
    }

    /**
     * 修复标题和海报
     * @param  {$节点}  $it 当前收藏Item
     * @param  {$节点}  $a  标题链接
     * @param  {数字}   aid av号
     */
    function fixTitleAndPic($it, $a, aid) {

        $a.text("Loading...");

        fixTitleAndPicEnhance3($it, $a, aid);
    }

    /**
     * 修复标题和海报 增强 - 0
     * 使用公开的API
     * @param  {$节点}   $it 当前收藏Item
     * @param  {$节点}   $a  标题链接
     * @param  {数字}    aid av号
     * @param  {布尔}    delayRetry 延迟重试
     */
    function fixTitleAndPicEnhance0($it, $a, aid, delayRetry) {

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.biliplus.com/api/view?id=${aid}`,
            responseType: "json",
            onload: function(response) {

                const res = response.response;

                if (isDebug) {
                    console.log("0---->:");
                    console.log(res);
                }

                // 找到了
                if (res.title) {

                    fixFavorites($it, $a, aid, res.title, res.pic, "");

                } else if (res.code == -503) { // 请求过快

                    retryLoad($a, aid, delayRetry, function() {
                        fixTitleAndPicEnhance0($it, $a, aid, true);
                    });

                } else { // 未找到

                    fixTitleAndPicEnhance1($it, $a, aid);
                }
            },
            onerror: function(e) {
                console.log("出错啦");
                console.log(e);
            }
        });
    }

    /**
     * 修复标题和海报 增强 - 1
     * 使用cache库
     * @param  {$节点}  $it 当前收藏Item
     * @param  {$节点}  $a  标题链接
     * @param  {数字}   aid av号
     */
    function fixTitleAndPicEnhance1($it, $a, aid) {

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.biliplus.com/all/video/av${aid}/`,
            onload: function(response) {

                if (isDebug) {
                    console.log("1---->:");
                    console.log(response.response);
                }

                const params = response.responseText.match(/getjson\('(\/api\/view_all.+)'/);

                fixTitleAndPicEnhance2($it, $a, aid, params[1]);
            }
        });
    }

    /**
     * 修复标题和海报 增强 - 2
     * 使用cache库,第一段,需与fixTitleAndPicEnhance1连用
     * @param  {$节点}  	$it 		当前收藏Item
     * @param  {$节点}  	$a  		标题链接
     * @param  {数字}   	aid 		av号
     * @param  {字符串} 	param 		待拼接参数
     * @param  {布尔}  	delayRetry	延迟重试
     */
    function fixTitleAndPicEnhance2($it, $a, aid, param, delayRetry) {

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.biliplus.com${param}`,
            responseType: "json",
            onload: function(response) {

                const res = response.response;

                if (isDebug) {
                    console.log("2---->:");
                    console.log(res);
                }

                // 找到了
                if (res.code === 0) {

                    fixFavorites($it, $a, aid, res.data.info.title, res.data.info.pic, "all/");

                } else if (res.code == -503) { // 请求过快

                    retryLoad($a, aid, delayRetry, function() {
                        fixTitleAndPicEnhance2($it, $a, aid, param, true);
                    });

                } else { // 未找到

                    $a.text(`已失效(${aid})`);
                    $a.attr("title", $a.text());
                }
            }
        });
    }

    /**
     * 修复标题和海报 增强 - 3
     * 模拟常规查询
     * @param  {$节点}  	$it 当前收藏Item
     * @param  {$节点}  	$a  标题链接
     * @param  {数字}   	aid av号
     */
    function fixTitleAndPicEnhance3($it, $a, aid) {

        let jsonRegex;

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.biliplus.com/video/av${aid}/`,
            onload: function(response) {

                try {

                    if (isDebug) {
                        console.log("3---->:");
                        console.log(response.response);
                    }

                    jsonRegex = response.responseText.match(/window\.addEventListener\('DOMContentLoaded',function\(\){view\((.+)\);}\);/);

                    if (isDebug) {
                        console.log(jsonRegex);
                    }

                    const jsonStr = jsonRegex[1];

                    if (isDebug) {
                        console.log(jsonStr);
                    }

                    const res = $.parseJSON(jsonStr);

                    if (res.title) { // 存在

                        fixFavorites($it, $a, aid, res.title, res.pic, "");

                    } else if (res.code == -503) { // 请求过快

                        retryLoad($a, aid, null, function() {
                            fixTitleAndPicEnhance0($it, $a, aid, true);
                        });

                    } else { // 不存在

                        fixTitleAndPicEnhance1($it, $a, aid);
                    }

                } catch (err) {

                    console.error(err);
                    console.log(jsonRegex);

                    // 当出现错误时,出现手动加载
                    retryLoad($a, aid, null, function() {
                        fixTitleAndPicEnhance0($it, $a, aid, true);
                    });
                }
            }
        });
    }

    /**
     * 判断一个url是否可以访问
     * @param  {字符串}  url http地址
     * @param  {函数}  	fun 有效时的回调
     */
    function isLoad(url, fun) {
        $.ajax({
            url: url,
            type: 'GET',
            success: function(response) {
                fun();
            },
            error: function(e) {}
        });
    }

    /**
     * 显示详细
     * @param  {$节点} $lis 失效收藏节点集
     */
    function showDetail($lis) {

        const fidRegex = window.location.href.match(/fid=(\d+)/);

        let fid;

        if (fidRegex) {
            fid = fidRegex[1];
        } else {
            fid = $("div.fav-item.cur").attr("fid");
        }

        const pn = $("ul.be-pager li.be-pager-item.be-pager-item-active").text();

        $.ajax({
            url: `https://api.bilibili.com/medialist/gateway/base/spaceDetail?media_id=${fid}&pn=${pn}&ps=20&keyword=&order=mtime&type=0&tid=0&jsonp=jsonp`,
            success: function(json) {

                const $medias = json.data.medias;

                $lis.each(function(i, it) {

                    const bv = $(it).attr("data-aid");

                    const $mediaF = $medias.filter(function(it) {
                        if (it.bvid == bv) {
                            return it;
                        }
                    });

                    const $media = $mediaF[0];

                    const $a = $(it).find("a");

                    let titles = "";

                    if ($media.pages) {

                        const $titlesM = $media.pages.map(function(it, i, arry) {
                            return it.title;
                        });

                        titles = $titlesM.join("、");
                    }

                    const aid = bv2aid(bv);

                    const content = `av:${aid}\nP数:${$media.page}\n子P:${titles}\n简介:${$media.intro}`;

                    $($a[0]).attr("title", content);

                    addCopyInfoButton($(it), content);
                });
            }
        });
    }

    const bvTable = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF";
    const bvArray = [
        { bvIndex: 11, bvTimes: 1 },
        { bvIndex: 10, bvTimes: 58 },
        { bvIndex: 3, bvTimes: 3364 },
        { bvIndex: 8, bvTimes: 195112 },
        { bvIndex: 4, bvTimes: 11316496 },
        { bvIndex: 6, bvTimes: 656356768 },
    ];
    const bvXor = 177451812;
    const bvAdd = 8728348608;


    /**
     * BV号转aid
     * @param  {字符串}	bv BV号
     * @return {数字}	av号
     */
    function bv2aid(bv) {

        const value = bvArray
            .map((it, i) => {
                return bvTable.indexOf(bv[it.bvIndex]) * it.bvTimes;
            })
            .reduce((total, num) => {
                return total + num;
            });

        return (value - bvAdd) ^ bvXor;
    }

    function tip(text, iconName) {
        GM_notification({
            text: text,
            image: GM_getResourceURL(iconName)
        });
    }

    function tipInfo(text) {
        tip(text, "iconInfo");
    }

    function tipError(text) {
        tip(text, "iconError");
    }

    function tipSuccess(text) {
        tip(text, "iconSuccess");
    }

    setInterval(handleFavorites, space);
})();