B站-破茧

破除信息茧房,我看什么我决定!

// ==UserScript==
// @name        B站-破茧
// @namespace   Violentmonkey Scripts
// @match       *://www.bilibili.com/video/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant       GM_unregisterMenuCommand
// @grant       GM_addValueChangeListener
// @grant       GM_info
// @grant       GM_addElement
// @grant       GM_addStyle
// @grant       GM_openInTab
// @version     0.4.7.8
// @inject-into page
// @require     https://greasyfork.org/scripts/476522-config-manager/code/Config_Manager.js?version=1285584
// @author      axototl
// @license     MPL-2.0
// @description 破除信息茧房,我看什么我决定!
// @icon        https://s1.hdslb.com/bfs/static/laputa-home/client/assets/vip-login-banner.c0cbe3b2.png
// ==/UserScript==
'use strict';

(() => {
    if (GM_info.scriptHandler === "Violentmonkey") return true;
    GM_addElement("p", {
        textContent: `脚本 ${GM_info.script.name} 大量使用了暴力猴独有特性, 建议使用Violentmonkey(暴力猴)获得更好体验.`,
        style: "position: fixed; bottom: 0; right: 0; color: #F92672; font-size: 16px; font-weight: bold;"
    }).onclick = () => GM_openInTab("https://violentmonkey.github.io/get-it/");
    return false;
})();

let kw;
(() => {
    const upd = (x => kw = x?.split(' ').filter(el => !!el));
    upd(GM_getValue("keywords"));
    GM_registerMenuCommand("设置自定义搜索关键词", () => {
        const p = prompt("请输入自定义搜索关键词,用 空格 隔开", kw.join(" "));
        if (p == null) return;
        GM_setValue("keywords", p || null);
    });
    GM_addValueChangeListener("keywords", (_1, _2, nv) => upd(nv));
})();


const translate = json => {
    const params = new URLSearchParams();
    for (const it in json)
        params.append(it, json[it]);
    return params;
};


let sender;
const sendTrash = (uri, body) => fetch(uri, {
    referrer: "no-referrer",
    referrerPolicy: "no-referrer",
    method: "POST",
    credentials: "omit",
    priority: "low",
    mode: 'no-cors',
    body,
});

let Random; // Random-js Lib
let engine;

const delay = n => new Promise(resolve => setTimeout(resolve, n));

// const cid_api = [
//     "https://api.bilibili.com/x/web-interface/view/detail?aid=",
//     "https://api.bilibili.com/x/web-interface/view?aid="
// ];

const vid_arr = [];
const get_rand = async () => {
    const MAX = config.maxaid;
    const _rnd_helper = Random.integer(2, MAX);
    const getf = () => _rnd_helper(engine);
    for (let aid; ;) {
        if (vid_arr.length <= 0)
            for (let _ = 1; _ <= 20; ++_) vid_arr.push(getf());
        aid = vid_arr.pop();
        let res = await (await fetch("https://api.bilibili.com/x/web-interface/view?aid=" + aid)).json();
        if (res.code == -412) {
            throw Error("API被拦截");
        }
        if (res.code != 0) {
            await delay(500);
            continue;
        }
        res = res.data;
        if (undefined != res.forward) {
            aid = res.forward;
            continue;
        }
        let subtype = null;
        let type = res.rights?.movie ? (subtype = 2, 4) : 3;
        res = res.pages;
        const ct = res[(Math.random() * res.length) | 0];
        return {aid, type, subtype, ...ct};
    }
}

const getmid = async () => {
    let js = await (await fetch("https://api.bilibili.com/x/web-interface/nav", {credentials: "include"})).json();
    // console.debug(js);
    return js.data?.mid;
};

let jscookie;

// getmid();

let id0, id2;

const main_light = () => {
    id0 = setInterval(async () => {
        if (!config.cookie) {
            config.light_main = false;
            return;
        }
        const csrf = jscookie.get("bili_jct");
        if (!csrf) return;
        const {aid, cid, duration: last} = await get_rand();
        const para = {
            aid, cid,
            progress: Math.trunc(last*Math.random()),
            csrf
        };
        const params = translate(para);
        sender("https://api.bilibili.com/x/v2/history/report", params);
    }, config.delay);
};


// 建议根据自己的喜好动态调整
const refer_uris = [
    "https://www.bilibili.com/",
    "https://www.bing.com/",
    "https://www.zhihu.com/",
    "https://example.org/",
    '',
];

