Bilibili UP屏蔽插件 - 屏蔽UP主视频卡片,支持精确匹配和正则匹配,支持视频页面、分类页面、搜索页面等。
// ==UserScript==
// @name Bilibili-BlackList
// @namespace https://github.com/HeavenTTT/bilibili-blacklist
// @version 1.2.4
// @author HeavenTTT
// @description Bilibili UP屏蔽插件 - 屏蔽UP主视频卡片,支持精确匹配和正则匹配,支持视频页面、分类页面、搜索页面等。
// @match *://*.bilibili.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @icon https://www.bilibili.com/favicon.ico
// @license MIT
// ==/UserScript==
(function () {
"use strict";
/*
* Bilibili-BlackList -- Bilibili UP屏蔽插件
* 脚本大部分代码由AI生成,作者一点都不懂JavaScript,出现bug请联系Gemini / ChatGPT / DeepSeek
* this script is mainly generated by AI, the author doesn't know JavaScript at all, if there are bugs, please contact Gemini / ChatGPT / DeepSeek
* 感谢你的使用
* Thank you for using this script
*
* 本段注释为VS code 自动生成 this is a comment generated by VS code
*/
// 从存储中获取黑名单
// 默认精确匹配黑名单(区分大小写)
let exactMatchBlacklist = GM_getValue("exactBlacklist", [
"绝区零",
"崩坏星穹铁道",
"崩坏3",
"原神",
"米哈游miHoYo",
]);
// 默认正则匹配黑名单(不区分大小写)
let regexMatchBlacklist = GM_getValue("regexBlacklist", [
"王者荣耀",
"和平精英",
"PUBG",
"绝地求生",
"吃鸡",
]);
// 默认标签名黑名单
let tagNameBlacklist = GM_getValue("tNameBlacklist", []);
// 从存储中获取全局配置,并为旧版本配置补充新增字段
const defaultGlobalPluginConfig = {
flagInfo: true, // 启用/禁用按UP主名/标题屏蔽
flagAD: true, // 启用/禁用屏蔽一般广告
flagTName: true, // 启用/禁用按标签名屏蔽(需要API调用)
flagCM: true, // 启用/禁用屏蔽cm.bilibili.com软广
flagKirby: true, // 启用/禁用被屏蔽视频的卡比覆盖模式
flagHoverReveal: false, // 启用/禁用悬停后临时显示被遮挡视频
hoverRevealDelaySeconds: 1, // 悬停显示延迟(秒)
processQueueInterval: 200, // 处理队列中单个卡片的延迟时间(毫秒)
blockScanInterval: 200, // BlockCard扫描新卡片的间隔时间(毫秒)
flagHideOnLoad: true, // 启用/禁用页面加载时自动隐藏
flagVertical: true, // 启用/禁用屏蔽竖屏视频
verticalScaleThreshold: 0.7, // 竖屏视频的宽高比阈值(0-1)
// 自动连播遇到被屏蔽视频时的处理方式(三态):
// "skip" = 切换到未屏蔽视频;"stop" = 停止播放;"off" = 不处理(B站默认行为,继续播放被屏蔽视频)
flagSkipBlockedAutoplay: "off",
};
let globalPluginConfig = {
...defaultGlobalPluginConfig,
...(GM_getValue("globalConfig", {}) || {}),
};
// 防止旧配置或手动修改写入超出允许范围的悬停延迟
const storedHoverRevealDelay = Number(
globalPluginConfig.hoverRevealDelaySeconds
);
globalPluginConfig.hoverRevealDelaySeconds = Number.isFinite(
storedHoverRevealDelay
)
? Math.min(5, Math.max(0.1, storedHoverRevealDelay))
: defaultGlobalPluginConfig.hoverRevealDelaySeconds;
// 校验/修复自动连播处理方式,只允许 "skip" / "stop" / "off"
const AUTOPLAY_SKIP_MODES = ["skip", "stop", "off"];
if (!AUTOPLAY_SKIP_MODES.includes(globalPluginConfig.flagSkipBlockedAutoplay)) {
globalPluginConfig.flagSkipBlockedAutoplay =
defaultGlobalPluginConfig.flagSkipBlockedAutoplay;
}
// 将黑名单保存到存储中
function saveBlacklistsToStorage() {
GM_setValue("exactBlacklist", exactMatchBlacklist);
GM_setValue("regexBlacklist", regexMatchBlacklist);
GM_setValue("tNameBlacklist", tagNameBlacklist);
}
// 将全局配置保存到存储中
function saveGlobalConfigToStorage() {
GM_setValue("globalConfig", globalPluginConfig);
}
// 标签名列表:存储ID到名称的映射
let tagNameList = GM_getValue("tagNameList", []); // 默认为空数组,每个条目为 { id, name , name_v2}
let tagListLastTime = GM_getValue("tLastTime", 0);
// 将标签名列表保存到存储中
function saveTagNameListToStorage() {
GM_setValue("tagNameList", tagNameList);
GM_setValue("tLastTime", Date.now());
}
// 根据ID查找标签名
function getTagNameById(id) {
if (id === null || id === undefined) return null;
// 支持字符串或数字ID
const entry = tagNameList.find(entry => entry.id == id); // 使用宽松相等以匹配类型
return entry ? { name: entry.name, name_v2: entry.name_v2 } : null;
}
// 根据name_v2查找标签名
function getTagNameByV2(name_v2) {
if (name_v2 === null || name_v2 === undefined) return null;
// 支持字符串或数字ID
const entry = tagNameList.find(entry => entry.name_v2 == name_v2); // 使用宽松相等以匹配类型
return entry ? entry.name: null;
}
// UI元素(稍后初始化)
let tempUnblockButton;
let managerPanel;
let exactMatchListElement;
let regexMatchListElement;
let tagNameListElement;
let configListElement;
let blockCountTitleElement;
let blockCountDisplayElement = null;
// 内部状态变量
let isShowAllVideos = false; // 是否显示全部视频卡片
let isBlockingOperationInProgress = false; // 是否正在执行BlockCard扫描操作
let lastBlockScanExecutionTime = 0; // 上次执行BlockCard扫描的时间戳
let blockedVideoCards = new Set(); // 存储已屏蔽的视频卡片元素
let processedVideoCards = new WeakSet(); // 记录已处理过的卡片(避免重复处理,包括 UP主/标题检查和 tname 获取)
let videoCardProcessQueue = new Set(); // 存储待处理的卡片,用于统一的队列处理
let isVideoCardQueueProcessing = false; // 是否正在处理队列
let isPageCurrentlyActive = true; // 页面是否可见
let countBlockInfo = 0; // 已屏蔽视频计数
let countBlockAD = 0; // 已屏蔽广告计数
let countBlockTName = 0; // 已屏蔽标签名计数
let countBlockCM = 0; // 已屏蔽cm.bilibili.com软广计数
// 用于不同页面UP主名称选择器
const UP_NAME_SELECTORS = [
".bili-video-card__info--author", // 主页
".bili-video-card__author", // 分类页面 -> span title
".name", // 视频播放页
".upname a span", // 视频播放页“接下来播放/相关推荐”卡片(新版结构)
".upname a",
".upname",
];
// 用于不同页面视频标题选择器
const VIDEO_TITLE_SELECTORS = [
".bili-video-card__info--tit", // 主页
".bili-video-card__title", // 分类页面 -> span title
".title", // 视频播放页
];
// 屏蔽类型对应的原因文案
const BLOCK_REASON_MAP = {
info: "标题/UP主名",
ad: "广告",
tname: "分类标签",
cm: "软广",
vertical: "竖屏视频",
};
/**
* 获取视频卡片上容器应挂载的宿主元素,并确保宿主可被绝对定位。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLElement} 容器宿主元素。
*/
function getBlockContainerHost(cardElement) {
// 视频播放页面的视频卡片结构特殊,需要调整位置
if (isCurrentPageVideo()) {
const cardBox = cardElement.querySelector(".card-box");
if (cardBox) {
cardBox.style.position = "relative";
cardBox.classList.add("bilibili-blacklist-block-container-host");
return cardBox;
}
} else if (isCurrentPageCategory()) {
// 分类页面的视频卡片结构特殊,需要调整位置
const biliVideoCard = cardElement.querySelector(".bili-video-card");
if (biliVideoCard) {
biliVideoCard.classList.add("bilibili-blacklist-block-container-host");
return biliVideoCard;
}
}
// 默认宿主:确保可被绝对定位的子元素正常显示
const hostStyle = getComputedStyle(cardElement);
if (hostStyle.position === "static" || !hostStyle.position) {
cardElement.style.position = "relative";
}
cardElement.classList.add("bilibili-blacklist-block-container-host");
return cardElement;
}
/**
* 确保视频卡片上存在屏蔽容器,不存在则创建(用于广告等未走扫描流程的卡片)。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLElement} 已存在的或新创建的容器元素。
*/
function ensureBlockContainerOnCard(cardElement) {
const existing = cardElement.querySelector(
".bilibili-blacklist-block-container"
);
if (existing) return existing;
const container = document.createElement("div");
container.classList.add("bilibili-blacklist-block-container");
const host = getBlockContainerHost(cardElement);
host.appendChild(container);
return container;
}
/**
* 为视频卡片添加屏蔽按钮容器。
* @param {string} upName - UP主名称。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLElement} 创建的容器元素。
*/
function addBlockContainerToCard(upName, cardElement) {
const container = ensureBlockContainerOnCard(cardElement);
if (!container.querySelector(".bilibili-blacklist-block-btn")) {
const blockButton = createBlockUpButton(upName, cardElement);
container.appendChild(blockButton);
}
return container;
}
/**
* 隐藏给定的视频卡片。
* @param {HTMLElement} cardElement - 要隐藏的视频卡片元素。
* @param {string} type - 隐藏类型,默认为"none"。
* @returns {void}
*
*/
function hideVideoCard(cardElement, type = "none") {
const realCardToBlock = getRealVideoCardElement(cardElement);
if (!realCardToBlock) {
console.warn(
"[bililili-blacklist] hideVideoCard: realCardToBlock is null"
);
return;
}
if (blockedVideoCards.has(realCardToBlock)) {
return;
}
blockedVideoCards.add(realCardToBlock);
if (type === "info") {
countBlockInfo++;
}
if (type === "ad") {
countBlockAD++;
}
if (type === "tname") {
countBlockTName++;
}
if (type === "cm") {
countBlockCM++;
}
if (type === "vertical") {
countBlockTName++;
}
if (globalPluginConfig.flagKirby) {
addKirbyOverlayToCard(cardElement);
} else {
realCardToBlock.style.display = "none";
}
setBlockReasonOnCard(cardElement, type);
}
/**
* 在卡片的屏蔽按钮容器中设置屏蔽原因。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @param {string} type - 屏蔽类型。
*/
function setBlockReasonOnCard(cardElement, type) {
const reasonText = BLOCK_REASON_MAP[type];
if (!reasonText) return;
// 广告等卡片未经过 scanAndBlockVideoCards 流程,可能不存在容器,需确保创建
const container = ensureBlockContainerOnCard(cardElement);
let reasonElement = container.querySelector(
".bilibili-blacklist-block-reason"
);
if (!reasonElement) {
reasonElement = document.createElement("span");
reasonElement.className = "bilibili-blacklist-block-reason";
// 位于"屏蔽"按钮之后、标签组之前
const tnameGroup = container.querySelector(
".bilibili-blacklist-tname-group"
);
if (tnameGroup) {
container.insertBefore(reasonElement, tnameGroup);
} else {
container.appendChild(reasonElement);
}
}
reasonElement.textContent = `屏蔽原因: ${reasonText}`;
}
/**
* 移除卡片上的屏蔽原因显示。
* @param {HTMLElement} cardElement - 视频卡片元素。
*/
function removeBlockReason(cardElement) {
const container = cardElement.querySelector(
".bilibili-blacklist-block-container"
);
if (!container) return;
const reasonElement = container.querySelector(
".bilibili-blacklist-block-reason"
);
if (reasonElement) {
reasonElement.remove();
}
}
/**
* 获取应该被屏蔽的卡片的真正父元素。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLElement} 应用显示更改的实际元素。
*/
function getRealVideoCardElement(cardElement) {
// 搜索页面的视频卡片父元素是上一级
if (isCurrentPageSearch()) {
return cardElement.parentElement;
}
// 主页视频卡片可能有多层父元素
if (isCurrentPageMain()) {
if (cardElement.parentElement.classList.contains("bili-feed-card")) {
cardElement = cardElement.parentElement;
if (cardElement.parentElement.classList.contains("feed-card")) {
cardElement = cardElement.parentElement;
}
}
}
return cardElement;
}
/**
* 根据当前页面选择所有视频卡片。
* @returns {NodeListOf<HTMLElement> | null} 视频卡片元素的NodeList,如果不是识别的页面则返回null。
*/
function queryAllVideoCards() {
if (isCurrentPageMain()) {
return document.querySelectorAll(".bili-video-card");
} else if (isCurrentPageVideo()) {
return document.querySelectorAll(".video-page-card-small");
} else if (isCurrentPageCategory()) {
return document.querySelectorAll(".feed-card");
} else if (isCurrentPageSearch()) {
return document.querySelectorAll(".bili-video-card");
}
return null;
}
/**
* 扫描并处理视频卡片进行屏蔽。
*/
function scanAndBlockVideoCards() {
const now = Date.now();
// 限制扫描频率,防止性能问题
if (
isBlockingOperationInProgress ||
now - lastBlockScanExecutionTime < globalPluginConfig.blockScanInterval
) {
return;
}
isBlockingOperationInProgress = true;
lastBlockScanExecutionTime = now;
try {
const videoCards = queryAllVideoCards();
if (!videoCards) return;
videoCards.forEach((card) => {
// 如果卡片已经处理过,则跳过
if (processedVideoCards.has(card)) {
return;
}
const { upName, videoTitle } = getVideoCardInfo(card);
// 如果获取到UP主名称和视频标题,则添加屏蔽按钮
if (upName && videoTitle) {
addBlockContainerToCard(upName, card);
// --- 根据 flagHideOnLoad 开关决定是否立即隐藏卡片 ---
const realCard = getRealVideoCardElement(card);
if (globalPluginConfig.flagHideOnLoad && !isShowAllVideos) {
// 只有在"显示全部"模式关闭时才执行
if (globalPluginConfig.flagKirby) {
addKirbyOverlayToCard(card); // 卡比模式下添加遮罩
realCard.style.display = "block"; // 确保卡片本身是显示的
} else {
realCard.style.display = "none"; // 非卡比模式下直接隐藏
}
}
}
// --- 立即隐藏卡片的逻辑结束 ---
// 将卡片添加到处理队列
videoCardProcessQueue.add(card);
});
// 如果队列中有待处理的卡片且当前未在处理中,则开始处理队列
if (videoCardProcessQueue.size > 0 && !isVideoCardQueueProcessing) {
processVideoCardQueue();
}
// 刷新屏蔽计数显示
refreshBlockCountDisplay();
// 修正主页布局
fixMainPageLayout();
} finally {
isBlockingOperationInProgress = false;
}
}
/**
* 修正主页在屏蔽后的布局。
*/
function fixMainPageLayout() {
if (!isCurrentPageMain()) return;
const container = document.querySelector(
".recommended-container_floor-aside .container"
);
if (container) {
const children = container.children;
let visibleIndex = 0;
// 调整可见卡片的边距,使布局更紧凑
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.style.display !== "none") {
if (visibleIndex <= 6) {
child.style.marginTop = "0px";
} else if (visibleIndex < 12) {
child.style.marginTop = "24px";
} else {
break;
}
visibleIndex++;
}
}
}
}
/**
* 切换所有被屏蔽视频卡片的显示。
*/
function toggleShowAllBlockedVideos() {
isShowAllVideos = !isShowAllVideos;
blockedVideoCards.forEach((card) => {
if (globalPluginConfig.flagKirby) {
const kirbyOverlay = card.querySelector("#bilibili-blacklist-kirby");
if (kirbyOverlay) {
if (isShowAllVideos) {
kirbyOverlay.style.display = "none";
} else {
fadeInKirbyOverlay(kirbyOverlay);
}
}
card.style.display = "block";
} else {
card.style.display = isShowAllVideos ? "block" : "none";
}
});
tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽";
tempUnblockButton.style.background = isShowAllVideos
? "#dddddd"
: "#fb7299";
}
/**
* 从视频卡片中检索UP主名称和视频标题。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {{upName: string, videoTitle: string}} 包含UP主名称和视频标题的对象。
*/
function getVideoCardInfo(cardElement) {
let upName = "";
let videoTitle = "";
const upNameElements = cardElement.querySelectorAll(
UP_NAME_SELECTORS.join(", ")
);
if (upNameElements.length > 0) {
upName = upNameElements[0].textContent.trim();
if (isCurrentPageCategory()) {
// 分类页面的UP主名称可能包含其他信息,需要进一步处理
upName = upName.split(" · ")[0].trim();
}
}
const titleElements = cardElement.querySelectorAll(
VIDEO_TITLE_SELECTORS.join(", ")
);
if (titleElements.length > 0) {
videoTitle = titleElements[0].textContent.trim();
}
return { upName, videoTitle };
}
/**
* 检查UP主名称或标题是否在黑名单中。
* @param {string} upName - 要检查的UP主名称。
* @param {string} title - 要检查的视频标题。
* @returns {boolean} 如果在黑名单中则返回true,否则返回false。
*/
function isBlacklisted(upName, title) {
const lowerCaseUpName = upName.toLowerCase();
// 检查精确匹配黑名单
if (
exactMatchBlacklist.some((item) => item.toLowerCase() === lowerCaseUpName)
) {
return true;
}
// 检查正则匹配黑名单
const regexList = regexMatchBlacklist.map(
(regex) => new RegExp(regex, "i")
);
if (regexList.some((regex) => regex.test(upName))) {
return true;
}
if (regexList.some((regex) => regex.test(title))) {
return true;
}
return false;
}
/**
* 将UP主名称添加到精确匹配黑名单并刷新。
* @param {string} upName - 要添加的UP主名称。
* @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。
*/
function addToExactBlacklist(upName, cardElement = null) {
try {
if (!upName) return;
if (!exactMatchBlacklist.includes(upName)) {
exactMatchBlacklist.push(upName);
saveBlacklistsToStorage();
refreshAllPanelTabs();
if (cardElement) {
hideVideoCard(cardElement, "info");
}
hideAllCardsByUpName(upName);
}
} catch (e) {
console.error("[bilibili-blacklist] 添加黑名单出错:", e);
}
}
/**
* 从精确匹配黑名单中移除UP主名称。
* @param {string} upName - 要移除的UP主名称。
*/
function removeFromExactBlacklist(upName) {
try {
if (exactMatchBlacklist.includes(upName)) {
const index = exactMatchBlacklist.indexOf(upName);
exactMatchBlacklist.splice(index, 1);
saveBlacklistsToStorage();
refreshExactMatchList();
}
} catch (e) {
console.error("[bilibili-blacklist] 移除黑名单出错:", e);
}
}
/**
* 将标签名添加到黑名单并刷新。
* @param {string} tagName - 要添加的标签名。
* @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。
*/
function addToTagNameBlacklist(tagName, cardElement = null) {
try {
if (!tagName) {
return;
}
if (!tagNameBlacklist.includes(tagName)) {
tagNameBlacklist.push(tagName);
saveBlacklistsToStorage();
refreshAllPanelTabs();
if (cardElement) {
hideVideoCard(cardElement, "tname");
}
hideAllCardsByTagName(tagName);
}
} catch (e) {
console.error("[bilibili-blacklist] 添加标签黑名单出错:", e);
}
}
/**
* 从黑名单中移除标签名。
* @param {string} tagName - 要移除的标签名。
*/
function removeFromTagNameBlacklist(tagName) {
try {
if (tagNameBlacklist.includes(tagName)) {
const index = tagNameBlacklist.indexOf(tagName);
tagNameBlacklist.splice(index, 1);
saveBlacklistsToStorage();
refreshTagNameList();
}
} catch (e) {
console.error("[bilibili-blacklist] 移除标签黑名单出错:", e);
}
}
/**
* 隐藏所有匹配指定UP主名称的视频卡片。
* @param {string} upName - 要匹配的UP主名称。
*/
function hideAllCardsByUpName(upName) {
const videoCards = queryAllVideoCards();
if (!videoCards) return;
videoCards.forEach(card => {
const { upName: cardUpName, videoTitle } = getVideoCardInfo(card);
if (cardUpName && isBlacklisted(cardUpName, videoTitle)) {
hideVideoCard(card, "info");
}
});
}
/**
* 隐藏所有匹配指定标签名的视频卡片。
* @param {string} tagName - 要匹配的标签名。
*/
function hideAllCardsByTagName(tagName) {
const videoCards = queryAllVideoCards();
if (!videoCards) return;
videoCards.forEach(card => {
if (isCardBlacklistedByTagName(card)) {
hideVideoCard(card, "tname");
}
});
}
/**
* 获取视频卡片的链接。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {string|null} 视频链接,如果未找到则返回null。
*/
function getCardHrefLink(cardElement) {
const hrefLink = cardElement.querySelector("a");
if (hrefLink) {
return hrefLink.getAttribute("href");
}
return null;
}
function checkLinkCM(link) {
if (!link) return false;
// 如果是cm.bilibili.com的链接,且启用了CM广告屏蔽,则隐藏卡片
if (link.match(/cm.bilibili.com/) && globalPluginConfig.flagCM) {
return true;
}
return false;
}
/**
* 从视频链接中提取BV ID。
* @param {string} link - 视频链接。
* @returns {string|null} BV ID,如果未找到则返回null。
*/
function getLinkBvId(link) {
try {
if (!link) {
return null;
} else {
const bv = link.match(/BV\w+/);
return bv ? bv[0] : null;
}
} catch (e) {
return null;
}
}
/**
* 使用BV ID从Bilibili API获取视频信息。
* @param {string} bvid - 视频的BV ID。
* @returns {Promise<object|null>} 解析为视频数据或null的Promise。
*/
async function getBilibiliVideoApiData(bvid) {
if (!bvid || bvid.length >= 24) {
return null;
}
const url = `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`;
try {
const response = await fetch(url);
const json = await response.json();
if (json.code === 0) {
return json.data;
} else {
return null;
}
} catch (error) {
console.error("[bilibili-blacklist] API 请求失败:", error);
}
}
/**
* 检查卡片是否包含任何黑名单标签。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {boolean} 如果有任何标签被列入黑名单,则返回true,否则返回false。
*/
function isCardBlacklistedByTagName(cardElement) {
const tnameGroup = cardElement.querySelector(
".bilibili-blacklist-tname-group"
);
if (tnameGroup) {
const tnameElements = tnameGroup.querySelectorAll(
".bilibili-blacklist-tname"
);
for (const tnameElement of tnameElements) {
const tname = tnameElement.textContent.trim();
if (tagNameBlacklist.includes(tname)) {
return true;
}
// 临时更新,根据V2查找名称
const name = getTagNameByV2(tname);
if (name === null) continue;
if (tagNameBlacklist.includes(name)) {
return true;
}
}
}
return false;
}
/**
* 处理视频卡片队列进行屏蔽。
*/
async function processVideoCardQueue() {
if (isVideoCardQueueProcessing) return;
isVideoCardQueueProcessing = true;
while (videoCardProcessQueue.size > 0) {
// 如果页面不可见,则暂停处理
if (!isPageCurrentlyActive) {
await sleep(1000);
continue;
}
const iterator = videoCardProcessQueue.values();
const card = iterator.next().value;
videoCardProcessQueue.delete(card);
if (!card || processedVideoCards.has(card)) {
continue;
}
let shouldHide = false;
let blockType = "none";
// 如果启用了标签屏蔽且当前卡片未被隐藏
const link = getCardHrefLink(card);
if (checkLinkCM(link)) {
shouldHide = true;
blockType = "cm";
}
const { upName, videoTitle } = getVideoCardInfo(card);
if (upName && videoTitle && !shouldHide) {
// 如果UP主名称或标题在黑名单中,且启用了信息屏蔽
if (isBlacklisted(upName, videoTitle) && globalPluginConfig.flagInfo) {
shouldHide = true;
blockType = "info";
}
} else {
// 如果无法获取UP主名称和标题,但卡片已被隐藏或有Kirby覆盖,则也认为应该隐藏
if (
getRealVideoCardElement(card).style.display === "none" &&
!globalPluginConfig.flagKirby
) {
shouldHide = true;
} else if (
getRealVideoCardElement(card).querySelector(
"#bilibili-blacklist-kirby"
)
) {
shouldHide = true;
}
}
if (
(globalPluginConfig.flagTName || globalPluginConfig.flagVertical) &&
!shouldHide
) {
const bvId = getLinkBvId(link);
// 如果存在BV ID且卡片尚未添加标签组
if (bvId && !card.querySelector(".bilibili-blacklist-tname-group")) {
const data = await getBilibiliVideoApiData(bvId);
if (data) {
const container = card.querySelector(
".bilibili-blacklist-block-container"
);
if (container) {
const tnameGroup = document.createElement("div");
tnameGroup.className = "bilibili-blacklist-tname-group";
let hasTname = false;
if (data.tname) {
const btn = createTNameBlockButton(data.tname, card);
tnameGroup.appendChild(btn);
hasTname = true;
}
if (data.tname_v2) {
const tnameElement = createTNameBlockButton(
data.tname_v2,
card
);
tnameGroup.appendChild(tnameElement);
hasTname = true;
}
//#region 临时修复,仅ID
if (data.tid_v2) {
const obj = getTagNameById(data.tid_v2);
if (obj) {
const tnameElement = createTNameBlockButton(
obj.name,
card
);
tnameGroup.appendChild(tnameElement);
const tnameElement_v2 = createTNameBlockButton(
obj.name_v2,
card
);
tnameGroup.appendChild(tnameElement_v2);
hasTname = true;
}
}
//#endregion
if (hasTname) {
container.appendChild(tnameGroup);
}
}
if (isCardBlacklistedByTagName(card)) {
shouldHide = true;
blockType = "tname";
}
// 如果启用了垂直视频屏蔽
if (
data.dimension.width &&
data.dimension.height &&
!shouldHide &&
globalPluginConfig.flagVertical
) {
const dimension = data.dimension.width / data.dimension.height;
if (dimension < globalPluginConfig.verticalScaleThreshold) {
shouldHide = true;
blockType = "vertical";
}
}
}
}
}
if (shouldHide) {
hideVideoCard(card, blockType);
} else {
const realCardToDisplay = getRealVideoCardElement(card);
if (blockedVideoCards.has(realCardToDisplay)) {
blockedVideoCards.delete(realCardToDisplay);
}
removeBlockReason(card);
if (globalPluginConfig.flagKirby) {
removeKirbyOverlay(card);
}
realCardToDisplay.style.display = "block";
}
processedVideoCards.add(card); // 标记卡片已处理
await sleep(globalPluginConfig.processQueueInterval);
}
isVideoCardQueueProcessing = false;
refreshBlockCountDisplay();
}
// 异步等待函数
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const KIRBY_FADE_DURATION_MS = 800;
const hoverRevealBoundCards = new WeakSet();
const hoverRevealTimers = new WeakMap();
const kirbyFadeTimers = new WeakMap();
/**
* 为UP主创建屏蔽按钮,显示在视频卡片上。
* @param {string} upName - UP主名称。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLDivElement} 创建的按钮元素。
*/
function createBlockUpButton(upName, cardElement) {
const button = document.createElement("div");
button.className = "bilibili-blacklist-block-btn";
button.innerHTML = "屏蔽";
button.title = `屏蔽: ${upName}`;
button.addEventListener("click", (e) => {
e.stopPropagation(); // 阻止事件冒泡,防止触发视频点击事件
addToExactBlacklist(upName, cardElement);
});
return button;
}
/**
* 为标签名创建屏蔽按钮,显示在视频卡片上。
* @param {string} tagName - 标签名。
* @param {HTMLElement} cardElement - 视频卡片元素。
* @returns {HTMLSpanElement} 创建的按钮元素。
*/
function createTNameBlockButton(tagName, cardElement) {
const button = document.createElement("span");
button.className = "bilibili-blacklist-tname";
button.innerHTML = `${tagName}`;
button.title = `屏蔽: ${tagName}`;
button.addEventListener("click", (e) => {
e.stopPropagation(); // 阻止事件冒泡
addToTagNameBlacklist(tagName, cardElement);
});
return button;
}
/**
* 将黑名单管理器按钮添加到右侧导航条。
*/
function addBlacklistManagerButton() {
const rightEntry = document.querySelector(".right-entry");
if (!rightEntry) {
console.warn("[bilibili-blacklist] 未找到右侧导航栏");
return;
}
// 顶栏由Vue延迟渲染,等li数量超过6个(顶栏基本渲染完成)后再插入按钮,避免被重渲染顶掉
if (rightEntry.querySelectorAll("li").length <= 6) {
return;
}
if (!rightEntry.querySelector("#bilibili-blacklist-manager-button")) {
const listItem = document.createElement("li");
listItem.id = "bilibili-blacklist-manager-button";
listItem.className = "v-popover-wrap";
const button = document.createElement("div");
button.className = "right-entry-item";
const icon = document.createElement("div");
icon.className = "right-entry__outside";
icon.innerHTML = getKirbySVG(); // 获取卡比SVG图标
blockCountDisplayElement = document.createElement("span");
blockCountDisplayElement.textContent = `0`;
button.appendChild(icon);
button.appendChild(blockCountDisplayElement);
listItem.appendChild(button);
// 将按钮插入到导航栏的特定位置
if (rightEntry.children.length > 1) {
rightEntry.insertBefore(listItem, rightEntry.children[1]);
} else {
rightEntry.appendChild(listItem);
}
// 点击按钮显示/隐藏管理面板
listItem.addEventListener("click", () => {
managerPanel.style.display =
managerPanel.style.display === "flex" ? "none" : "flex";
});
}
}
/**
* 更新已屏蔽视频的显示计数。
*/
function refreshBlockCountDisplay() {
if (blockCountDisplayElement) {
blockCountDisplayElement.textContent = `${blockedVideoCards.size}`;
}
if (blockCountTitleElement) {
blockCountTitleElement.textContent = `已屏蔽视频 (${blockedVideoCards.size} = ${countBlockInfo} + ${countBlockAD} + ${countBlockCM} + ${countBlockTName})`;
}
}
// 辅助函数:创建通用按钮
function createPanelButton(text, bgColor, onClick) {
const button = document.createElement("button");
button.className = "bilibili-blacklist-panel-btn";
button.textContent = text;
button.style.background = bgColor;
button.addEventListener("click", onClick);
return button;
}
// 辅助函数:为黑名单面板创建列表项
function createBlacklistListItem(contentText, onRemoveClick) {
const item = document.createElement("li");
item.className = "bilibili-blacklist-list-item";
const content = document.createElement("span");
content.textContent = contentText;
const removeBtn = createPanelButton("移除", "#f56c6c", onRemoveClick);
item.appendChild(content);
item.appendChild(removeBtn);
return item;
}
/**
* 刷新面板中的精确匹配黑名单显示。
*/
function refreshExactMatchList() {
if (!exactMatchListElement) {
if (!isBlacklistPanelCreated()) {
return;
}
exactMatchListElement = document.querySelector(
"#bilibili-blacklist-exact-list"
);
if (!exactMatchListElement) {
console.warn("[Bilibili-Blacklist] exactMatchListElement 未定义");
return;
}
}
exactMatchListElement.innerHTML = "";
exactMatchBlacklist.forEach((upName) => {
const item = createBlacklistListItem(upName, () => {
removeFromExactBlacklist(upName);
});
exactMatchListElement.appendChild(item);
});
// 反转列表顺序,使最新添加的显示在顶部
Array.from(exactMatchListElement.children)
.reverse()
.forEach((item) => exactMatchListElement.appendChild(item));
if (exactMatchBlacklist.length === 0) {
const empty = document.createElement("div");
empty.className = "bilibili-blacklist-empty";
empty.textContent = "暂无精确匹配屏蔽UP主";
exactMatchListElement.appendChild(empty);
}
}
/**
* 刷新面板中的正则匹配黑名单显示。
*/
function refreshRegexMatchList() {
if (!regexMatchListElement) {
if (!isBlacklistPanelCreated()) {
return;
}
regexMatchListElement = document.querySelector(
"#bilibili-blacklist-regex-list"
);
if (!regexMatchListElement) {
console.warn("[Bilibili-Blacklist] regexMatchListElement 未定义");
return;
}
}
regexMatchListElement.innerHTML = "";
regexMatchBlacklist.forEach((regex, index) => {
const item = createBlacklistListItem(regex, () => {
regexMatchBlacklist.splice(index, 1);
saveBlacklistsToStorage();
refreshRegexMatchList();
});
regexMatchListElement.appendChild(item);
});
// 反转列表顺序,使最新添加的显示在顶部
Array.from(regexMatchListElement.children)
.reverse()
.forEach((item) => regexMatchListElement.appendChild(item));
if (regexMatchBlacklist.length === 0) {
const empty = document.createElement("div");
empty.className = "bilibili-blacklist-empty";
empty.textContent = "暂无正则匹配屏蔽规则";
regexMatchListElement.appendChild(empty);
}
}
/**
* 刷新面板中的标签名黑名单显示。
*/
function refreshTagNameList() {
if (!tagNameListElement) {
if (!isBlacklistPanelCreated()) {
return;
}
tagNameListElement = document.querySelector(
"#bilibili-blacklist-tname-list"
);
if (!tagNameListElement) {
console.warn("[Bilibili-Blacklist] tagNameListElement 未定义");
return;
}
}
tagNameListElement.innerHTML = "";
tagNameBlacklist.forEach((tagName) => {
const item = createBlacklistListItem(tagName, () => {
removeFromTagNameBlacklist(tagName);
});
tagNameListElement.appendChild(item);
});
// 反转列表顺序,使最新添加的显示在顶部
Array.from(tagNameListElement.children)
.reverse()
.forEach((item) => tagNameListElement.appendChild(item));
if (tagNameBlacklist.length === 0) {
const empty = document.createElement("div");
empty.className = "bilibili-blacklist-empty";
empty.textContent = "暂无标签屏蔽规则";
tagNameListElement.appendChild(empty);
}
}
// 辅助函数:为设置创建切换按钮
function createSettingToggleButton(labelText, configKey, title = null) {
const container = document.createElement("div");
container.className =
"bilibili-blacklist-panel-row bilibili-blacklist-setting-toggle";
container.title = title; // 设置鼠标悬停提示
const label = document.createElement("span");
label.textContent = labelText;
const button = document.createElement("button");
button.className = "bilibili-blacklist-config-btn";
function refreshButtonAppearance() {
button.textContent = globalPluginConfig[configKey] ? "开启" : "关闭";
button.style.backgroundColor = globalPluginConfig[configKey]
? "#fb7299"
: "#909399";
}
button.addEventListener("click", () => {
globalPluginConfig[configKey] = !globalPluginConfig[configKey];
refreshButtonAppearance();
saveGlobalConfigToStorage();
if (configKey === "flagHoverReveal" && !globalPluginConfig[configKey]) {
restoreAllBlockedVideoOverlays();
}
});
refreshButtonAppearance(); // 初始化按钮外观
container.appendChild(label);
container.appendChild(button);
return container;
}
// 辅助函数:为设置创建输入文本
function createSettingInput(
labelText,
configKey,
title = null,
constraints = {}
) {
// 卡片扫描间隔设置
const Container = document.createElement("div");
Container.className =
"bilibili-blacklist-panel-row bilibili-blacklist-setting-input-row";
Container.title = title;
const Label = document.createElement("span");
Label.textContent = labelText;
const Input = document.createElement("input");
Input.type = "number";
Input.className = "bilibili-blacklist-number-input";
const { min = 0, max = null, step = null } = constraints;
Input.min = `${min}`;
if (max !== null) Input.max = `${max}`;
if (step !== null) Input.step = `${step}`;
Input.value = globalPluginConfig[configKey];
const Button = document.createElement("button");
Button.className =
"bilibili-blacklist-config-btn bilibili-blacklist-config-btn-primary";
Button.textContent = "保存";
Button.addEventListener("click", () => {
const val = Number(Input.value);
const isInRange =
Input.value.trim() !== "" &&
Number.isFinite(val) &&
val >= min &&
(max === null || val <= max);
if (isInRange) {
globalPluginConfig[configKey] = val;
saveGlobalConfigToStorage();
} else {
const rangeText = max === null ? `不小于 ${min}` : `${min} 到 ${max}`;
alert(`请输入${rangeText}之间的有效数字!`);
}
});
Container.appendChild(Label);
Container.appendChild(Input);
Container.appendChild(Button);
return Container;
}
// 辅助函数:为设置创建下拉选择框
function createSettingSelect(labelText, configKey, title = null, options = []) {
const container = document.createElement("div");
container.className =
"bilibili-blacklist-panel-row bilibili-blacklist-setting-select-row";
container.title = title;
const label = document.createElement("span");
label.textContent = labelText;
const select = document.createElement("select");
select.className = "bilibili-blacklist-select";
select.addEventListener("change", () => {
globalPluginConfig[configKey] = select.value;
saveGlobalConfigToStorage();
});
// 按当前配置值选中对应项
options.forEach((opt) => {
const option = document.createElement("option");
option.value = opt.value;
option.textContent = opt.label;
if (String(globalPluginConfig[configKey]) === String(opt.value)) {
option.selected = true;
}
select.appendChild(option);
});
container.appendChild(label);
container.appendChild(select);
return container;
}
/**
* 刷新面板中的配置设置显示。
*/
function refreshConfigSettings() {
if (!configListElement) {
if (!isBlacklistPanelCreated()) {
return;
}
configListElement = document.querySelector(
"#bilibili-blacklist-config-list"
);
if (!configListElement) {
console.warn("[Bilibili-Blacklist] configListElement 未定义");
return;
}
}
configListElement.innerHTML = "";
// 临时开关按钮
const tempToggleContainer = document.createElement("div");
tempToggleContainer.className =
"bilibili-blacklist-panel-row bilibili-blacklist-temp-toggle";
const tempToggleLabel = document.createElement("span");
tempToggleLabel.textContent = "临时开关";
tempUnblockButton = document.createElement("button");
tempUnblockButton.className = "bilibili-blacklist-config-btn";
tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽";
tempUnblockButton.style.background = isShowAllVideos
? "#dddddd"
: "#fb7299";
tempUnblockButton.addEventListener("click", toggleShowAllBlockedVideos);
tempToggleContainer.appendChild(tempToggleLabel);
tempToggleContainer.appendChild(tempUnblockButton);
configListElement.appendChild(tempToggleContainer);
const title = document.createElement("h4");
title.textContent = "全局配置开关(部分功能刷新后生效)";
configListElement.appendChild(title);
// 添加配置切换按钮
configListElement.appendChild(
createSettingToggleButton(
"屏蔽标题/Up主名",
"flagInfo",
"屏蔽标题/Up主名"
)
);
configListElement.appendChild(
createSettingToggleButton(
"屏蔽分类标签",
"flagTName",
"通过请求API获取分类标签"
)
);
// 标签缓存数量显示与清除按钮
const tagNameListControlContainer = document.createElement("div");
tagNameListControlContainer.className =
"bilibili-blacklist-panel-row bilibili-blacklist-cache-control";
tagNameListControlContainer.title = "打开视频播放页面可刷新";
const tagNameListLabel = document.createElement("span");
tagNameListLabel.textContent = `分类标签缓存数量: ${tagNameList.length}`;
const clearTagNameListButton = document.createElement("button");
clearTagNameListButton.className =
"bilibili-blacklist-config-btn bilibili-blacklist-config-btn-danger";
clearTagNameListButton.textContent = "清除";
clearTagNameListButton.addEventListener("click", () => {
if (confirm("确定要清除分类标签缓存吗?这不会影响已屏蔽的标签,但会使得下次需要重新从API获取标签信息。")) {
tagNameList.length = 0;
if (typeof saveTagNameListToStorage === "function") {
saveTagNameListToStorage();
} else {
GM_setValue("tagNameList", []);
GM_setValue("tLastTime", 0);
}
tagNameListLabel.textContent = `分类标签缓存数量: 0`;
}
});
tagNameListControlContainer.appendChild(tagNameListLabel);
tagNameListControlContainer.appendChild(clearTagNameListButton);
configListElement.appendChild(tagNameListControlContainer);
configListElement.appendChild(
createSettingToggleButton(
"屏蔽竖屏视频",
"flagVertical",
"通过请求API获取视频分辨率"
)
);
configListElement.appendChild(
createSettingToggleButton("屏蔽主页推荐", "flagAD", "直播/广告/分区推送")
);
configListElement.appendChild(
createSettingToggleButton(
"屏蔽主页视频软广",
"flagCM",
"cm.bilibili.com软广"
)
);
// 自动连播遇到被屏蔽视频的处理方式
configListElement.appendChild(
createSettingSelect(
"自动连播遇到被屏蔽视频:",
"flagSkipBlockedAutoplay",
"播放页开启自动连播并播到被屏蔽视频时:切换为未屏蔽视频 / 停止播放 / 不处理(按B站默认继续播放)。",
[
{ value: "skip", label: "切换为未屏蔽视频" },
{ value: "stop", label: "停止播放" },
{ value: "off", label: "不处理(默认)" },
]
)
);
//分割线
const hr = document.createElement("hr");
configListElement.appendChild(hr);
configListElement.appendChild(
createSettingToggleButton("遮挡被屏蔽视频", "flagKirby", "更加温和的方式")
);
configListElement.appendChild(
createSettingToggleButton(
"悬停后显示被遮挡视频",
"flagHoverReveal",
"鼠标在被遮挡的视频卡片上停留指定时间后临时显示,移开后重新遮挡。仅在“遮挡被屏蔽视频”开启时生效。"
)
);
configListElement.appendChild(
createSettingInput(
"悬停显示延迟 (秒):",
"hoverRevealDelaySeconds",
"允许设置 0.1 到 5 秒。",
{ min: 0.1, max: 5, step: 0.1 }
)
);
configListElement.appendChild(
createSettingToggleButton(
"加载时立即隐藏卡片",
"flagHideOnLoad",
"新卡片加载出来时是否立即隐藏,待处理完成后再决定显示或继续屏蔽。关闭此功能可能会导致卡片先显示后隐藏的闪烁。"
)
);
configListElement.appendChild(
createSettingInput(
"卡片扫描间隔 (ms):",
"blockScanInterval",
"扫描新卡片的间隔时间,单位 ms。值越小,新卡片隐藏越快,但可能会增加CPU负担。建议值 200ms。"
)
);
configListElement.appendChild(
createSettingInput(
"视频信息API请求间隔 (ms):",
"processQueueInterval",
"每个视频获取分类标签/视频分辨率时的API请求间隔时间,单位 ms。间隔时间越长,越不容易触发B站API限速。建议值 200ms。"
)
);
configListElement.appendChild(
createSettingInput(
"竖屏视频比例阈值:",
"verticalScaleThreshold",
"获取的视频API信息后,判断视频是否为竖屏(长 除于 宽)的阈值。建议值 0.7。"
)
);
}
/**
* 刷新黑名单管理面板中的所有标签页。
*/
function refreshAllPanelTabs() {
refreshExactMatchList();
refreshRegexMatchList();
refreshTagNameList();
refreshConfigSettings();
}
/**
* 检查黑名单管理面板是否已创建并存在于DOM中。
* 如果找到,则设置全局 `managerPanel` 引用。
* @returns {boolean} 如果面板存在则返回true,否则返回false。
*/
function isBlacklistPanelCreated() {
const panelInDom = document.querySelector(
"#bilibili-blacklist-manager-panel"
);
if (panelInDom) {
if (!managerPanel) {
managerPanel = panelInDom;
}
return true;
}
return false;
}
/**
* 创建黑名单管理面板。
*/
function createBlacklistPanel() {
if (isBlacklistPanelCreated()) {
return;
}
managerPanel = document.createElement("div");
managerPanel.id = "bilibili-blacklist-manager-panel"; // 确保ID唯一
// 创建标签容器
const tabContainer = document.createElement("div");
tabContainer.className = "bilibili-blacklist-tabs";
// 创建各个标签页的内容区域
const exactContent = document.createElement("div");
exactContent.className = "bilibili-blacklist-panel-content";
exactContent.style.display = "block"; // 默认显示精确匹配
const regexContent = document.createElement("div");
regexContent.className = "bilibili-blacklist-panel-content";
regexContent.style.display = "none";
const tnameContent = document.createElement("div");
tnameContent.className = "bilibili-blacklist-panel-content";
tnameContent.style.display = "none";
const configContent = document.createElement("div");
configContent.className = "bilibili-blacklist-panel-content";
configContent.style.display = "none";
// 定义标签页数据
const tabs = [
{ name: "精确匹配(Up名字)", content: exactContent },
{ name: "正则匹配(Up/标题)", content: regexContent },
{ name: "屏蔽分类", content: tnameContent },
{ name: "插件配置", content: configContent },
];
tabs.forEach((tabData) => {
const tab = document.createElement("div");
tab.className = "bilibili-blacklist-tab";
tab.textContent = tabData.name;
tab.style.borderBottom =
tabData.content.style.display === "block"
? "2px solid #fb7299"
: "none";
// 标签点击事件,切换内容显示
tab.addEventListener("click", () => {
tabs.forEach(({ tab: t, content: c }) => {
t.style.borderBottom = "none";
c.style.display = "none";
});
tab.style.borderBottom = "2px solid #fb7299";
tabData.content.style.display = "block";
});
tabData.tab = tab; // 保存对标签元素的引用
tabContainer.appendChild(tab);
});
// 创建面板头部
const header = document.createElement("div");
header.className = "bilibili-blacklist-panel-header";
blockCountTitleElement = document.createElement("h3");
blockCountTitleElement.title = "总数 =(UP/标题 + 广告 + CM + 分类/竖屏)";
const closeBtn = document.createElement("button");
closeBtn.className = "bilibili-blacklist-panel-close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", () => {
managerPanel.style.display = "none";
});
header.appendChild(blockCountTitleElement);
header.appendChild(closeBtn);
const contentContainer = document.createElement("div");
contentContainer.className = "bilibili-blacklist-panel-body";
// 精确匹配添加输入框和按钮
const addExactContainer = document.createElement("div");
addExactContainer.className = "bilibili-blacklist-add-row";
const exactInput = document.createElement("input");
exactInput.type = "text";
exactInput.placeholder = "输入要屏蔽的UP主名称";
const addExactBtn = document.createElement("button");
addExactBtn.className = "bilibili-blacklist-primary-btn";
addExactBtn.textContent = "添加";
addExactBtn.addEventListener("click", () => {
const upName = exactInput.value.trim();
if (upName) {
addToExactBlacklist(upName);
exactInput.value = "";
}
});
addExactContainer.appendChild(exactInput);
addExactContainer.appendChild(addExactBtn);
exactContent.appendChild(addExactContainer);
// 正则匹配添加输入框和按钮
const addRegexContainer = document.createElement("div");
addRegexContainer.className = "bilibili-blacklist-add-row";
const regexInput = document.createElement("input");
regexInput.type = "text";
regexInput.placeholder = "输入正则表达式 (如: 小小.*Official)";
const addRegexBtn = document.createElement("button");
addRegexBtn.className = "bilibili-blacklist-primary-btn";
addRegexBtn.textContent = "添加";
addRegexBtn.addEventListener("click", () => {
const regex = regexInput.value.trim();
if (regex && !regexMatchBlacklist.includes(regex)) {
try {
new RegExp(regex); // 验证正则表达式
regexMatchBlacklist.push(regex);
saveBlacklistsToStorage();
regexInput.value = "";
refreshRegexMatchList();
} catch (e) {
alert("无效的正则表达式: " + e.message);
}
}
});
addRegexContainer.appendChild(regexInput);
addRegexContainer.appendChild(addRegexBtn);
regexContent.appendChild(addRegexContainer);
// 创建列表元素
exactMatchListElement = document.createElement("ul");
exactMatchListElement.id = "bilibili-blacklist-exact-list";
regexMatchListElement = document.createElement("ul");
regexMatchListElement.id = "bilibili-blacklist-regex-list";
tagNameListElement = document.createElement("ul");
tagNameListElement.id = "bilibili-blacklist-tname-list";
configListElement = document.createElement("ul");
configListElement.id = "bilibili-blacklist-config-list";
refreshAllPanelTabs(); // 初始化所有标签页内容
exactContent.appendChild(exactMatchListElement);
regexContent.appendChild(regexMatchListElement);
tnameContent.appendChild(tagNameListElement);
configContent.appendChild(configListElement);
contentContainer.appendChild(exactContent);
contentContainer.appendChild(regexContent);
contentContainer.appendChild(tnameContent);
contentContainer.appendChild(configContent);
managerPanel.appendChild(tabContainer);
managerPanel.appendChild(header);
managerPanel.appendChild(contentContainer);
document.body.appendChild(managerPanel);
return managerPanel;
}
/**
* 为插件添加全局CSS样式。
*/
GM_addStyle(`
/* ===== 屏蔽按钮容器 ===== */
.bilibili-blacklist-block-container {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 2px;
font-size: 12px;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 3px;
z-index: 9999;
pointer-events: none;
}
.bili-video-card:hover .bilibili-blacklist-block-container,
.card-box:hover .bilibili-blacklist-block-container,
.bilibili-blacklist-block-container-host:hover .bilibili-blacklist-block-container {
display: flex !important;
}
.card-box .bilibili-blacklist-block-container {
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
height: 100%;
}
.card-box .bilibili-blacklist-tname-group {
flex-direction: column;
align-items: flex-end;
margin-top: auto;
}
/* btn / reason / tname 共用基础外观 */
.bilibili-blacklist-block-btn,
.bilibili-blacklist-block-reason,
.bilibili-blacklist-tname {
display: flex;
justify-content: center;
align-items: center;
height: 20px;
padding: 0 6px;
box-sizing: border-box;
font-size: 12px;
line-height: 1;
color: white;
text-align: center;
white-space: nowrap;
border: none;
border-radius: 2px;
}
.bilibili-blacklist-block-btn {
position: static;
width: 40px;
pointer-events: auto !important;
background-color: #fb7299dd;
cursor: pointer;
}
.bilibili-blacklist-block-reason {
background-color: #f56c6c;
pointer-events: none;
}
.bilibili-blacklist-tname-group {
display: flex;
flex-direction: row;
padding: 0 5px;
gap: 3px;
align-items: center;
margin-left: auto;
max-width: 80%;
pointer-events: none;
}
.bilibili-blacklist-tname {
background-color: #fb7299dd;
text-overflow: ellipsis;
overflow: hidden;
pointer-events: auto;
cursor: pointer;
}
/* ===== 修复视频卡片布局 ===== */
.bili-video-card__cover {
contain: layout !important;
}
/* ===== 管理面板 ===== */
#bilibili-blacklist-manager-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
max-height: 80vh;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 99999;
overflow: hidden;
display: none;
flex-direction: column;
font-size: 15px;
color: var(--text2, #000);
background-color: var(--bg1, #fff);
opacity: 0.85;
}
#bilibili-blacklist-manager-panel h3,
#bilibili-blacklist-manager-panel h4 {
color: var(--text2, #000);
}
#bilibili-blacklist-manager-panel h3 {
margin: 0;
font-weight: 500;
}
#bilibili-blacklist-manager-panel h4 {
font-weight: bold;
margin-bottom: 12px;
}
#bilibili-blacklist-manager-panel ul {
list-style: none;
padding: 0;
margin: 0;
}
#bilibili-blacklist-manager-panel hr {
margin: 12px 0;
border: none;
border-top: 2px solid #ddd;
}
/* 按钮基础交互 */
#bilibili-blacklist-manager-panel button {
transition: background-color 0.2s;
}
#bilibili-blacklist-manager-panel button:hover {
opacity: 0.9;
}
/* 输入框 */
#bilibili-blacklist-manager-panel input:focus {
outline: none;
border-color: #fb7299 !important;
}
#bilibili-blacklist-manager-panel input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
#bilibili-blacklist-manager-panel select {
flex: 1;
min-width: 120px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
color: var(--text2, #000);
background-color: var(--bg1, #fff);
}
.bilibili-blacklist-setting-select-row {
margin-bottom: 8px;
}
/* 面板结构 */
.bilibili-blacklist-tabs {
display: flex;
border-bottom: 1px solid #f1f2f3;
}
.bilibili-blacklist-tab {
padding: 12px 16px;
cursor: pointer;
font-weight: 500;
}
.bilibili-blacklist-panel-content {
padding: 16px;
overflow-y: auto;
flex: 1;
}
.bilibili-blacklist-panel-header {
padding: 16px;
border-bottom: 1px solid #f1f2f3;
display: flex;
justify-content: space-between;
align-items: center;
}
.bilibili-blacklist-panel-close {
background: none;
border: none;
cursor: pointer;
padding: 0 8px;
color: var(--text2, #000);
}
.bilibili-blacklist-panel-body {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
/* 布局行 */
.bilibili-blacklist-panel-row,
.bilibili-blacklist-add-row {
display: flex;
align-items: center;
gap: 8px;
}
.bilibili-blacklist-panel-row > span:first-child {
flex: 1;
}
.bilibili-blacklist-add-row {
margin-bottom: 16px;
}
.bilibili-blacklist-setting-toggle {
margin-bottom: 8px;
}
.bilibili-blacklist-setting-input-row {
margin-top: 16px;
}
.bilibili-blacklist-temp-toggle {
margin: 20px 0;
}
.bilibili-blacklist-cache-control {
margin-bottom: 8px;
}
/* 列表项 */
.bilibili-blacklist-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f1f2f3;
}
.bilibili-blacklist-list-item > span {
flex: 1;
}
.bilibili-blacklist-empty {
text-align: center;
padding: 16px;
color: #999;
}
/* 按钮 */
.bilibili-blacklist-panel-btn,
.bilibili-blacklist-config-btn,
.bilibili-blacklist-primary-btn {
color: #fff;
border: none;
cursor: pointer;
}
.bilibili-blacklist-panel-btn {
padding: 4px 8px;
border-radius: 4px;
}
.bilibili-blacklist-config-btn {
padding: 6px 12px;
border-radius: 4px;
}
.bilibili-blacklist-config-btn-primary {
background-color: #fb7299;
}
.bilibili-blacklist-config-btn-danger {
background-color: #f56c6c;
}
.bilibili-blacklist-primary-btn {
padding: 8px 16px;
background: #fb7299;
border-radius: 4px;
}
.bilibili-blacklist-number-input {
width: 100px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* ===== 顶栏管理按钮 ===== */
#bilibili-blacklist-manager-button {
cursor: pointer;
}
#bilibili-blacklist-manager-button .right-entry-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#bilibili-blacklist-manager-button .right-entry__outside {
margin-bottom: -5px;
}
#bilibili-blacklist-manager-button:hover svg {
transform: scale(1.1);
}
#bilibili-blacklist-manager-button svg {
transition: transform 0.2s;
}
/* ===== 卡比覆盖层 ===== */
#bilibili-blacklist-kirby {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
border-radius: 6px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: opacity ${KIRBY_FADE_DURATION_MS / 1000}s ease;
}
#bilibili-blacklist-kirby.bilibili-blacklist-kirby-video {
justify-content: flex-start;
}
#bilibili-blacklist-kirby svg {
opacity: 0.15;
filter: none;
margin-top: -40px;
}
#bilibili-blacklist-kirby.bilibili-blacklist-kirby-video svg {
margin-top: -10px;
}
/* ===== 用户空间页屏蔽按钮 ===== */
.bilibili-blacklist-up-block-btn-host {
display: inline-flex;
align-items: center;
}
.bilibili-blacklist-up-block-btn {
width: 100px;
height: 30px;
margin-left: 10px;
color: #fff;
border-radius: 5px;
border: 1px solid #fb7299;
}
/* ===== 灰度效果 ===== */
.bilibili-blacklist-grayscale {
filter: grayscale(95%);
}
`);
/**
* 返回卡比图标的SVG代码。
* @returns {string} SVG字符串。
*/
function getKirbySVG() {
return `
<svg width="35" height="35" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" >
<ellipse cx="70" cy="160" rx="30" ry="15" fill="#cc3333" />
<ellipse cx="130" cy="160" rx="30" ry="15" fill="#cc3333" />
<ellipse cx="50" cy="120" rx="20" ry="20" fill="#ffb6c1" />
<ellipse cx="150" cy="120" rx="20" ry="20" fill="#ffb6c1" />
<circle cx="100" cy="110" r="60" fill="#ffb6c1" />
<ellipse cx="80" cy="90" rx="10" ry="22" fill="blue" />
<ellipse cx="80" cy="88" rx="10" ry="15" fill="black" />
<ellipse cx="80" cy="82" rx="8" ry="12" fill="#ffffff" />
<ellipse cx="80" cy="90" rx="10" ry="22" fill="#00000000" stroke="#000000" strokeWidth="4" />
<ellipse cx="120" cy="90" rx="10" ry="22" fill="blue" />
<ellipse cx="120" cy="88" rx="10" ry="15" fill="black" />
<ellipse cx="120" cy="82" rx="8" ry="12" fill="#ffffff" />
<ellipse cx="120" cy="90" rx="10" ry="22" fill="#00000000" stroke="#000000" strokeWidth="4" />
<ellipse cx="60" cy="110" rx="8" ry="5" fill="#ff4466" />
<ellipse cx="140" cy="110" rx="8" ry="5" fill="#ff4466" />
<path d="M 90 118 Q 100 125, 110 118" stroke="black" strokeWidth="3" fill="transparent" />
</svg>
`;
}
/**
* 渐显卡比覆盖层(鼠标移开后恢复遮挡)。
* @param {HTMLElement} overlay - 卡比覆盖层元素。
*/
function fadeInKirbyOverlay(overlay) {
if (!overlay) return;
const pendingTimer = kirbyFadeTimers.get(overlay);
if (pendingTimer) {
clearTimeout(pendingTimer);
kirbyFadeTimers.delete(overlay);
}
overlay.style.display = "flex";
overlay.style.opacity = "0";
void overlay.offsetHeight; // 强制重排以触发过渡动画
overlay.style.opacity = "1";
}
/**
* 渐隐卡比覆盖层(悬停临时显示视频)。
* @param {HTMLElement} overlay - 卡比覆盖层元素。
*/
function fadeOutKirbyOverlay(overlay) {
if (!overlay) return;
const pendingTimer = kirbyFadeTimers.get(overlay);
if (pendingTimer) clearTimeout(pendingTimer);
overlay.style.opacity = "0";
kirbyFadeTimers.set(
overlay,
setTimeout(() => {
kirbyFadeTimers.delete(overlay);
if (overlay.isConnected && overlay.style.opacity === "0") {
overlay.style.display = "none";
}
}, KIRBY_FADE_DURATION_MS)
);
}
/**
* 取消卡比覆盖层的渐隐/渐显计时器。
* @param {HTMLElement} overlay - 卡比覆盖层元素。
*/
function cancelKirbyFade(overlay) {
if (!overlay) return;
const pendingTimer = kirbyFadeTimers.get(overlay);
if (pendingTimer) {
clearTimeout(pendingTimer);
kirbyFadeTimers.delete(overlay);
}
}
/**
* 恢复所有被悬停临时显示的视频遮罩。
*/
function restoreAllBlockedVideoOverlays() {
if (isShowAllVideos) return;
blockedVideoCards.forEach((card) => {
const overlay = card.querySelector("#bilibili-blacklist-kirby");
if (overlay) fadeInKirbyOverlay(overlay);
});
}
/**
* 为被遮挡的视频卡片绑定悬停临时显示行为。
* @param {HTMLElement} cardElement - 视频卡片元素。
*/
function bindHoverRevealToCard(cardElement) {
if (hoverRevealBoundCards.has(cardElement)) return;
hoverRevealBoundCards.add(cardElement);
cardElement.addEventListener("mouseenter", () => {
const realCard = getRealVideoCardElement(cardElement);
if (
!globalPluginConfig.flagHoverReveal ||
isShowAllVideos ||
!blockedVideoCards.has(realCard)
) {
return;
}
const overlayOnEnter = cardElement.querySelector(
"#bilibili-blacklist-kirby"
);
// 若正在渐隐,先取消,避免悬停期间遮罩消失
cancelKirbyFade(overlayOnEnter);
const existingTimer = hoverRevealTimers.get(cardElement);
if (existingTimer) clearTimeout(existingTimer);
const delaySeconds = Math.min(
5,
Math.max(0.1, Number(globalPluginConfig.hoverRevealDelaySeconds) || 1)
);
const timer = setTimeout(() => {
hoverRevealTimers.delete(cardElement);
if (!globalPluginConfig.flagHoverReveal || isShowAllVideos) return;
const overlay = cardElement.querySelector(
"#bilibili-blacklist-kirby"
);
if (overlay && blockedVideoCards.has(realCard)) {
fadeOutKirbyOverlay(overlay);
}
}, delaySeconds * 1000);
hoverRevealTimers.set(cardElement, timer);
});
cardElement.addEventListener("mouseleave", () => {
const timer = hoverRevealTimers.get(cardElement);
if (timer) {
clearTimeout(timer);
hoverRevealTimers.delete(cardElement);
}
if (isShowAllVideos) return;
const overlay = cardElement.querySelector("#bilibili-blacklist-kirby");
if (
overlay &&
blockedVideoCards.has(getRealVideoCardElement(cardElement))
) {
fadeInKirbyOverlay(overlay);
}
});
}
/**
* 为视频卡片添加卡比主题的覆盖层。
* @param {HTMLElement} cardElement - 视频卡片元素。
*/
function addKirbyOverlayToCard(cardElement) {
bindHoverRevealToCard(cardElement);
// 如果已经有Kirby覆盖层,则不重复添加
if (cardElement.querySelector("#bilibili-blacklist-kirby") != null) return;
const kirbyWrapper = document.createElement("div");
kirbyWrapper.innerHTML = getKirbySVG();
kirbyWrapper.id = "bilibili-blacklist-kirby";
if (isCurrentPageVideo()) {
kirbyWrapper.classList.add("bilibili-blacklist-kirby-video");
}
const svg = kirbyWrapper.querySelector("svg");
if (svg) {
const cardRect = cardElement.getBoundingClientRect();
const size = Math.min(cardRect.width, cardRect.height) * 0.8;
svg.setAttribute("width", `${size}px`);
svg.setAttribute("height", `${size}px`);
}
const hostElement = isCurrentPageCategory()
? cardElement.querySelector(".bili-video-card") || cardElement
: cardElement;
// 确保宿主元素有position属性以便子元素绝对定位
const hostStyle = getComputedStyle(hostElement);
if (hostStyle.position === "static" || !hostStyle.position) {
hostElement.style.position = "relative";
}
hostElement.appendChild(kirbyWrapper);
}
/**
* 从视频卡片中移除卡比覆盖层。
* @param {HTMLElement} cardElement - 视频卡片元素。
*/
function removeKirbyOverlay(cardElement) {
const kirbyWrapper = cardElement.querySelector("#bilibili-blacklist-kirby");
if (kirbyWrapper) {
kirbyWrapper.remove();
}
}
// 监听页面可见性变化
document.addEventListener("visibilitychange", () => {
isPageCurrentlyActive = !document.hidden;
});
// 监听窗口焦点获取 (用户请求停用)
/*
window.addEventListener("focus", () => {
isPageCurrentlyActive = true;
});
*/
// 监听窗口焦点失去 (用户请求停用)
/*
window.addEventListener("blur", () => {
isPageCurrentlyActive = false;
});
*/
// MutationObserver 检测动态加载的新内容
const contentObserver = new MutationObserver((mutations) => {
let shouldCheck = false;
// 对视频播放页进行优化,只在实际添加了可见元素时触发扫描
if (isCurrentPageVideo()) {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
shouldCheck = Array.from(mutation.addedNodes).some((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return false;
// 检查节点是否有实际的尺寸,避免不必要的扫描
const hasVisibleContent =
node.offsetWidth > 0 ||
node.offsetHeight > 0 ||
node.querySelector("[offsetWidth], [offsetHeight]");
return hasVisibleContent;
});
}
});
} else {
// 其他页面只要有节点添加就触发
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
shouldCheck = true;
}
});
}
if (shouldCheck) {
// 使用setTimeout延迟扫描,避免短时间内多次触发
setTimeout(() => {
scanAndBlockVideoCards();
if (isCurrentPageMain()) {
blockMainPageAds(); // 主页广告屏蔽
}
if (isCurrentPageVideo()) {
blockVideoPageAds(); // 视频页广告屏蔽
}
if (!document.getElementById("bilibili-blacklist-manager-button")) {
// addBlacklistManagerButton(); // 确保管理按钮存在
}
}, globalPluginConfig.blockScanInterval);
}
});
/**
* 在指定容器上初始化MutationObserver。
* @param {string} containerIdOrSelector - 要观察的容器的ID或CSS选择器。
*/
function initializeObserver(containerIdOrSelector) {
const rootNode =
document.getElementById(containerIdOrSelector) ||
document.querySelector(containerIdOrSelector) ||
document.documentElement; // 默认观察整个文档
contentObserver.observe(rootNode, {
childList: true,
subtree: true,
});
}
/**
* 根据当前页面初始化脚本。
*/
function initializeScript() {
if (!isfirstLoad) return;
isfirstLoad = false;
// 重置状态变量
isBlockingOperationInProgress = false;
lastBlockScanExecutionTime = 0;
blockedVideoCards = new Set();
videoCardProcessQueue = new Set();
processedVideoCards = new WeakSet();
// 根据当前页面URL判断并初始化
if (isCurrentPageMain()) {
initializeMainPage();
blockMainPageAds();
} else if (isCurrentPageSearch()) {
initializeSearchPage();
blockMainPageAds(); // 搜索页也进行主页广告屏蔽
} else if (isCurrentPageVideo()) {
initializeVideoPage();
updateTNameList();
} else if (isCurrentPageCategory()) {
initializeCategoryPage();
updateTNameList();
} else if (isCurrentUserSpace()) {
initializeUserSpace();
} else {
return; // 不支持的页面不进行初始化
}
createBlacklistPanel(); // 创建管理面板
addBlacklistManagerButton(); // 立即挂载管理按钮,避免在视频页被迟到的顶栏渲染顶掉前不可见;后续由观察器兜底
console.log("[bilibili-blacklist] 脚本已加载🥔");
}
let isfirstLoad = true;
let rescanVideoPageTimer = null; // 视频页定时补充扫描定时器
// 监听DOMContentLoaded并检查readyState以进行早期初始化
// initializeScript 内部已通过 isfirstLoad 保证只执行一次
document.addEventListener("DOMContentLoaded", initializeScript);
if (document.readyState === "complete" && isfirstLoad) {
initializeScript();
}
/**
* 检查当前页面是否为Bilibili主页。
* @returns {boolean} 如果是主页则返回true,否则返回false。
*/
function isCurrentPageMain() {
return location.pathname === "/" || location.pathname === "/index.html";
}
/**
* 初始化主页特有的功能。
*/
function initializeMainPage() {
initializeObserver("feedchannel-main"); // 观察主页内容区域
console.log("[bilibili-blacklist] 主页已加载🍓");
}
/**
* 检查当前页面是否为Bilibili搜索结果页。
* @returns {boolean} 如果是搜索页则返回true,否则返回false。
*/
function isCurrentPageSearch() {
return location.hostname === "search.bilibili.com";
}
/**
* 初始化搜索页特有的功能。
*/
function initializeSearchPage() {
initializeObserver("i_cecream"); // 观察搜索结果内容区域
console.log("[bilibili-blacklist] 搜索页已加载🍉");
}
/**
* 检查当前页面是否为Bilibili视频播放页。
* @returns {boolean} 如果是视频播放页则返回true,否则返回false。
*/
function isCurrentPageVideo() {
return location.pathname.startsWith("/video/");
}
/**
* 初始化视频播放页特有的功能。
*/
function initializeVideoPage() {
// **用户修改 2: 延迟 5 秒启动屏蔽功能**
console.log("[bilibili-blacklist] 播放页已加载,将延迟 5 秒启动功能。🍇");
const flag = globalPluginConfig.flagSkipBlockedAutoplay;
globalPluginConfig.flagSkipBlockedAutoplay = "off";
// 延迟 5 秒执行核心功能
setTimeout(() => {
initializeObserver("right-container"); // 观察视频播放页右侧推荐区域
// 首次手动扫描和广告屏蔽
scanAndBlockVideoCards();
blockVideoPageAds();
// 自动连播遇到被屏蔽视频时的处理(停止/切换/不处理,由用户配置)
initAutoplaySkip();
// 视频页在页面内切集后,右侧推荐会原地重建;观察器可能绑定到已替换的节点,
// 这里定时补充扫描,确保新加载的卡片也能被处理(scanAndBlockVideoCards 内部有节流与去重)。
rescanVideoPageTimer = setInterval(() => {
scanAndBlockVideoCards();
globalPluginConfig.flagSkipBlockedAutoplay= flag; // 第一次打开页面时,无论如何都不做处理
}, 2500);
// 顶栏可能有数秒延迟渲染,若在这之前已超过6个li,手动补挂管理按钮
addBlacklistManagerButton();
console.log("[bilibili-blacklist] 视频播放页屏蔽功能已启动。");
}, 5000); // 5000 毫秒 = 5 秒
}
/**
* 检查当前页面是否为Bilibili分类页。
* @returns {boolean} 如果是分类页则返回true,否则返回false。
*/
function isCurrentPageCategory() {
return location.pathname.startsWith("/c/");
}
/**
* 初始化分类页特有的功能。
*/
function initializeCategoryPage() {
initializeObserver("app"); // 观察整个app容器
console.log("[bilibili-blacklist] 分类页已加载🍊");
}
/**
* 检查当前页面是否为Bilibili用户空间页。
* @returns {boolean} 如果是用户空间页则返回true,否则返回false。
*/
function isCurrentUserSpace() {
return location.hostname === "space.bilibili.com";
}
/**
* 初始化用户空间页特有的功能。
*/
function initializeUserSpace() {
console.log("[bilibili-blacklist] 用户空间已加载🍎");
const upNameSelector = "#h-name, .nickname"; // UP主名称的选择器
// 创建一个MutationObserver来等待UP主名称元素加载
const observerForUpName = new MutationObserver((mutations, observer) => {
const upNameElement = document.querySelector(upNameSelector);
if (upNameElement) {
observer.disconnect(); // 找到元素后停止观察
addBlockButtonToUserSpace(upNameElement);
}
});
observerForUpName.observe(document.body, {
childList: true,
subtree: true,
});
// 立即检查一次,如果元素已经存在则直接处理
const initialUpNameElement = document.querySelector(upNameSelector);
if (initialUpNameElement) {
observerForUpName.disconnect();
addBlockButtonToUserSpace(initialUpNameElement);
}
}
/**
* 在用户空间页面上的UP主名称元素添加屏蔽/取消屏蔽按钮。
* @param {HTMLElement} upNameElement - 包含UP主名称的元素。
*/
function addBlockButtonToUserSpace(upNameElement) {
const upName = upNameElement.textContent.trim();
// 避免重复添加按钮
if (upNameElement.querySelector(".bilibili-blacklist-up-block-btn")) {
return;
}
// 调整UP主名称元素的样式,以便容纳按钮
upNameElement.classList.add("bilibili-blacklist-up-block-btn-host");
const button = document.createElement("button");
button.className = "bilibili-blacklist-up-block-btn";
button.textContent = "屏蔽";
// 刷新按钮状态和页面灰度效果
const refreshButtonStatus = () => {
const blocked = isBlacklisted(upName);
if (blocked) {
button.textContent = "已屏蔽";
button.style.backgroundColor = "#dddddd";
button.style.border = "1px solid #ccc";
upNameElement.style.textDecoration = "line-through"; // 添加删除线
document.body.classList.add("bilibili-blacklist-grayscale"); // 添加灰度滤镜
} else {
button.textContent = "屏蔽";
button.style.backgroundColor = "#fb7299";
button.style.border = "1px solid #fb7299";
upNameElement.style.textDecoration = "none"; // 移除删除线
document.body.classList.remove("bilibili-blacklist-grayscale"); // 移除灰度滤镜
}
};
button.addEventListener("click", (e) => {
e.stopPropagation();
const blocked = isBlacklisted(upName);
if (blocked) {
removeFromExactBlacklist(upName);
} else {
addToExactBlacklist(upName);
}
refreshButtonStatus(); // 更新按钮状态
});
refreshButtonStatus(); // 设置按钮初始状态
upNameElement.appendChild(button);
}
/**
* 屏蔽主页上的广告。
*/
function blockMainPageAds() {
if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回
const adSelectors = [
".floor-single-card", // 分区推荐
".bili-live-card", // 直播推广
".btn-ad", // 广告按钮
];
adSelectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((adCard) => {
hideVideoCard(adCard, "ad"); // 隐藏广告卡片
});
});
}
/**
* 屏蔽视频播放页上的广告。
*/
function blockVideoPageAds() {
if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回
const adSelectors = [
".video-card-ad-small", // 右上角推广
".slide-ad-exp", // 大推广
".video-page-game-card-small", // 游戏推广
".activity-m-v1", // 活动推广
".video-page-special-card-small", // 特殊卡片推广
".ad-floor-exp", // 广告地板
".btn-ad", // 广告按钮
".video-page-operator-card-small", // 运营推广
".ad-report",//广告
];
adSelectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((adCard) => {
hideVideoCard(adCard, "ad"); // 隐藏广告卡片
});
});
}
/// 12-16-2025 临时修复B站API无法获取Tname 问题,使用tid + 保存在本地的列表实现
// 从Video page 获取 本地资源
function getTNameListFormVideoPage() {
try {
var channelKv = unsafeWindow.__INITIAL_STATE__.channelKv;
if (!channelKv) return [];
var result = [];
// 遍历主频道
if (Array.isArray(channelKv)) {
channelKv.forEach(element => {
// if (!element.channelId || !element.name) {
//result.push({ id: element.channelId, tname: element.name });
// }
// 遍历子频道(sub)
var subList = element.sub;
if (Array.isArray(subList)) {
subList.forEach(subelement => {
if (element.channelId && element.name && subelement.tid && subelement.name) {
result.push({ id: subelement.tid, name: element.name, name_v2: subelement.name });
}
});
}
});
}
return result;
} catch (e) {
console.error("[bilibili-blacklist] 获取频道数据失败:", e);
return [];
}
}
// 增量更新 Tname list //24小时一次
function updateTNameList() {
if (tagNameList.length >= 1000) tagNameList = []; //防止过大时卡顿,清空重建
if (tagNameList.length === 0) tagListLastTime = 0; //确保初始为空时进行更新
const now = Date.now();
if (now - tagListLastTime < 6000) {
console.log("[bilibili-blacklist] 标签名列表最近已更新,跳过本次更新。");
return;
}
const newList = getTNameListFormVideoPage();
if (newList.length === 0) {
console.warn("[bilibili-blacklist] 未能获取到新的标签名列表。");
return;
}
console.log(`[bilibili-blacklist] 获取到 ${newList.length} 个标签名,开始合并更新。`);
// 构建现有标签的映射以便快速查找(基于id)
const existingMap = new Map();
tagNameList.forEach(item => existingMap.set(String(item.id), item));
let updated = false;
for (const item of newList) {
const id = String(item.id);
const name = item.name; // 注意:getTNameListFormVideoPage 返回的是 tname 属性
const name_v2 = item.name_v2;
if (!existingMap.has(id)) {
// 新增条目
tagNameList.push({ id: item.id, name, name_v2 });
existingMap.set(id, { id: item.id, name, name_v2 });
updated = true;
} else {
// 已存在,检查名称是否一致,若不一致则更新
const existing = existingMap.get(id);
if (existing.name !== name) {
existing.name = name;
updated = true;
}
}
}
if (updated) {
saveTagNameListToStorage();
tagListLastTime = now; // 更新局部变量以保持同步
console.log("[bilibili-blacklist] 标签名列表已更新并保存。");
} else {
console.log("[bilibili-blacklist] 标签名列表无变化,仅更新时间戳。");
// 即使没有变化,也更新最后更新时间,避免频繁检查
GM_setValue("tLastTime", now);
tagListLastTime = now; // 更新局部变量以保持同步
}
}
/**
* 自动连播处理模块。
*
* 背景:B站播放页(/video/BVxx) 开启“自动连播”后,播放器会自己从“相关推荐 / 接下来播放”里挑
* 下一个视频播放。这个“下一个”来自播放器内部的推荐/播放列表数据,而不是右侧已经
* 渲染出来的卡片;而且 B 站新版播放器是在页面内原地切换视频,地址栏 URL 不一定同步变。
* 因此仅靠隐藏卡片(藏 DOM)或监听 URL 变化都不靠谱。
*
* 本模块的正确做法:识别“当前正在播放的视频”本身(读取播放器当前 UP 主名 + 标题,
* 视切集时会原地更新),一旦发现当前播放的视频被屏蔽,就按配置
* (globalPluginConfig.flagSkipBlockedAutoplay)采取三种行为之一:
* "skip" 切换到第一条未屏蔽的视频(避免继续播被屏蔽视频)
* "stop" 停止播放
* "off" 什么都不做(保留 B 站默认行为,继续播放被屏蔽的视频)
*/
// 内部运行状态
let autoplayWatchTimer = null; // 轮询定时器
let lastSignature = ""; // 上一次检测到的“当前播放视频”特征(UP名 + 标题 + BV)
let lastHandledBv = ""; // 上一次已经处理过的目标 BV,避免重复处理
let isHandling = false; // 防止多次处理并发
// 当前播放视频的标题选择器(B站标题通常为 h1)
const CURRENT_VIDEO_TITLE_SELECTORS = [
"h1.video-info-title",
"h1.video-title",
"h1",
];
// 当前播放视频的 UP 主名选择器(视频信息栏附近;新版常用 .upname)
const CURRENT_VIDEO_UP_SELECTORS = [
".up-info-container .name",
".up-info .name",
".video-info .name",
".video-info-v2 .up-name",
".bili-video-info .up-name",
".up-name",
".up-info-container .upname a span",
".video-info .upname a span",
".video-info-container .upname a span",
".upname a span",
".upname a",
".upname",
];
/**
* 从 URL 提取 BV ID。
* @returns {string|null}
*/
function getBvFromUrl() {
const m = location.pathname.match(/\/video\/(BV\w+)/);
return m ? m[1] : null;
}
/**
* 尝试从 window.player 读取当前播放视频的 BV(比 URL 更贴近“正在播的视频”)。
* @returns {string|null}
*/
function getBvFromPlayer() {
const p = window.player;
if (!p) return null;
const tries = [
() => p.getVideoID && p.getVideoID(),
() => p.getVideo && p.getVideo().bvid,
() => p.getVideoInfo && p.getVideoInfo().bvid,
() => p.__video && p.__video.bvid,
];
for (const f of tries) {
try {
const v = f();
if (typeof v === "string" && /^BV\w+/.test(v)) return v;
} catch (e) {
// 该方法不适用,继续尝试
}
}
return null;
}
/**
* 取当前播放视频的 BV:优先播放器,其次 URL。
* @returns {string|null}
*/
function getCurrentBv() {
return getBvFromPlayer() || getBvFromUrl();
}
/**
* 从页面 DOM 读取“当前正在播放的视频”的标题和 UP 主名。
* 播放器在切集时会原地更新这些信息,因此它是判断“当前播放视频”最可靠的信号。
* @returns {{upName: string, title: string}}
*/
function getPlayingVideoInfo() {
let title = "";
for (const sel of CURRENT_VIDEO_TITLE_SELECTORS) {
const el = document.querySelector(sel);
if (el && el.textContent.trim()) {
title = el.textContent.trim();
break;
}
}
let upName = "";
for (const sel of CURRENT_VIDEO_UP_SELECTORS) {
const el = document.querySelector(sel);
if (el && el.textContent.trim()) {
upName = el.textContent.trim();
break;
}
}
return { upName, title };
}
/**
* 从 __INITIAL_STATE__ 读取“bvid -> {upName, title}”映射。
* 数据源:videoData(当前视频)+ related(推荐列表),二者都带 UP 主名/标题。
* @returns {Object<string, {upName:string,title:string}>}
*/
function buildBvInfoMapFromInitialState() {
const map = {};
const state =
typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
if (!state) return map;
if (state.videoData && state.videoData.bvid) {
map[state.videoData.bvid] = {
upName: (state.videoData.owner && state.videoData.owner.name) || "",
title: state.videoData.title || "",
};
}
if (Array.isArray(state.related)) {
for (const item of state.related) {
if (!item.bvid) continue;
map[item.bvid] = {
upName: (item.owner && item.owner.name) || "",
title: item.title || "",
};
}
}
return map;
}
/**
* 从 __INITIAL_STATE__ 读取“title -> {upName, bvid}”映射。
* 自动连播卡片里只显示标题、没有 UP 名,所以用标题去 __INITIAL_STATE__.related 里反查 UP 名最靠谱。
* @returns {Object<string, {upName:string, bvid:string}>}
*/
function buildTitleInfoMapFromInitialState() {
const map = {};
const state =
typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
if (!state) return map;
const add = (title, upName, bvid) => {
if (!title || !upName) return;
if (!map[title] || !map[title].upName) {
map[title] = { upName, bvid };
}
};
if (state.videoData && state.videoData.bvid) {
add(state.videoData.title, state.videoData.owner && state.videoData.owner.name, state.videoData.bvid);
}
if (Array.isArray(state.related)) {
for (const item of state.related) {
add(item.title, item.owner && item.owner.name, item.bvid);
}
}
return map;
}
/**
* 判断视频数据是否命中“分类标签”黑名单(与卡片屏蔽逻辑一致)。
* @param {object} data - 一个视频的 view 接口数据。
* @returns {boolean}
*/
function isVideoTagNameBlacklisted(data) {
const checkTname = (tname) => {
if (!tname) return false;
if (tagNameBlacklist.includes(tname)) return true;
const mapped = getTagNameByV2(tname); // 若该名字是 V2 名,映射回主名再判断
if (mapped !== null && tagNameBlacklist.includes(mapped)) return true;
return false;
};
if (checkTname(data.tname)) return true;
if (checkTname(data.tname_v2)) return true;
if (data.tid_v2 !== undefined && data.tid_v2 !== null) {
const obj = getTagNameById(data.tid_v2);
if (obj) {
if (checkTname(obj.name)) return true;
if (obj.name_v2 && checkTname(obj.name_v2)) return true;
}
}
return false;
}
/**
* 判断视频是否为竖屏(与卡片屏蔽逻辑一致)。
* @param {object} data - 一个视频的 view 接口数据。
* @returns {boolean}
*/
function isVerticalVideo(data) {
if (data.dimension && data.dimension.width && data.dimension.height) {
const dimension = data.dimension.width / data.dimension.height;
return dimension < globalPluginConfig.verticalScaleThreshold;
}
return false;
}
/**
* 只依据 B 站 view 接口数据判断某 BV 是否“按分类标签 / 竖屏”被屏蔽(不依赖 DOM 是否已渲染标签组)。
* @param {string} bvid - 视频 BV。
* @returns {Promise<boolean>}
*/
async function isBlockedByTagOrVertical(bvid) {
const cfg = globalPluginConfig;
if (!cfg.flagTName && !cfg.flagVertical) return false;
if (!bvid) return false;
const data = await getBilibiliVideoApiData(bvid);
if (!data) return false;
if (cfg.flagTName && isVideoTagNameBlacklisted(data)) return true;
if (cfg.flagVertical && isVerticalVideo(data)) return true;
return false;
}
/**
* 判断“当前播放视频”是否被屏蔽,规则与卡片屏蔽完全一致:
* flagInfo(UP名/标题)、flagTName(分类标签)、flagVertical(竖屏)。
* 权威来源是 __INITIAL_STATE__ 的 videoData/related(带 owner.name):
* 用标题反查 UP 名最靠谱(自动连播卡片只有标题、没有 UP 名),再按 bvid 反查,最后 view 接口兜底。
* @param {{upName:string,title:string}} info - DOM 读取的信息。
* @param {string} bv - 当前 BV。
* @returns {Promise<boolean>}
*/
async function isPlayingVideoBlacklisted(info, bv) {
const cfg = globalPluginConfig;
let upName = info.upName;
let title = info.title;
let bvid = bv;
let resolved = false;
// 1) __INITIAL_STATE__ 按标题反查 UP 名 + bvid(自动连播卡片只有标题)
if (title) {
const byTitle = buildTitleInfoMapFromInitialState()[title];
if (byTitle && byTitle.upName) {
upName = byTitle.upName;
if (byTitle.bvid) bvid = byTitle.bvid;
resolved = true;
}
}
// 2) __INITIAL_STATE__ 按 bvid 反查
if (!resolved && bvid) {
const byBv = buildBvInfoMapFromInitialState()[bvid];
if (byBv && byBv.upName) {
upName = upName || byBv.upName;
title = title || byBv.title;
resolved = true;
}
}
// 3) 先用反查到的权威 UP名/标题做 UP名屏蔽(避免依赖已过期的 view 接口)
if (cfg.flagInfo && upName && isBlacklisted(upName, title)) {
return true;
}
// 4) 仅当开启了“标签/竖屏屏蔽”时,才拿 view 接口数据(含 tname/dimension)
if (cfg.flagTName || cfg.flagVertical) {
const data = bvid ? await getBilibiliVideoApiData(bvid) : null;
if (data) {
// 若还没拿到 UP名,用接口数据补一次
if (cfg.flagInfo && !upName) {
const dUpName = (data.owner && data.owner.name) || "";
if (dUpName && isBlacklisted(dUpName, data.title)) return true;
}
if (cfg.flagTName && isVideoTagNameBlacklisted(data)) return true;
if (cfg.flagVertical && isVerticalVideo(data)) return true;
}
}
return false;
}
/**
* 暂停当前播放。优先 <video>,其次 window.player。
*/
function pauseCurrentPlayback() {
const video = document.querySelector(
"#bilibili-player video, .bilibili-player video, video"
);
if (video && !video.paused) {
try {
video.pause();
return;
} catch (e) {
// 忽略,继续尝试 player
}
}
if (window.player && typeof window.player.pause === "function") {
try {
window.player.pause();
} catch (e) {
// 忽略
}
}
}
/**
* 取消自动连播:点击 B 站播放器结局面板里的“取消连播”按钮;若按钮不可见则暂停兜底。
* 当相关推荐全部被屏蔽时调用,比“暂停”更贴近 B 站原生语义。
*/
function cancelAutoplay() {
try {
const btns = document.querySelectorAll(
".bpx-player-ending-related-item-cancel"
);
for (const btn of btns) {
// 只点可见的取消连播按钮
if (btn.getBoundingClientRect().height > 0) {
btn.click();
console.log("[bilibili-blacklist] 相关推荐全部被屏蔽,已取消自动连播。");
return;
}
}
} catch (e) {
// 忽略,走下方暂停兜底
}
pauseCurrentPlayback();
console.log("[bilibili-blacklist] 相关推荐全部被屏蔽,已停止自动连播。");
}
/**
* 尝试让播放器不刷新地切换到指定 BV(best-effort,方法名不稳定需运行时确认)。
* @param {string} bvid
* @returns {boolean} 是否成功触发切换。
*/
function tryInPageSwitch(bvid) {
const player = window.player;
if (!player) return false;
const trySwitch = (method, arg) => {
if (typeof player[method] !== "function") return false;
const ret = player[method](arg);
return ret !== false;
};
const attempts = [
() => trySwitch("changeVideo", { bvid }),
() => trySwitch("switchVideo", { bvid }),
() => trySwitch("loadVideo", { bvid }),
() => trySwitch("changeVideo", bvid),
() => trySwitch("switchVideo", bvid),
];
for (const attempt of attempts) {
try {
if (attempt()) {
console.log(
`[bilibili-blacklist] 自动连播已切换到未屏蔽视频: ${bvid}`
);
return true;
}
} catch (e) {
// 该形态不适用,继续尝试下一种
}
}
return false;
}
/**
* 找到页面上指向指定 BV 的链接并点击它(不限右侧列表,覆盖相关推荐/接下来播放等)。
* @param {string} bvid
* @returns {boolean}
*/
function clickRecommendCardByBv(bvid) {
try {
const links = document.querySelectorAll("a[href]");
for (const link of links) {
const href = link.getAttribute("href") || "";
const m = href.match(/\/video\/(BV\w+)/);
if (m && m[1] === bvid) {
link.click();
console.log(
`[bilibili-blacklist] 自动连播已点击未屏蔽推荐卡片: ${bvid}`
);
return true;
}
}
return false;
} catch (e) {
return false;
}
}
/**
* 从当前页面“相关推荐 / 接下来播放”卡片里找第一条未被屏蔽的视频 BV(DOM 优先,最贴近屏幕内容)。
* 标签/竖屏用 B 站 view 接口(getBilibiliVideoApiData)精确判定,不依赖 DOM 是否已渲染标签组。
* @returns {Promise<string|null>}
*/
async function getFirstNonBlockedFromDom() {
const cfg = globalPluginConfig;
const cards = document.querySelectorAll(
".video-page-card-small, .bili-video-card"
);
for (const card of cards) {
// 跳过已被插件整体屏蔽(隐藏 / 卡比遮挡)的卡片
try {
const real = getRealVideoCardElement(card);
if (blockedVideoCards.has(real)) continue;
if (real && real.style.display === "none") continue;
if (real && real.querySelector("#bilibili-blacklist-kirby")) continue;
} catch (e) {
// 忽略,继续
}
const { upName, videoTitle } = getVideoCardInfo(card);
if (!upName || !videoTitle) continue;
// UP名/标题屏蔽
if (cfg.flagInfo && isBlacklisted(upName, videoTitle)) continue;
const bv = getLinkBvId(getCardHrefLink(card));
if (!bv) continue;
// 标签/竖屏屏蔽:用 view 接口精确判定
if (await isBlockedByTagOrVertical(bv)) continue;
return bv;
}
return null;
}
/**
* 用 B 站相关推荐接口取第一条未被屏蔽的视频 BV(DOM 找不到时兜底)。
* @param {string} curBv - 当前视频 BV。
* @returns {Promise<string|null>}
*/
async function getFirstNonBlockedFromApi(curBv) {
if (!curBv) return null;
try {
const res = await fetch(
`https://api.bilibili.com/x/web-interface/archive/related?bvid=${curBv}`
);
const json = await res.json();
if (json.code !== 0 || !Array.isArray(json.data)) return null;
for (const item of json.data) {
if (!item.bvid || item.bvid === curBv) continue;
const upName = (item.owner && item.owner.name) || "";
const title = item.title || "";
if (!upName) continue;
// UP名/标题屏蔽
if (globalPluginConfig.flagInfo && isBlacklisted(upName, title)) {
continue;
}
// 标签/竖屏屏蔽:用 view 接口精确判定
if (await isBlockedByTagOrVertical(item.bvid)) continue;
return item.bvid;
}
return null;
} catch (e) {
console.error("[bilibili-blacklist] 获取相关推荐失败:", e);
return null;
}
}
/**
* 读取 __INITIAL_STATE__.availableVideoList(B 站连播真正依据的有序“可播列表”)。
* @returns {Array<{bvid:string,title:string}>}
*/
function getAvailableVideoList() {
const state =
typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
return state && Array.isArray(state.availableVideoList)
? state.availableVideoList
: [];
}
/**
* 从 availableVideoList 里找“当前视频之后”的第一条未屏蔽视频 BV。
* 这是连播最权威的“下一个”顺序;当前视频为 index 0,其后紧跟 related 推荐。
* @param {string} curBv - 当前视频 BV。
* @param {Object<string,{upName:string,title:string}>} infoMap - bvid -> {upName,title}。
* @returns {string|null}
*/
async function getFirstNonBlockedFromAvailableList(curBv, infoMap) {
const list = getAvailableVideoList();
if (list.length === 0) return null;
let start = -1;
for (let i = 0; i < list.length; i++) {
if (list[i].bvid === curBv) {
start = i;
break;
}
}
// 当前视频不在列表里(可能已播到 SSR 列表之外),交给 DOM/API 兜底,避免误取已播视频
if (start < 0) return null;
for (let i = start + 1; i < list.length; i++) {
const item = list[i];
if (!item || !item.bvid) continue;
const rel = infoMap[item.bvid] || {};
if (!rel.upName) continue; // 拿不到 UP 名,跳过,靠后续 DOM/API 兜底
if (globalPluginConfig.flagInfo && isBlacklisted(rel.upName, rel.title)) {
continue;
}
// 标签/竖屏屏蔽:用 view 接口精确判定
if (await isBlockedByTagOrVertical(item.bvid)) continue;
return item.bvid;
}
return null;
}
/**
* 取第一条未屏蔽的视频 BV。
* 优先级:页面 DOM 卡片(实时“接下来播放/相关推荐”,随切集更新)-> availableVideoList ->
* 相关推荐 API。
* @param {string} curBv - 当前播放视频 BV。
* @returns {Promise<string|null>}
*/
async function getFirstNonBlockedBv(curBv) {
// DOM 卡片是实时的,切集后会跟着更新;优先用它,避免用到页面加载时的旧缓存
const fromDom = await getFirstNonBlockedFromDom();
if (fromDom) return fromDom;
const infoMap = buildBvInfoMapFromInitialState();
const fromAvailableList = await getFirstNonBlockedFromAvailableList(
curBv,
infoMap
);
if (fromAvailableList) return fromAvailableList;
return await getFirstNonBlockedFromApi(curBv);
}
/**
* 处理“当前播放视频被屏蔽”的逻辑(按配置走三态)。
* @param {{upName:string,title:string}} info - 当前播放视频信息。
* @param {string} bv - 当前播放视频 BV。
*/
async function handleBlockedVideo(info, bv) {
const mode = globalPluginConfig.flagSkipBlockedAutoplay;
if (mode === "off") return; // 不处理:保留 B 站默认行为
const blocked = await isPlayingVideoBlacklisted(info, bv);
if (!blocked) return;
if (mode === "stop") {
pauseCurrentPlayback();
return;
}
// mode === "skip"
const nextBv = await getFirstNonBlockedBv(bv);
if (nextBv && nextBv !== bv) {
lastHandledBv = nextBv; // 记录目标,避免反复处理
if (tryInPageSwitch(nextBv)) {
return;
}
if (clickRecommendCardByBv(nextBv)) {
return;
}
location.href = `/video/${nextBv}`;
} else if (!nextBv) {
// 相关推荐全部被屏蔽:取消自动连播(点击“取消连播”按钮),按钮不可见则暂停兜底
cancelAutoplay();
}
}
/**
* 初始化自动连播监听。
* 每 700ms 读一次“当前播放视频”的信息(标题/UP名/BV),一旦发生变化(无论是切集还是
* 加载了新视频),就检查其是否被屏蔽,并按配置选择“跳过/停止/不处理”。
*/
function initAutoplaySkip() {
if (autoplayWatchTimer) return; // 防止重复初始化
const check = async () => {
const bv = getCurrentBv();
if (!bv) {
// 不在视频播放页,重置基准
lastSignature = "";
return;
}
const info = getPlayingVideoInfo();
const signature = `${info.upName}||${info.title}||${bv}`;
if (signature === lastSignature) return; // 视频没变
lastSignature = signature;
if (isHandling) return;
isHandling = true;
try {
await handleBlockedVideo(info, bv);
} catch (e) {
console.error("[bilibili-blacklist] 自动连播处理出错:", e);
} finally {
isHandling = false;
}
};
autoplayWatchTimer = setInterval(check, 700);
window.addEventListener("popstate", check);
// 捕获阶段监听播放事件(video 事件不冒泡,用 capture 才能捕捉),
// 切集开始播放新视频时会触发一次检测,比轮询更快、更稳。
const onPlayback = () => check();
["playing", "loadstart", "loadedmetadata", "load", "emptied"].forEach(
(evt) => document.addEventListener(evt, onPlayback, true)
);
}
/*
* Bilibili-BlackList -- Bilibili UP屏蔽插件
* 脚本大部分代码由AI生成,作者一点都不懂JavaScript,出现bug请联系Gemini / ChatGPT / DeepSeek
* this script is mainly generated by AI, the author doesn't know JavaScript at all, if there are bugs, please contact Gemini / ChatGPT / DeepSeek
* 感谢你的使用
* Thank you for using this script
*
* 本段注释为VS code 自动生成 this is a comment generated by VS code
*/
// 启动脚本
initializeScript();
})();