Bilibili 首页推送过滤

自用脚本,过滤 B 站首页的广告、直播、影视、付费课程等杂项。为避免显示问题,建议配合 bilibili 页面净化大师食用(启用:隐藏全部加载骨架)。

// ==UserScript==
// @name               Bilibili 首页推送过滤
// @name:en            Bilibili Homepage Feed Filter
// @namespace          http://tampermonkey.net/
// @version            2025-05-09
// @description        自用脚本,过滤 B 站首页的广告、直播、影视、付费课程等杂项。为避免显示问题,建议配合 bilibili 页面净化大师食用(启用:隐藏全部加载骨架)。
// @description:en     Personal userscript: cleans up Bilibili feeds in homepage – strips logs, ads, paid courses, films/OGV, live streams, etc.
// @author             vvbbnn00
// @license            MIT
// @match              https://www.bilibili.com/*
// @icon               https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant              none
// ==/UserScript==

(() => {
  "use strict";

  /**********************************************************************
   * 0. 开关(想关掉某项拦截就把 true 改成 false)
   * 0. Switches (set to false to disable a specific interceptor)
   *********************************************************************/
  const CFG = {
    BLOCK_LOG_REPORT: true, // 0-1 关闭埋点/日志上报 | Disable log reporting
    FILTER_FEED_AV_ONLY: true, // 0-2 移除广告和推广视频 | Remove ads and promotional videos
    REMOVE_PUGV_COURSE: true, // 0-3 拦截课程推荐 | Intercept course recommendations
    FILTER_DYNAMIC_FILM_ALL: true, // 0-4 拦截电视剧、电影等推荐 | Intercept TV series and movie recommendations
    CLEAR_LIVE_RECOMMEND: true, // 0-5 拦截直播推荐 | Intercept live stream recommendations
    FILTER_DYNAMIC_VARIETY: true, // 0-6 拦截综艺娱乐推荐 | Intercept variety show recommendations
    FILTER_DYNAMIC_ANIME: true, // 0-7 拦截番剧、国创等推荐 | Intercept anime and original content recommendations
    FILTER_DYNAMIC_MANGA: true, // 0-8 拦截漫画推荐 | Intercept manga recommendations
  };

  /*********************** 1. Interceptor Manager ************************/

  class RequestContext {
    constructor({ type, url, method, request, xhr }) {
      this.type = type; // 'fetch' | 'xhr'
      this.url = url;
      this.method = method || "GET";
      this.request = request; // fetch Request
      this.xhr = xhr; // XMLHttpRequest
    }
  }

  class InterceptorManager {
    #rules = [];
    use(rule) {
      this.#rules.push(rule);
    }
    find(ctx) {
      return this.#rules.find((r) => r.match(ctx));
    }
  }
  const interceptors = new InterceptorManager();

  /************************ 2. Register rules ***************************/

  /**
   * 0-1 关闭埋点 / 日志上报
   *     Disable log reporting
   */
  if (CFG.BLOCK_LOG_REPORT) {
    interceptors.use({
      match: (ctx) => ctx.url.includes("data.bilibili.com"),
      onRequest(ctx) {
        if (ctx.type === "fetch") {
          return new Response("{}", {
            status: 200,
            headers: { "Content-Type": "application/json" },
          });
        }
        if (ctx.type === "xhr") {
          ctx.xhr.__interceptorFinalText = "{}";
          return true; // 阻断 XHR
        }
      },
    });
  }

  /**
   * 0-2 移除广告和推广视频
   *     Remove ads and promotional videos
   */
  if (CFG.FILTER_FEED_AV_ONLY) {
    (function () {
      const FEED_API =
        "api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd";
      const filterFn = (list) => list.filter((v) => v.goto === "av");
      interceptors.use({
        match: (ctx) => ctx.url.includes(FEED_API),
        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          if (data?.data?.item) data.data.item = filterFn(data.data.item);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },
        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            if (data?.data?.item) data.data.item = filterFn(data.data.item);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-3 拦截课程推荐
   *     Intercept course recommendations
   */
  if (CFG.REMOVE_PUGV_COURSE) {
    (function () {
      const API = "api.bilibili.com/pugv/app/web/floor/switch";
      const wipe = (d) => {
        if (d?.data) {
          d.data.ranks = [];
          d.data.season = [];
        }
      };
      interceptors.use({
        match: (ctx) => ctx.url.includes(API),
        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },
        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-4 拦截电视剧、电影等推荐
   *     Intercept TV series and movie recommendations
   */
  if (CFG.FILTER_DYNAMIC_FILM_ALL) {
    (function () {
      const API = "api.bilibili.com/x/web-interface/dynamic/region";
      const wipe = (d) => {
        if (d?.data) d.data.archives = [];
      };

      interceptors.use({
        match: (ctx) => ctx.url.includes(API),

        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },

        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-5 拦截直播推荐
   *     Intercept live stream recommendations
   */
  if (CFG.CLEAR_LIVE_RECOMMEND) {
    (function () {
      const API =
        "api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList";
      const wipe = (d) => {
        if (d?.data) {
          d.data.recommend_room_list = [];
          d.data.top_room_id = 0;
        }
      };
      interceptors.use({
        match: (ctx) => ctx.url.includes(API),
        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },
        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-6 拦截综艺娱乐推荐
   *     Intercept variety show recommendations
   */
  if (CFG.FILTER_DYNAMIC_VARIETY) {
    (function () {
      const API = "api.bilibili.com/pgc/web/variety/feed";
      const wipe = (d) => {
        if (d?.data) {
          d.data.cursor = "";
          d.data.list = [];
        }
      };

      interceptors.use({
        match: (ctx) => ctx.url.includes(API),

        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },

        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-7 拦截番剧、国创等推荐
   *     Intercept anime and original content recommendations
   */
  if (CFG.FILTER_DYNAMIC_ANIME) {
    (function () {
      const API = "api.bilibili.com/pgc/web/timeline/v2";
      const wipe = (d) => {
        if (d?.result) {
          d.result.latest = [];
          d.result.timeline = [];
        }
      };

      interceptors.use({
        match: (ctx) => ctx.url.includes(API),

        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },

        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /**
   * 0-8 拦截漫画推荐
   *     Intercept manga recommendations
   */
  if (CFG.FILTER_DYNAMIC_ANIME) {
    (function () {
      const API = "manga.bilibili.com/twirp/comic.v1.MainStation/Feed";
      const wipe = (d) => {
        if (d?.data) {
          d.data.list = [];
          d.data.total = 0;
        }
      };

      interceptors.use({
        match: (ctx) => ctx.url.includes(API),

        async onFetchResponse(ctx, res) {
          const data = await res.clone().json();
          wipe(data);
          return new Response(JSON.stringify(data), {
            status: res.status,
            statusText: res.statusText,
            headers: res.headers,
          });
        },

        onXHRResponse(ctx, txt) {
          try {
            const data = JSON.parse(txt);
            wipe(data);
            return JSON.stringify(data);
          } catch {
            return txt;
          }
        },
      });
    })();
  }

  /*********************** 3. Fetch hook ************************/

  const nativeFetch = window.fetch;
  window.fetch = async function (input, init) {
    const url = typeof input === "string" ? input : input.url;
    const ctx = new RequestContext({
      type: "fetch",
      url,
      method: init?.method || input?.method,
      request: input,
    });
    const rule = interceptors.find(ctx);
    if (rule?.onRequest) {
      const res = rule.onRequest(ctx);
      if (res) return res;
    }
    const response = await nativeFetch.call(this, input, init);
    if (rule?.onFetchResponse) {
      try {
        return await rule.onFetchResponse(ctx, response);
      } catch (e) {
        console.error("[Interceptor] fetch", e);
      }
    }
    return response;
  };

  /*********************** 4. XHR hook **************************/

  const nativeOpen = XMLHttpRequest.prototype.open;
  const nativeSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this.__interceptorCtx = new RequestContext({
      type: "xhr",
      url,
      method,
      xhr: this,
    });
    nativeOpen.call(this, method, url, ...rest);
  };

  XMLHttpRequest.prototype.send = function (body) {
    const ctx = this.__interceptorCtx;
    const rule = ctx && interceptors.find(ctx);

    if (rule?.onRequest && rule.onRequest(ctx)) {
      finishXHR(this, ctx, ctx.xhr.__interceptorFinalText ?? "");
      return;
    }

    if (rule?.onXHRResponse) {
      this.addEventListener("readystatechange", function () {
        if (this.readyState === 4) {
          try {
            const newText = rule.onXHRResponse(ctx, this.responseText);
            overwriteResponse(this, newText);
          } catch (e) {
            console.error("[Interceptor] xhr", e);
          }
        }
      });
    }
    nativeSend.call(this, body);
  };

  /************************ helpers ****************************/

  function overwriteResponse(xhr, text) {
    const desc = { configurable: true, get: () => text };
    try {
      Object.defineProperty(xhr, "responseText", desc);
      Object.defineProperty(xhr, "response", desc);
    } catch {
      /* read-only browsers */
    }
  }

  function finishXHR(xhr, ctx, text) {
    overwriteResponse(xhr, text);
    xhr.readyState = 4;
    xhr.status = 200;
    xhr.statusText = "OK";
    xhr.dispatchEvent(new Event("readystatechange"));
    xhr.dispatchEvent(new Event("load"));
    xhr.dispatchEvent(new Event("loadend"));
  }
})();