const main_classical = () => {

    const engine = Random.MersenneTwister19937.autoSeed();
    const _die4 = Random.die(4);
    const die4 = () => _die4(engine);
    // let x = 0;
    // let reducing = false;
    id0 = setInterval(async () => {
        // if (x > 65536) reducing = true;
        // if (reducing)
        //     if (--x > 0) return;
        //     else reducing = false;
        // else ++x;
        // 暂时禁用冷却
        // 冷却时间 65536 防止卡顿
        const {aid, cid, duration: last, type, subtype: sub_type} = await get_rand();
        // const time2 = (next()) % last;
        const time = Random.die(last)(engine);
        // console.log("time = ", time, "time2 = ", t2);
        const now = (Date.now() / 1000 | 0);
        const real = (1 + die4()) * time;
        const play_type = Random.pick(engine, [0, 2, 3]);
        const para = {
            start_ts: now - real,
            aid, cid, type,
            sub_type: sub_type ?? 0,
            dt: 2,
            play_type,
            realtime: real,
            played_time: time,
            real_played_time: time,
            refer_url: Random.pick(engine, refer_uris),
            quality: 0,
            video_duration: last,
            last_play_progress_time: time,
            max_play_progress_time: time,
            outer: 0,
            extra: '{"player_version":"4.5.7"}',
        };
        const jct = config.cookie ? jscookie.get("bili_jct") : undefined;
        // console.debug("your CSRF token(bili_jct) =", jct);
        if (!!jct) para.csrf = jct;
        const params = translate(para);
        // console.debug("parsing -------->");
        sender("https://api.bilibili.com/x/click-interface/web/heartbeat", params);
    }, config.delay);
};
const unload = () => (clearInterval(id0));

const load_query = () => {
    if (kw?.length == 0) return;
    const _ref_pick = Random.picker(kw);
    const ref_pick = () =>_ref_pick(engine);
    const cfg = {credentials: "include", referrer: "search.bilibili.com"};
    const MAX = 100;
    id2 = setInterval(async () => {
        let res = fetch("https://api.bilibili.com/x/web-interface/search/all/v2?keyword=" + ref_pick(), cfg);
        if (vid_arr.length > MAX || !config.main) return;
        res = await (await res).json();
        if (res.code != 0) return config.query = false;
        res = res.data.result;
        while(res.length > 0) {
            const s = res.pop();
            if (s.result_type === 'video') {
                res = s;
                break;
            }
        }
        if (typeof res !== 'object') return;
        res = res.data;
        Random.shuffle(engine, res);
        for (const x of res) vid_arr.push(x.aid);
    }, 15e3);
};
const unload_query = () => clearInterval(id2);

let load;

const _need_cookie = (_1, val) => val ? config.cookie : true;

const reload_cb = (_1, _2, nv) => {
    if (!config.main) return;
    unload();
    load = (nv ? main_light : main_classical);
    load();
};

let __dislike_div;
const cfgs = [
    {
        name: "delay",
        type: "uint",
        desc: "设置轮询时间",
        tips: "设置轮询时间(请输入正数)",
        default: 60e3,
    },
    {
        name: "maxaid",
        type: "uint",
        desc: "设置最大AV号",
        tips: "请输入轮询应当获取到的最大av号\n(注意只需要输入数字即可)",
        default: (1e9|0),
    },
    {
        name: "use_beacon",
        type: "bool",
        default: false,
        desc: "[实验性] 使用SendBeacon API(缓解内存泄露)",
        callback: (_1, _2, nv) => (sender = (nv ? (uri, para) => navigator.sendBeacon(uri, para) : sendTrash)),
        init: true,
        autoClose: false,
    },
    {
        name: "query",
        type: 'bool',
        desc: "轮询搜索API (定制推送内容)",
        callback: (_1, _2, nv) => ((nv ? load_query : unload_query)()),
        default: false,
        init: true,
        autoClose: false,
    },
    {
        name: "cookie",
        type: "bool",
        desc: "携带Cookie(会污染历史记录)",
        default: true,
        judge: (_, i) => !i || !!cookie.get("bili_jct"),
        autoClose: false,
    },
    {
        name: "light_main",
        type: "bool",
        desc: "[试验性] 使用轻量版主程序",
        default: false,
        callback: reload_cb,
        init: (_1, val) => load = (val ? main_light : main_classical),
        autoClose: false,
        judge: _need_cookie,
    },
    {
        name: "main",
        type: 'bool',
        desc: "主程序 (清洗个人喜好)",
        callback: ((_1, _2, nv) => (nv ? load : unload)()),
        default: true,
        init: true,
    },
    {
        name: "access_key",
        type: "other",
        desc: "设置access_token",
        tips: "请输入access_token",
        default: null,
    },
    {
        name: "dislike",
        type: "bool",
        desc: "[实验性] 点踩 (需APP_token)",
        callback: (_1, _2, nv) => (nv ? (__dislike_div = dislike_f()) : __dislike_div?.remove()),
        judge: () => (alert("注意,只有在bilibili.com/video/av123456 情况下可用 BV号不可用"), true),
        init: (name, val) => (val ? (__dislike_div = dislike_f()) : 0),
    },
];

let md5;
const require = mod => import(`https://fastly.jsdelivr.net/npm/${mod}/+esm`);

