Greasy Fork 还支持 简体中文。

BilibiliPromote

增强B站脚本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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...");
    });

})();