BilibiliPromote

增强B站脚本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         BilibiliPromote
// @namespace    https://github.com/Mlikiowa/BilibiliPromote
// @version      1.0.0
// @description  增强B站脚本
// @author       Mlikiowa
// @license MIT
// @match        *://bilibili.com/*
// @match        *://*.bilibili.com/*
// @connect      *
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

var ajaxHooker = function() {
    'use strict';
    const win = window.unsafeWindow || document.defaultView || window;
    const toString = Object.prototype.toString;
    const getDescriptor = Object.getOwnPropertyDescriptor;
    const hookFns = [];
    const realXhr = win.XMLHttpRequest;
    const realFetch = win.fetch;
    const resProto = win.Response.prototype;
    const xhrResponses = ['response', 'responseText', 'responseXML'];
    const fetchResponses = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
    const fetchInitProps = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect',
        'referrer', 'referrerPolicy', 'integrity', 'keepalive', 'signal', 'priority'];
    const xhrAsyncEvents = ['readystatechange', 'load', 'loadend'];
    let filter;
    function emptyFn() {}
    function errorFn(err) {
        console.error(err);
    }
    function defineProp(obj, prop, getter, setter) {
        Object.defineProperty(obj, prop, {
            configurable: true,
            enumerable: true,
            get: getter,
            set: setter
        });
    }
    function readonly(obj, prop, value = obj[prop]) {
        defineProp(obj, prop, () => value, emptyFn);
    }
    function writable(obj, prop, value = obj[prop]) {
        Object.defineProperty(obj, prop, {
            configurable: true,
            enumerable: true,
            writable: true,
            value: value
        });
    }
    function shouldFilter(type, url, method, async) {
        return filter && !filter.find(obj => {
            switch (true) {
                case obj.type && obj.type !== type:
                case toString.call(obj.url) === '[object String]' && !url.includes(obj.url):
                case toString.call(obj.url) === '[object RegExp]' && !obj.url.test(url):
                case obj.method && obj.method.toUpperCase() !== method.toUpperCase():
                case 'async' in obj && obj.async !== async:
                    return false;
            }
            return true;
        });
    }
    function parseHeaders(obj) {
        const headers = {};
        switch (toString.call(obj)) {
            case '[object String]':
                for (const line of obj.trim().split(/[\r\n]+/)) {
                    const parts = line.split(/\s*:\s*/);
                    if (parts.length !== 2) continue;
                    const lheader = parts[0].toLowerCase();
                    if (lheader in headers) {
                        headers[lheader] += ', ' + parts[1];
                    } else {
                        headers[lheader] = parts[1];
                    }
                }
                return headers;
            case '[object Headers]':
                for (const [key, val] of obj) {
                    headers[key] = val;
                }
                return headers;
            case '[object Object]':
                return {...obj};
            default:
                return headers;
        }
    }
    class AHRequest {
        constructor(request) {
            this.request = request;
            this.requestClone = {...this.request};
            this.response = {};
        }
        waitForHookFns() {
            return Promise.all(hookFns.map(fn => {
                try {
                    return Promise.resolve(fn(this.request)).then(emptyFn, errorFn);
                } catch (err) {
                    console.error(err);
                }
            }));
        }
        waitForResponseFn() {
            try {
                return Promise.resolve(this.request.response(this.response)).then(emptyFn, errorFn);
            } catch (err) {
                console.error(err);
                return Promise.resolve();
            }
        }
        waitForRequestKeys() {
            if (this.reqPromise) return this.reqPromise;
            const requestKeys = ['url', 'method', 'abort', 'headers', 'data'];
            return this.reqPromise = this.waitForHookFns().then(() => Promise.all(
                requestKeys.map(key => Promise.resolve(this.request[key]).then(
                    val => this.request[key] = val,
                    e => this.request[key] = this.requestClone[key]
                ))
            ));
        }
        waitForResponseKeys() {
            if (this.resPromise) return this.resPromise;
            const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses;
            return this.resPromise = this.waitForResponseFn().then(() => Promise.all(
                responseKeys.map(key => {
                    const descriptor = getDescriptor(this.response, key);
                    if (descriptor && 'value' in descriptor) {
                        return Promise.resolve(descriptor.value).then(
                            val => this.response[key] = val,
                            e => delete this.response[key]
                        );
                    } else {
                        delete this.response[key];
                    }
                })
            ));
        }
    }
    class XhrEvents {
        constructor() {
            this.events = {};
        }
        add(type, event) {
            if (type.startsWith('on')) {
                this.events[type] = typeof event === 'function' ? event : null;
            } else {
                this.events[type] = this.events[type] || new Set();
                this.events[type].add(event);
            }
        }
        remove(type, event) {
            if (type.startsWith('on')) {
                this.events[type] = null;
            } else {
                this.events[type] && this.events[type].delete(event);
            }
        }
        _sIP() {
            this.ajaxHooker_isStopped = true;
        }
        trigger(e) {
            if (e.ajaxHooker_isTriggered || e.ajaxHooker_isStopped) return;
            e.stopImmediatePropagation = this._sIP;
            this.events[e.type] && this.events[e.type].forEach(fn => {
                !e.ajaxHooker_isStopped && fn.call(e.target, e);
            });
            this.events['on' + e.type] && this.events['on' + e.type].call(e.target, e);
            e.ajaxHooker_isTriggered = true;
        }
        clone() {
            const eventsClone = new XhrEvents();
            for (const type in this.events) {
                if (type.startsWith('on')) {
                    eventsClone.events[type] = this.events[type];
                } else {
                    eventsClone.events[type] = new Set([...this.events[type]]);
                }
            }
            return eventsClone;
        }
    }
    const xhrMethods = {
        readyStateChange(e) {
            if (e.target.readyState === 4) {
                e.target.dispatchEvent(new CustomEvent('ajaxHooker_responseReady', {detail: e}));
            } else {
                e.target.__ajaxHooker.eventTrigger(e);
            }
        },
        asyncListener(e) {
            e.target.__ajaxHooker.eventTrigger(e);
        },
        setRequestHeader(header, value) {
            const ah = this.__ajaxHooker;
            ah.originalXhr.setRequestHeader(header, value);
            if (this.readyState !== 1) return;
            if (header in ah.headers) {
                ah.headers[header] += ', ' + value;
            } else {
                ah.headers[header] = value;
            }
        },
        addEventListener(...args) {
            const ah = this.__ajaxHooker;
            if (xhrAsyncEvents.includes(args[0])) {
                ah.proxyEvents.add(args[0], args[1]);
            } else {
                ah.originalXhr.addEventListener(...args);
            }
        },
        removeEventListener(...args) {
            const ah = this.__ajaxHooker;
            if (xhrAsyncEvents.includes(args[0])) {
                ah.proxyEvents.remove(args[0], args[1]);
            } else {
                ah.originalXhr.removeEventListener(...args);
            }
        },
        open(method, url, async = true, ...args) {
            const ah = this.__ajaxHooker;
            ah.url = url.toString();
            ah.method = method.toUpperCase();
            ah.async = !!async;
            ah.openArgs = args;
            ah.headers = {};
            for (const key of xhrResponses) {
                ah.proxyProps[key] = {
                    get: () => {
                        const val = ah.originalXhr[key];
                        ah.originalXhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
                            detail: {key, val}
                        }));
                        return val;
                    }
                };
            }
            return ah.originalXhr.open(method, url, ...args);
        },
        sendFactory(realSend) {
            return function(data) {
                const ah = this.__ajaxHooker;
                const xhr = ah.originalXhr;
                if (xhr.readyState !== 1) return realSend.call(xhr, data);
                ah.eventTrigger = e => ah.proxyEvents.trigger(e);
                if (shouldFilter('xhr', ah.url, ah.method, ah.async)) {
                    xhr.addEventListener('ajaxHooker_responseReady', e => {
                        ah.eventTrigger(e.detail);
                    }, {once: true});
                    return realSend.call(xhr, data);
                }
                const request = {
                    type: 'xhr',
                    url: ah.url,
                    method: ah.method,
                    abort: false,
                    headers: ah.headers,
                    data: data,
                    response: null,
                    async: ah.async
                };
                if (!ah.async) {
                    const requestClone = {...request};
                    hookFns.forEach(fn => {
                        try {
                            toString.call(fn) === '[object Function]' && fn(request);
                        } catch (err) {
                            console.error(err);
                        }
                    });
                    for (const key in request) {
                        if (toString.call(request[key]) === '[object Promise]') {
                            request[key] = requestClone[key];
                        }
                    }
                    xhr.open(request.method, request.url, ah.async, ...ah.openArgs);
                    for (const header in request.headers) {
                        xhr.setRequestHeader(header, request.headers[header]);
                    }
                    data = request.data;
                    xhr.addEventListener('ajaxHooker_responseReady', e => {
                        ah.eventTrigger(e.detail);
                    }, {once: true});
                    realSend.call(xhr, data);
                    if (toString.call(request.response) === '[object Function]') {
                        const response = {
                            finalUrl: xhr.responseURL,
                            status: xhr.status,
                            responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
                        };
                        for (const key of xhrResponses) {
                            defineProp(response, key, () => {
                                return response[key] = ah.originalXhr[key];
                            }, val => {
                                if (toString.call(val) !== '[object Promise]') {
                                    delete response[key];
                                    response[key] = val;
                                }
                            });
                        }
                        try {
                            request.response(response);
                        } catch (err) {
                            console.error(err);
                        }
                        for (const key of xhrResponses) {
                            ah.proxyProps[key] = {get: () => response[key]};
                        };
                    }
                    return;
                }
                const req = new AHRequest(request);
                req.waitForRequestKeys().then(() => {
                    if (request.abort) return;
                    xhr.open(request.method, request.url, ...ah.openArgs);
                    for (const header in request.headers) {
                        xhr.setRequestHeader(header, request.headers[header]);
                    }
                    data = request.data;
                    xhr.addEventListener('ajaxHooker_responseReady', e => {
                        if (typeof request.response !== 'function') return ah.eventTrigger(e.detail);
                        req.response = {
                            finalUrl: xhr.responseURL,
                            status: xhr.status,
                            responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
                        };
                        for (const key of xhrResponses) {
                            defineProp(req.response, key, () => {
                                return req.response[key] = ah.originalXhr[key];
                            }, val => {
                                delete req.response[key];
                                req.response[key] = val;
                            });
                        }
                        const resPromise = req.waitForResponseKeys().then(() => {
                            for (const key of xhrResponses) {
                                if (!(key in req.response)) continue;
                                ah.proxyProps[key] = {
                                    get: () => {
                                        const val = req.response[key];
                                        xhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
                                            detail: {key, val}
                                        }));
                                        return val;
                                    }
                                };
                            }
                        });
                        xhr.addEventListener('ajaxHooker_readResponse', e => {
                            const descriptor = getDescriptor(req.response, e.detail.key);
                            if (!descriptor || 'get' in descriptor) {
                                req.response[e.detail.key] = e.detail.val;
                            }
                        });
                        const eventsClone = ah.proxyEvents.clone();
                        ah.eventTrigger = event => resPromise.then(() => eventsClone.trigger(event));
                        ah.eventTrigger(e.detail);
                    }, {once: true});
                    realSend.call(xhr, data);
                });
            };
        }
    };
    function fakeXhr() {
        const xhr = new realXhr();
        let ah = xhr.__ajaxHooker;
        let xhrProxy = xhr;
        if (!ah) {
            const proxyEvents = new XhrEvents();
            ah = xhr.__ajaxHooker = {
                headers: {},
                originalXhr: xhr,
                proxyProps: {},
                proxyEvents: proxyEvents,
                eventTrigger: e => proxyEvents.trigger(e),
                toJSON: emptyFn // Converting circular structure to JSON
            };
            xhrProxy = new Proxy(xhr, {
                get(target, prop) {
                    try {
                        if (target === xhr) {
                            if (prop in ah.proxyProps) {
                                const descriptor = ah.proxyProps[prop];
                                return descriptor.get ? descriptor.get() : descriptor.value;
                            }
                            if (typeof xhr[prop] === 'function') return xhr[prop].bind(xhr);
                        }
                    } catch (err) {
                        console.error(err);
                    }
                    return target[prop];
                },
                set(target, prop, value) {
                    try {
                        if (target === xhr && prop in ah.proxyProps) {
                            const descriptor = ah.proxyProps[prop];
                            descriptor.set ? descriptor.set(value) : (descriptor.value = value);
                        } else {
                            target[prop] = value;
                        }
                    } catch (err) {
                        console.error(err);
                    }
                    return true;
                }
            });
            xhr.addEventListener('readystatechange', xhrMethods.readyStateChange);
            xhr.addEventListener('load', xhrMethods.asyncListener);
            xhr.addEventListener('loadend', xhrMethods.asyncListener);
            for (const evt of xhrAsyncEvents) {
                const onEvt = 'on' + evt;
                ah.proxyProps[onEvt] = {
                    get: () => proxyEvents.events[onEvt] || null,
                    set: val => proxyEvents.add(onEvt, val)
                };
            }
            for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open']) {
                ah.proxyProps[method] = { value: xhrMethods[method] };
            }
        }
        ah.proxyProps.send = { value: xhrMethods.sendFactory(xhr.send) };
        return xhrProxy;
    }
    function hookFetchResponse(response, req) {
        for (const key of fetchResponses) {
            response[key] = () => new Promise((resolve, reject) => {
                if (key in req.response) return resolve(req.response[key]);
                resProto[key].call(response).then(res => {
                    req.response[key] = res;
                    req.waitForResponseKeys().then(() => {
                        resolve(key in req.response ? req.response[key] : res);
                    });
                }, reject);
            });
        }
    }
    function fakeFetch(url, options = {}) {
        if (!url) return realFetch.call(win, url, options);
        let init = {...options};
        if (toString.call(url) === '[object Request]') {
            init = {};
            for (const prop of fetchInitProps) init[prop] = url[prop];
            Object.assign(init, options);
            url = url.url;
        }
        url = url.toString();
        init.method = init.method || 'GET';
        init.headers = init.headers || {};
        if (shouldFilter('fetch', url, init.method, true)) return realFetch.call(win, url, init);
        const request = {
            type: 'fetch',
            url: url,
            method: init.method.toUpperCase(),
            abort: false,
            headers: parseHeaders(init.headers),
            data: init.body,
            response: null,
            async: true
        };
        const req = new AHRequest(request);
        return new Promise((resolve, reject) => {
            req.waitForRequestKeys().then(() => {
                if (request.abort) return reject(new DOMException('aborted', 'AbortError'));
                init.method = request.method;
                init.headers = request.headers;
                init.body = request.data;
                realFetch.call(win, request.url, init).then(response => {
                    if (typeof request.response === 'function') {
                        req.response = {
                            finalUrl: response.url,
                            status: response.status,
                            responseHeaders: parseHeaders(response.headers)
                        };
                        hookFetchResponse(response, req);
                        response.clone = () => {
                            const resClone = resProto.clone.call(response);
                            hookFetchResponse(resClone, req);
                            return resClone;
                        };
                    }
                    resolve(response);
                }, reject);
            }).catch(err => {
                console.error(err);
                resolve(realFetch.call(win, url, init));
            });
        });
    }
    win.XMLHttpRequest = fakeXhr;
    Object.keys(realXhr).forEach(key => fakeXhr[key] = realXhr[key]);
    fakeXhr.prototype = realXhr.prototype;
    win.fetch = fakeFetch;
    return {
        hook: fn => hookFns.push(fn),
        filter: arr => {
            filter = Array.isArray(arr) && arr;
        },
        protect: () => {
            readonly(win, 'XMLHttpRequest', fakeXhr);
            readonly(win, 'fetch', fakeFetch);
        },
        unhook: () => {
            writable(win, 'XMLHttpRequest', realXhr);
            writable(win, 'fetch', realFetch);
        }
    };
}();
(function () {
    'use strict';
    let TargetURL = new URL(window.location.href);
    if (TargetURL.pathname == "/") {
        // 当前在主页 挂载Hook
        ajaxHooker.filter([
            { url: 'api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd' },
            { url: 'api.bilibili.com/pugv/app/web/floor/switch' },
            { url: 'api.bilibili.com/pgc/web/variety/feed' },
            { url: 'api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList' },
            { url: 'api.bilibili.com/pgc/web/timeline/v2' },
            { url: 'api.bilibili.com/x/web-interface/dynamic/region' },
            { url: 'manga.bilibili.com/twirp/comic.v1.Comic/GetClassPageSixComics' },
            { url: 'api.bilibili.com/x/web-show/wbi/res/locs' }
        ]);
        ajaxHooker.hook(async request => {
            let HookURL = new URL(request.url);
            //console.log("[BilibiliPromote] 拦截URL:" + HookURL.pathname);
            if (HookURL.pathname == "/x/web-interface/wbi/index/top/feed/rcmd") {
                console.log("[BilibiliPromote] 拦截-视频列表-删除广告");
                request.response = async res => {
                    let data = [];
                    res.json.data.side_bar_column = []; //置空 我忘了是干嘛的
                    for (let k in res.json.data.item) {
                        if (res.json.data.item[k].id != 0) {
                            data.push(res.json.data.item[k]);
                        } else {
                            data.push(data[0]);//复制一份 保持数据对齐
                        }
                    }
                    res.json.data.item = data;
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/pugv/app/web/floor/switch") {
                console.log("[BilibiliPromote] 拦截-推荐列表-删除推课");
                request.response = async res => {
                    res.json.data.season = []; //列表置空
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/pgc/web/variety/feed") {
                console.log("[BilibiliPromote] 拦截-推荐列表-删除杂类推荐");
                request.response = async res => {
                    res.json.data.cursor = "0";
                    res.json.data.list = []; //列表置空
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/xlive/web-interface/v1/webMain/getMoreRecList") {
                console.log("[BilibiliPromote] 拦截-推荐列表-删除直播推荐");
                request.response = async res => {
                    res.json.data.recommend_room_list = []; //列表置空
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/pgc/web/timeline/v2") {
                return; //不破坏看番
                console.log("[BilibiliPromote] 拦截-推荐列表-删除番剧推荐");
                request.response = async res => {
                    res.json.result.latest = []; //列表置空
                    res.json.result.timeline = [];
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/x/web-interface/dynamic/region") {
                console.log("[BilibiliPromote] 拦截-推荐列表-删除电影纪录片推荐");
                request.response = async res => {
                    if (res.json.code != 0 && res.json.code != 200) return;
                    res.json.data.archives = []; //列表置空
                    res.json.data.page.size = 0;
                    //console.log(res.json);
                }

            }
            if (HookURL.pathname == "/twirp/comic.v1.Comic/GetClassPageSixComics") {
                return; //不破坏漫画
                console.log("[BilibiliPromote] 拦截-推荐列表-删除漫画推荐");
                request.response = async res => {
                    if (res.json.code != 0 && res.json.code != 200) return;
                    res.json.data.roll_six_comics = []; //列表置空
                    //console.log(res.json);
                }
            }
            if (HookURL.pathname == "/x/web-show/wbi/res/locs") {
                console.log("[BilibiliPromote] 拦截-推荐列表-删除赛事推荐");
                request.response = async res => {
                    for (let k in res.json.data) {
                        res.json.data[k] = [];// 全部列表置空 不遍历有漏的
                    }
                    //console.log(res.json);
                }
            }
        });
    }

    // 预加载样式
    let AdVideoCss = `
    .ad-floor-cover.b-img {
        display: none !important;
    }
    div.video-page-game-card-small{
        display: none !important;
    }
    a.ad-report.video-card-ad-small {
        display: none !important;
    }`;
    console.log("[BilibiliPromote] 预载-视频页广告-删除广告");

    let StorageBoxCss = `
    .storage-box{
        display: none !important;
    }`;
    console.log("[BilibiliPromote] 预载-主页浮窗-删除无用浮窗");


    let SwiperCss = `
        .recommended-container_floor-aside .container.is-version8>*:nth-of-type(n + 13) {
            margin-top: 40px !important;
        }
        .recommended-container_floor-aside .container>*:nth-of-type(7) {
            margin-top: 40px !important;
        }
        div.recommended-swipe.grid-anchor{
            display: none !important;
        }
    `;
    console.log("[BilibiliPromote] 预载-主页幻灯片-移除幻灯片(仅1080P适配)");
    let VipWarpCss = `
    .vip-wrap{
        display: none !important;
    }`;

    let TittleDisplayCss = `
    ul.left-entry > li:nth-child(n+4):nth-child(-n+7){
        display: none !important;
    }`;
    console.log("[BilibiliPromote] 预载-主页导航栏-删除无用导航");

    let DownloadEntryCss = `
    .download-entry{
        display: none !important;
    }`;
    console.log("[BilibiliPromote] 预载-主页标题栏-删除下载入口");
    //li.right-entry-item.right-entry-item--upload
    let UploadEntryCss = `
    li.right-entry-item.right-entry-item--upload{
        display: none !important;
    }`;
    console.log("[BilibiliPromote] 预载-主页标题栏-删除投稿入口");
    let VideoFllowCss = `
    .bpx-player-top-left-follow
    {
        display: none !important;
    }`
    console.log("[BilibiliPromote] 预载-视频播放器-删除多余关注")

    // 预载入批量处理
    GM_addStyle(AdVideoCss + SwiperCss + StorageBoxCss + VipWarpCss + TittleDisplayCss + DownloadEntryCss + UploadEntryCss + VideoFllowCss);

    // 以下是需要等待 Dom加载完毕
    document.addEventListener("DOMContentLoaded", (event) => {
        console.log("[BilibiliPromote] DOMContentLoaded...");
    });

})();