(async () => {
    Random = await require("random-js");
    // engine = Random.MersenneTwister19937.autoSeed();
    engine = Random.nativeMath;
    const {default: cookie} = await require("js-cookie");
    jscookie = cookie;
    const {md5: hash} = await require("js-md5");
    md5 = hash;
    // console.debug("Loading.....");
    cfgs.forEach(register);
    GM_registerMenuCommand("[实验性] 登录(获取Access Token)", getkey_ui);


    GM_addStyle(`
#gm_deslike {
    /* 设置形状及阴影 */
    color: white;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: black;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);

    display: flex;
    align-items: center;
    justify-content: center;

    /* 设置按钮的位置 */
    position: fixed;
    right: 20px;
    bottom: 20px;

    /* 设置过渡属性 */
    transition: width 0.5s, height 0.5s;
}

#gm_deslike:hover {
    /* 鼠标悬停时 */
    width: 60px;
    height: 60px;
}
`);
})();


async function dislike_f() {
    const paras = {
        access_key: config.access_key,
        dislike: 0,
    };
    // console.warn("11111111111111111111111111111");
    let id = /av(\d+)/.exec(location.pathname);
    if (id) id = id[1];
    else {
        id = /BV(\w+)/.exec(location.pathname);
        if (!id) return -2;
        id = (bv => {
            const pos = [11, 10, 3, 8, 4, 6];
            const base = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF';
            const table = {};
            for (let i = 0; i < base.length; i++) table[base[i]] = i;
            let r = 0;
            for (let i = 0; i < pos.length; i++) r += table[bv[pos[i]]] * 58 ** i;
            return (r - 8728348608) ^ 177451812;
        })(id[0]);
    }
    // console.warn("你好222222222222222");
    paras.aid = id;
    const div = GM_addElement("div", {id: "gm_deslike"});
    const p = GM_addElement(div, "p", {textContent: "不喜欢"});
    const par = translate(paras);
    par.sort();
    p.onmousedown = async ev => {
        div.remove();
        if (ev.button == 2) return;
        let res = fetch("https://app.biliapi.net/x/v2/view/dislike", {
            referrer: "no-referrer",
            referrerPolicy: "no-referrer",
            method: "POST",
            credentials: "omit",
            priority: "low",
            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
            body: par,
        });
        res = (await res);
        console.log(res);
        res = await res.json();
        if (res.code === 0) return;
        alert("点踩失败\n 原因: " + res.message);
    };
    return div;
}


function api_signer (salt, appkey, paras) {
    let par = {
        appkey,
        local_id: 0,
        ts: Math.trunc(Date.now()/1000),
    };
    Object.assign(par, paras);
    par = translate(par);
    par.sort();
    const sign = md5('' + par + salt);
    par.append("sign", sign);
    return {
        method: "POST",
        credentials: "omit",
        referrer: "no-referrer",
        referrerPolicy: "no-referrer",
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: par,
    };
}

let _share_win;
async function _alerter(uri) {
    const {default: QRCode} = await require("qrcode-svg");
    let img = new QRCode({content: uri}).svg();
    img = new Blob([img], {type: "image/svg+xml"});
    img = URL.createObjectURL(img);
    // console.log("你好 =========================");
    let xhtm =
`<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
    <script>
    setTimeout(()=>{
        alert("已超时!请重新获取二维码。");
        window.close();
    }, ${180e3 | 0});
    </script>
</head>
<body>
    <p style="font-size: 30px;">请在180秒内扫扫描以下二维码登录 <br/><br/> 若过期请关闭此标签页并重新开始</p>
    <img src="${img}" />
</body>
</html>
`;
    xhtm = new Blob([xhtm], {type: "application/xhtml+xml"});
    xhtm = URL.createObjectURL(xhtm);
    const revoker = () => [img, xhtm].forEach(URL.revokeObjectURL);
    if (GM_info.platform?.browserName === "firefox") {
        _share_win = window.open(xhtm, "_blank"); // Firefox disallow calling GM_openInTab with Data URL.
        alert("请注意窗口顶部,请允许打开弹出式窗口");
    } else {
        _share_win = GM_openInTab(xhtm);
    }
    setTimeout(revoker, 180e3 | 0);
}

async function getkey_ui () {
    const sign = par => api_signer("59b43e04ad6965f34319062b478f83dd", "4409e2ce8ffd12b8", par);
    let res = fetch("https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code", sign());
    res = await (await res).json();
    console.log(" =====> parsing <======");
    if (res.code != 0) return new Error(res.message);
    res = res.data;
    await _alerter(res.url);
    const auth_code = res.auth_code;
    const get_id_worker = setInterval(async () => {
        let res = fetch("https://passport.bilibili.com/x/passport-tv-login/qrcode/poll", sign({auth_code}));
        res = await (await res).json();
        if (res.code == 86038 || res.code < 0) {
            clearInterval(get_id_worker);
            throw "Timeout!";
        } else if (res.code != 0) return console.warn("登录未成功,原因:", res.message);
        res = res.data;
        config.access_key = res.access_token;
        clearInterval(get_id_worker);
        _share_win.close();
        alert("登录成功! 已获取到Access Token!");
    }, 5000);
};