// ==UserScript==
// @name Bilibili-BlackList
// @namespace https://github.com/HeavenTTT/bilibili-blacklist
// @version 1.1.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
// @require https://update.greasyfork.org/scripts/533940/Bilibili-BlackList.user.js
// @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 exactBlacklist = GM_getValue("exactBlacklist", [
"绝区零",
"崩坏星穹铁道",
"崩坏3",
"原神",
"米哈游miHoYo",
]);
// 默认正则匹配黑名单
let regexBlacklist = GM_getValue("regexBlacklist", [
"王者荣耀",
"和平精英",
"PUBG",
"绝地求生",
"吃鸡",
]);
// 标签屏蔽黑名单
let tNameBlacklist = GM_getValue("tNameBlacklist", ["手机游戏"]);
// let globalConfig = {
// flagInfo: true,
// flagAD: true,
// flagTName : true,
// flagCM: true,
// processQueueInterval: 500, // 单位 ms
// };
let globalConfig = GM_getValue("globalConfig", {
flagInfo: true,
flagAD: true,
flagTName: true,
flagCM: true,
processQueueInterval: 500, // 单位 ms
});
// 保存黑名单到存储
function saveBlacklists() {
GM_setValue("exactBlacklist", exactBlacklist);
GM_setValue("regexBlacklist", regexBlacklist);
GM_setValue("tNameBlacklist", tNameBlacklist);
}
function saveGlobalConfig() {
GM_setValue("globalConfig", globalConfig);
}
//开发用日志,bug:运行该函数后,脚本才能正常工作,原因未知,本插件依赖此bug运行,请勿删除任何引用
function Devlog(...args) {
if (GM_info.script.version === "Dev") console.log("[Devlog]", ...args);
}
//#region 核心功能 - 屏蔽视频卡片
let isShowAll = false; // 是否显示全部视频卡片
let isBlocking = false; // 是否正在执行屏蔽操作
let lastBlockTime = 0; // 上次执行屏蔽的时间戳
let blockedCards = new Set(); // 存储已屏蔽的视频卡片元素
let processedCards = new WeakSet(); // 记录已处理过的卡片(避免重复处理)
//给卡片添加屏蔽按钮
function cardAddBlockcContainer(upName, card) {
if (!card.querySelector("bilibili-blacklist-block-container")) {
const container = document.createElement("div");
container.classList.add("bilibili-blacklist-block-container");
if (!card.querySelector(".bilibili-blacklist-block-btn")) {
// 创建屏蔽按钮
if (isVideoPage()) {
// 如果是视频页面
if (isInit) {
const blockButton = createBlockButton(upName, card);
card.querySelector(".card-box").style.position = "relative";
card.querySelector(".card-box").appendChild(container);
container.appendChild(blockButton);
}
} else if (isCategoryPage()) {
// 如果是分类页面
const blockButton = createBlockButton(upName, card); // 创建屏蔽按钮
card.querySelector(".bili-video-card").appendChild(container); // 将按钮添加到卡片中信息中
container.appendChild(blockButton); // 将按钮添加到容器中
} else {
const blockButton = createBlockButton(upName, card); // 创建屏蔽按钮
card.appendChild(container); // 将按钮添加到卡片中
container.appendChild(blockButton); // 将按钮添加到容器中
}
}
}
}
//隐藏卡片
function hideCard(card) {
if (isSearchPage()) {
// 如果是搜索页面 -> 隐藏父元素
card = card.parentElement; // 获取父元素
}
if (isMainPage()) {
// 如果是主页
if (card.parentElement.classList.contains("feed-card")) {
// 如果父元素是feed-card
card = card.parentElement; // 获取父元素
}
}
if (!blockedCards.has(card)) {
blockedCards.add(card); // 将卡片添加到已屏蔽列表
}
if (!isShowAll) {
card.style.display = "none"; // 隐藏卡片
}
}
/// 查找所有视频卡片
function querySelectorAllVideoCard(selector) {
return document.querySelectorAll(selector);
}
/// 屏蔽视频卡片
function BlockCard() {
const now = Date.now();
if (isBlocking || now - lastBlockTime < 1000) {
return;
}
isBlocking = true;
lastBlockTime = now;
try {
let cards = null;
if (isMainPage()) {
cards = querySelectorAllVideoCard(".bili-video-card");
} else if (isVideoPage()) {
cards = querySelectorAllVideoCard(".video-page-card-small");
} else if (isCategoryPage()) {
cards = querySelectorAllVideoCard(".feed-card");
} else if (isSearchPage()) {
cards = querySelectorAllVideoCard(".bili-video-card");
} else return; // 如果不是视频页面,则不执行屏蔽操作
cards.forEach((card) => {
addButtontTNameQueue(card);
if (processedCards.has(card)) {
return; // 如果卡片已经处理过,则跳过
}
// 获取视频信息
const { upName, title } = GetVideoInfo(card);
if (upName && title) {
processedCards.add(card); // 将卡片标记为已处理
cardAddBlockcContainer(upName, card); // 添加屏蔽按钮
// 检查是否在黑名单中
if (isBlacklisted(upName, title) && globalConfig.flagInfo) {
// 如果在黑名单中,则隐藏卡片
hideCard(card);
}
if (isCardBlacklistTName(card) && globalConfig.flagTName) {
hideCard(card);
}
} else {
//console.warn("未找到UP主名称或视频标题,跳过屏蔽:", card);
}
});
refreshBlockCountDisplay();
FixedMainPage(); // 修正主页的错位问题
} finally {
isBlocking = false; // 重置屏蔽状态
}
}
//修正主页的错位问题
function FixedMainPage() {
if (!isMainPage()) return; // 仅在主页执行
const container = document.querySelector(
".recommended-container_floor-aside .container" // 推荐视频容器
);
// 检查父元素是否存在
if (container) {
const children = container.children; // 这是一个 HTMLCollection
let visableindex = 0; // 可见子元素的索引
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.style.display !== "none") {
if (visableindex <= 6) {
child.style.marginTop = "0px";
} else if (visableindex < 12) {
child.style.marginTop = "24px";
} else {
break; // 如果可见子元素超过10个,则停止处理
}
visableindex++; // 统计可见子元素的数量
}
}
}
}
// 更新屏蔽计数显示
function refreshBlockCountDisplay() {
if (blockCountDiv) {
blockCountDiv.textContent = `${blockedCards.size}`; // 更新右侧导航栏的屏蔽计数
}
// 更新面板标题(如果面板已打开)
//const panel = document.getElementById("bilibili-blacklist-panel");
if (isBlacklistPanelCreated() && blockTitle) {
blockTitle.textContent = `已屏蔽视频 (${blockedCards.size})`; // 更新面板标题
}
}
// 暂时取消屏蔽/恢复屏蔽功能
function toggleShowAll() {
isShowAll = !isShowAll;
blockedCards.forEach((card) => {
card.style.display = isShowAll ? "block" : "none";
});
btnTempUnblock.textContent = isShowAll ? "恢复屏蔽" : "取消屏蔽";
btnTempUnblock.style.background = isShowAll ? "#dddddd" : "#fb7299";
// 不需要更新blockCount,因为总数没有变化
}
const selectorUpName = [
".bili-video-card__info--author", // 主页
".bili-video-card__author", // 分类页面--> span title
".name", // 播放页面
];
const selectorTitle = [
".bili-video-card__info--tit", // 主页
".bili-video-card__title", // 分类页面--> span title
".title", // 播放页面
];
//获取视频信息 -UP主名称 -视频标题
function GetVideoInfo(card) {
let upName = "";
let title = "";
if (card.style.display === "none") return { upName, title }; // 如果卡片已被隐藏,则直接返回空信息
const upNameElement = card.querySelectorAll(selectorUpName.join(", ")); // 使用逗号分隔的选择器
if (upNameElement.length > 0) {
upName = upNameElement[0].textContent.trim(); // 获取第一个匹配到的元素的内容,并去除首尾空格
//处理分类页面的UP主名称
if (isCategoryPage()) {
upName = upName.split(" · ")[0].trim();
}
}
const titleElement = card.querySelectorAll(selectorTitle.join(", ")); // 使用逗号分隔的选择器
if (titleElement.length > 0) {
title = titleElement[0].textContent.trim(); // 获取第一个匹配到的元素的内容,并去除首尾空格
}
return { upName, title };
}
function isBlacklisted(upName, title) {
// 精确匹配黑名单(无视大小写)
// 将 upName 转换为小写,然后与黑名单中的每个项的小写进行比较
const lowerCaseUpName = upName.toLowerCase();
if (exactBlacklist.some((item) => item.toLowerCase() === lowerCaseUpName)) {
return true;
}
// 正则匹配黑名单(无视大小写)
// 为正则表达式添加 'i' 标志,表示忽略大小写
if (regexBlacklist.some((regex) => new RegExp(regex, "i").test(upName))) {
return true;
}
if (regexBlacklist.some((regex) => new RegExp(regex, "i").test(title))) {
return true;
}
return false; // 不在黑名单中
}
/// 添加UP主到精确黑名单并刷新页面
function addToExactBlacklist(upName, cardElement = null) {
Devlog("添加黑名单:", upName);
try {
if (!upName) return;
Devlog("添加黑名单:", upName);
if (!exactBlacklist.includes(upName)) {
exactBlacklist.push(upName);
Devlog("添加黑名单成功:", exactBlacklist.length);
saveBlacklists();
refreshAllTabs();
if (cardElement) {
hideCard(cardElement); // 隐藏当前卡片
}
}
} catch (e) {
console.error("添加黑名单出错:", e);
}
}
function removeFromExactBlacklist(upName) {
try {
if (exactBlacklist.includes(upName)) {
const index = exactBlacklist.indexOf(upName);
exactBlacklist.splice(index, 1);
saveBlacklists();
refreshAllTabs();
}
} catch (e) {
console.error("移除黑名单出错:", e);
} finally {
//BlockCard();
}
}
function addToTNameBlacklist(tname, cardElement = null) {
Devlog("添加标签黑名单:", tname);
try {
if (!tname) return;
Devlog("添加标签黑名单:", tname);
if (!tNameBlacklist.includes(tname)) {
tNameBlacklist.push(tname);
Devlog("添加标签黑名单成功:", tNameBlacklist.length);
saveBlacklists();
refreshAllTabs();
if (cardElement) {
hideCard(cardElement); //隐藏卡片
}
}
} catch (e) {
console.error("添加标签黑名单出错:", e);
}
}
function removeFromTNameBlacklist(tname) {
try {
if (tNameBlacklist.includes(tname)) {
const index = tNameBlacklist.indexOf(tname);
tNameBlacklist.splice(index, 1);
saveBlacklists();
updateTNameList();
}
} catch (e) {
console.error("移除标签黑名单出错:", e);
}
}
//#endregion
//#region Bv号以及视频信息
let cardSequenceGetJson = new Set(); // 存储卡片队列
let isProcessingCardQueue = false; // 是否正在处理队列
let isPageActive = true; // 页面是否可见
function getCardBv(card) {
const bvElement = card.querySelector("a");
if (!bvElement) {
return null;
}
try {
const link = bvElement.getAttribute("href");
if (!link) {
return null;
} else {
//判断链接是不是cm.bilili
if (link.match(/cm.bilibili.com/) && globalConfig.flagCM) {
hideCard(card);
//Devlog("软广链接,已屏蔽");
return null;
}
const bv = link.match(/BV\w+/);
return bv[0];
}
} catch (e) {
return null;
}
}
async function getBilibiliVideoAPI(bvid) {
if (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) {
//Devlog("data1:", json.data.tname);
//Devlog("data2:", json.data.tname_v2);
return json.data;
} else {
//console.error("获取视频信息失败:", bvid);
return null;
}
} catch (error) {
console.error("请求失败:", error);
}
}
function isCardBlacklistTName(card) {
const tnameGroup = card.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 (tNameBlacklist.includes(tname)) {
return true;
}
}
}
return false;
}
// 队列处理函数
let cardSequenceGetJsonDone = new Set(); // 存储卡片队列
async function processCardTNameQueue() {
if (isProcessingCardQueue) return;
isProcessingCardQueue = true;
while (cardSequenceGetJson.size > 0) {
// 页面不活动时暂停处理
if (!isPageActive) {
//Devlog("页面不活动,暂停队列处理...");
await sleep(1000); // 每秒检查一次
continue; // 不处理当前卡片,重新判断
}
const iterator = cardSequenceGetJson.values();
const card = iterator.next().value;
cardSequenceGetJson.delete(card);
if (!card) continue;
if (cardSequenceGetJsonDone.has(card)) {
//("卡片已处理过" + card.textContent);
continue;
}
const bv = getCardBv(card);
if (!bv) continue;
const container = card.querySelector(
".bilibili-blacklist-block-container"
);
if (!container) {
//Devlog("未找到容器" + card.textContent);
continue;
}
cardSequenceGetJsonDone.add(card);
const data = await getBilibiliVideoAPI(bv);
if (!data) {
// Devlog("未找到数据" + card.textContent);
cardSequenceGetJsonDone.remove(card);
continue;
}
// 如果 card 已经处理过 tname group,跳过
// 最终确认
if (!card.querySelector(".bilibili-blacklist-tname-group")) {
// 创建 tname group
const tnameGroup = document.createElement("div");
tnameGroup.className = "bilibili-blacklist-tname-group";
let hasTname = false;
// 添加一级 tname
if (data.tname) {
//Devlog(`处理 BV: ${bv} - 分类: ${data.tname}`);
const btn = createTNameBlockButton(data.tname, card);
tnameGroup.appendChild(btn);
hasTname = true;
}
// 添加二级 tname_v2
if (data.tname_v2) {
//Devlog(`处理 BV: ${bv} - 分类2: ${data.tname_v2}`);
const tnameElement = createTNameBlockButton(data.tname_v2, card);
tnameGroup.appendChild(tnameElement);
hasTname = true;
}
// 只有有 tname 才 append group,避免插入空容器
if (hasTname) {
container.appendChild(tnameGroup);
}
}
await sleep(globalConfig.processQueueInterval || 100);
}
isProcessingCardQueue = false;
}
function addButtontTNameQueue(card) {
if (!globalConfig.flagTName) return;
const bv = getCardBv(card);
if (!bv) return;
// 检查是否已在处理中或已有标签组
if (card.querySelector(".bilibili-blacklist-tname-group")) {
return;
}
// 检查是否已在队列中
if (cardSequenceGetJson.has(card)) {
return;
}
cardSequenceGetJson.add(card);
if (!isProcessingCardQueue) {
processCardTNameQueue();
}
}
//
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 页面可见性监听
document.addEventListener("visibilitychange", () => {
isPageActive = !document.hidden;
//Devlog(`页面可见状态改变: ${isPageActive ? "活动" : "隐藏"}`);
});
// 窗口焦点监听
window.addEventListener("focus", () => {
isPageActive = true;
//Devlog("窗口获得焦点");
});
window.addEventListener("blur", () => {
isPageActive = false;
//Devlog("窗口失去焦点");
});
//#endregion
//#region 页面修改
//创建屏蔽按钮(悬停在视频卡片上时显示)
function createBlockButton(upName, cardElement) {
const btn = document.createElement("div");
btn.className = "bilibili-blacklist-block-btn";
btn.innerHTML = "屏蔽";
btn.title = `屏蔽: ${upName}`;
// 屏蔽按钮样式
// 点击时添加到黑名单
btn.addEventListener("click", (e) => {
Devlog("屏蔽: " + upName);
e.stopPropagation(); // 防止事件冒泡
Devlog("屏蔽: " + upName);
addToExactBlacklist(upName, cardElement); // 使用公共函数
});
return btn;
}
function createTNameBlockButton(tName, cardElement) {
const btn = document.createElement("span");
btn.className = "bilibili-blacklist-tname";
btn.innerHTML = `${tName}`;
btn.title = `屏蔽: ${tName}`;
// 屏蔽按钮样式
// 点击时添加到黑名单
btn.addEventListener("click", (e) => {
Devlog("屏蔽: " + tName);
e.stopPropagation(); // 防止事件冒泡
Devlog("屏蔽: " + tName);
addToTNameBlacklist(tName, cardElement);
});
return btn;
}
// 在右侧导航栏添加黑名单管理按钮
let blockCountDiv = null;
function addBlacklistManagerButton() {
if (isVideoPage()) {
return;
}
const rightEntry = document.querySelector(".right-entry");
if (!rightEntry) {
console.warn("bilibili-blacklist: 未找到右侧导航栏");
return;
} else if (!rightEntry.querySelector("#bilibili-blacklist-manager")) {
//else if(rightEntry.getElementById('bilibili-blacklist-manager')){
const li = document.createElement("li");
li.id = "bilibili-blacklist-manager";
li.style.cursor = "pointer";
li.className = "v-popover-wrap";
const btn = document.createElement("div");
btn.className = "right-entry-item";
btn.style.display = "flex";
btn.style.flexDirection = "column";
btn.style.alignItems = "center";
btn.style.justifyContent = "center";
// 可爱的卡比图标SVG
const icon = document.createElement("div");
icon.className = "right-entry__outside";
icon.innerHTML = getKirbySVG();
//icon.style.color = '#fb7299'; // B站粉色
icon.style.marginBottom = "-5px";
blockCountDiv = document.createElement("span");
//const text = document.createElement('div');
blockCountDiv.textContent = `0`;
btn.appendChild(icon);
btn.appendChild(blockCountDiv);
li.appendChild(btn);
// 在导航中插入按钮
if (rightEntry.children.length > 1) {
rightEntry.insertBefore(li, rightEntry.children[1]);
} else {
rightEntry.appendChild(li);
}
// 点击按钮时显示面板
li.addEventListener("click", () => {
if (managerPanel.style.display === "none") {
managerPanel.style.display = "flex";
//updateBlockCountDisplay(); // 更新屏蔽计数显示
} else {
managerPanel.style.display = "none";
}
});
}
}
// 创建黑名单管理面板
let btnTempUnblock;
let managerPanel;
let exactList; //精确匹配列表
let regexList; //正则匹配列表
let tNameList; //tname匹配列表
let configList; //配置列表
let blockTitle;
// 工具函数:创建按钮
function createButton(text, bgColor, onClick) {
const button = document.createElement("button");
button.textContent = text;
button.style.padding = "4px 8px";
button.style.background = bgColor;
button.style.color = "#fff";
button.style.border = "none";
button.style.borderRadius = "4px";
button.style.cursor = "pointer";
button.addEventListener("click", onClick);
return button;
}
// 工具函数:创建列表项
function createListItem(contentText, onRemoveClick, isRegex = false) {
const item = document.createElement("li");
item.style.display = "flex";
item.style.justifyContent = "space-between";
item.style.alignItems = "center";
item.style.padding = "8px 0";
item.style.borderBottom = "1px solid #f1f2f3";
const content = document.createElement("span");
content.textContent = contentText;
content.style.flex = "1";
if (isRegex) {
content.style.fontFamily = "monospace";
}
const removeBtn = createButton("移除", "#f56c6c", onRemoveClick);
item.appendChild(content);
item.appendChild(removeBtn);
return item;
}
// 更新精确匹配列表
function refreshExactList() {
Devlog("refreshExactList1");
if (!exactList) {
if (!isBlacklistPanelCreated()) return;
exactList = document.querySelector("#bilibili-blacklist-exact-list");
if (!exactList) return;
}
Devlog("refreshExactList2");
exactList.innerHTML = "";
exactBlacklist.forEach((upName) => {
const item = createListItem(upName, () => {
removeFromExactBlacklist(upName);
//BlockCard(); //更新屏蔽卡片
});
exactList.appendChild(item);
});
Devlog("refreshExactList length:" + exactBlacklist.length);
Array.from(exactList.children)
.reverse()
.forEach((item) => exactList.appendChild(item));
if (exactBlacklist.length === 0) {
const empty = document.createElement("div");
empty.textContent = "暂无精确匹配屏蔽UP主";
empty.style.textAlign = "center";
empty.style.padding = "16px";
empty.style.color = "#999";
exactList.appendChild(empty);
}
}
// 更新正则匹配列表
function refreshRegexList() {
if (!regexList) return;
regexList.innerHTML = "";
regexBlacklist.forEach((regex, index) => {
const item = createListItem(
regex,
() => {
regexBlacklist.splice(index, 1);
saveBlacklists();
refreshRegexList();
//BlockCard();
},
true
);
regexList.appendChild(item);
});
Array.from(regexList.children)
.reverse()
.forEach((item) => regexList.appendChild(item));
if (regexBlacklist.length === 0) {
const empty = document.createElement("div");
empty.textContent = "暂无正则匹配屏蔽规则";
empty.style.textAlign = "center";
empty.style.padding = "16px";
empty.style.color = "#999";
regexList.appendChild(empty);
}
}
// 更新tname匹配列表
function refreshTagNameList() {
Devlog("refreshTagNameList1");
if (!tNameList) {
if (!isBlacklistPanelCreated()) return;
tNameList = document.querySelector("#bilibili-blacklist-exact-list");
if (!tNameList) return;
}
Devlog("refreshTagNameList2");
tNameList.innerHTML = "";
tNameBlacklist.forEach((tName) => {
const item = createListItem(tName, () => {
removeFromTNameBlacklist(tName);
});
tNameList.appendChild(item);
});
Devlog("refreshExactList length:" + tNameBlacklist.length);
Array.from(tNameList.children)
.reverse()
.forEach((item) => tNameList.appendChild(item));
if (tNameBlacklist.length === 0) {
const empty = document.createElement("div");
empty.textContent = "暂无标签屏蔽规则";
empty.style.textAlign = "center";
empty.style.padding = "16px";
empty.style.color = "#999";
tNameList.appendChild(empty);
}
}
// 更新配置列表
function refreshConfigSettings() {
if (!configList) return;
// 清空 configContent
configList.innerHTML = "";
//开关
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginBottom = "8px";
container.style.gap = "8px";
container.style.margin = "20px 0";
const label = document.createElement("span");
label.textContent = "临时开关";
label.style.flex = "1";
btnTempUnblock = document.createElement("button");
btnTempUnblock.textContent = isShowAll ? "恢复屏蔽" : "取消屏蔽";
btnTempUnblock.style.background = isShowAll ? "#dddddd" : "#fb7299";
btnTempUnblock.style.padding = "6px 12px";
btnTempUnblock.style.border = "none";
btnTempUnblock.style.cursor = "pointer";
btnTempUnblock.style.color = "#fff";
btnTempUnblock.addEventListener("click", toggleShowAll);
container.appendChild(label);
container.appendChild(btnTempUnblock);
configList.appendChild(container);
// 标题
const title = document.createElement("h4");
title.textContent = "全局配置开关(部分功能刷新后生效)";
title.style.fontWeight = "bold";
title.style.marginBottom = "12px";
configList.appendChild(title);
// 工具函数:创建一个开关按钮
function createToggleButton(labelText, configKey, title = null) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginBottom = "8px";
container.style.gap = "8px";
container.title = title;
const label = document.createElement("span");
label.textContent = labelText;
label.style.flex = "1";
const button = document.createElement("button");
button.style.padding = "6px 12px";
button.style.border = "none";
button.style.borderRadius = "4px";
button.style.cursor = "pointer";
button.style.color = "#fff";
function refreshButtonAppearance() {
button.textContent = globalConfig[configKey] ? "开启" : "关闭";
button.style.backgroundColor = globalConfig[configKey]
? "#fb7299"
: "#909399";
}
button.addEventListener("click", () => {
globalConfig[configKey] = !globalConfig[configKey];
refreshButtonAppearance();
saveGlobalConfig(); // 你可以实现此函数,将globalConfig存储到localStorage或其他
});
refreshButtonAppearance();
container.appendChild(label);
container.appendChild(button);
return container;
}
// 创建3个开关
configList.appendChild(
createToggleButton("屏蔽标题/Up主名", "flagInfo", "屏蔽标题/Up主名")
);
configList.appendChild(
createToggleButton("屏蔽分类标签", "flagTName", "通过请求API获取分类标签")
);
configList.appendChild(
createToggleButton("屏蔽主页推荐", "flagAD", "直播/广告/分区推送")
);
configList.appendChild(
createToggleButton("屏蔽主页视频软广", "flagCM", "cm.bilibili.com软广")
);
// 处理队列请求间隔
const intervalContainer = document.createElement("div");
intervalContainer.style.display = "flex";
intervalContainer.style.alignItems = "center";
intervalContainer.style.marginTop = "16px";
intervalContainer.style.gap = "8px";
intervalContainer.title =
"请求API间隔时间,间隔时间越长,屏蔽越快,请求频繁可以会被临时ban,建议值 100ms";
const intervalLabel = document.createElement("span");
intervalLabel.textContent = "视频分类-处理队列请求间隔 (ms):";
intervalLabel.style.flex = "1";
const intervalInput = document.createElement("input");
intervalInput.type = "number";
intervalInput.min = "0";
intervalInput.value = globalConfig.processQueueInterval;
intervalInput.style.width = "100px";
intervalInput.style.padding = "6px";
intervalInput.style.border = "1px solid #ddd";
intervalInput.style.borderRadius = "4px";
const saveIntervalBtn = document.createElement("button");
saveIntervalBtn.textContent = "保存";
saveIntervalBtn.style.padding = "6px 12px";
saveIntervalBtn.style.backgroundColor = "#fb7299";
saveIntervalBtn.style.color = "#fff";
saveIntervalBtn.style.border = "none";
saveIntervalBtn.style.borderRadius = "4px";
saveIntervalBtn.style.cursor = "pointer";
saveIntervalBtn.addEventListener("click", () => {
const val = parseInt(intervalInput.value, 10);
if (!isNaN(val) && val >= 0) {
globalConfig.processQueueInterval = val;
saveGlobalConfig(); // 保存配置
//alert("处理队列请求间隔已保存!");
} else {
alert("请输入有效的非负数字!");
}
});
intervalContainer.appendChild(intervalLabel);
intervalContainer.appendChild(intervalInput);
intervalContainer.appendChild(saveIntervalBtn);
configList.appendChild(intervalContainer);
return configList;
}
function refreshAllTabs() {
refreshExactList();
refreshRegexList();
refreshTagNameList();
refreshConfigSettings();
}
function isBlacklistPanelCreated() {
return document.querySelector("#bilibili-blacklist-panel") ? true : false;
}
// 创建黑名单面板
function createBlacklistPanel() {
Devlog(isBlacklistPanelCreated());
if (isBlacklistPanelCreated()) return;
managerPanel = document.createElement("div");
managerPanel.id = "bilibili-blacklist-panel";
managerPanel.style.position = "fixed";
managerPanel.style.top = "50%";
managerPanel.style.left = "50%";
managerPanel.style.transform = "translate(-50%, -50%)";
managerPanel.style.width = "500px";
managerPanel.style.maxHeight = "80vh";
managerPanel.style.backgroundColor = "#fff";
managerPanel.style.borderRadius = "8px";
managerPanel.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
managerPanel.style.zIndex = "99999";
managerPanel.style.overflow = "hidden";
managerPanel.style.display = "none";
managerPanel.style.flexDirection = "column";
const tabContainer = document.createElement("div");
tabContainer.style.display = "flex";
tabContainer.style.borderBottom = "1px solid #f1f2f3";
// 内容区
const exactContent = document.createElement("div");
exactContent.style.padding = "16px";
exactContent.style.overflowY = "auto";
exactContent.style.flex = "1";
exactContent.style.display = "block";
const regexContent = document.createElement("div");
regexContent.style.padding = "16px";
regexContent.style.overflowY = "auto";
regexContent.style.flex = "1";
regexContent.style.display = "none";
const tnameContent = document.createElement("div");
tnameContent.style.padding = "16px";
tnameContent.style.overflowY = "auto";
tnameContent.style.flex = "1";
tnameContent.style.display = "none";
const configContent = document.createElement("div");
configContent.style.padding = "16px";
configContent.style.overflowY = "auto";
configContent.style.flex = "1";
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.textContent = tabData.name;
tab.style.padding = "12px 16px";
tab.style.cursor = "pointer";
tab.style.fontWeight = "500";
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);
});
// Header 区域
const header = document.createElement("div");
header.style.padding = "16px";
header.style.borderBottom = "1px solid #f1f2f3";
header.style.display = "flex";
header.style.justifyContent = "space-between";
header.style.alignItems = "center";
blockTitle = document.createElement("h3");
blockTitle.style.margin = "0";
blockTitle.style.fontSize = "16px";
blockTitle.style.fontWeight = "500";
const closeBtn = document.createElement("button");
closeBtn.textContent = "×";
closeBtn.style.background = "none";
closeBtn.style.border = "none";
closeBtn.style.fontSize = "20px";
closeBtn.style.cursor = "pointer";
closeBtn.style.padding = "0 8px";
closeBtn.addEventListener("click", () => {
managerPanel.style.display = "none";
});
header.appendChild(blockTitle);
header.appendChild(closeBtn);
const contentContainer = document.createElement("div");
contentContainer.style.display = "flex";
contentContainer.style.flexDirection = "column";
contentContainer.style.flex = "1";
contentContainer.style.overflow = "hidden";
// 精确匹配输入框
const addExactContainer = document.createElement("div");
addExactContainer.style.display = "flex";
addExactContainer.style.marginBottom = "16px";
addExactContainer.style.gap = "8px";
const exactInput = document.createElement("input");
exactInput.type = "text";
exactInput.placeholder = "输入要屏蔽的UP主名称";
exactInput.style.flex = "1";
exactInput.style.padding = "8px";
exactInput.style.border = "1px solid #ddd";
exactInput.style.borderRadius = "4px";
const addExactBtn = document.createElement("button");
addExactBtn.textContent = "添加";
addExactBtn.style.padding = "8px 16px";
addExactBtn.style.background = "#fb7299";
addExactBtn.style.color = "#fff";
addExactBtn.style.border = "none";
addExactBtn.style.borderRadius = "4px";
addExactBtn.style.cursor = "pointer";
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.style.display = "flex";
addRegexContainer.style.marginBottom = "16px";
addRegexContainer.style.gap = "8px";
const regexInput = document.createElement("input");
regexInput.type = "text";
regexInput.placeholder = "输入正则表达式 (如: 小小.*Official)";
regexInput.style.flex = "1";
regexInput.style.padding = "8px";
regexInput.style.border = "1px solid #ddd";
regexInput.style.borderRadius = "4px";
const addRegexBtn = document.createElement("button");
addRegexBtn.textContent = "添加";
addRegexBtn.style.padding = "8px 16px";
addRegexBtn.style.background = "#fb7299";
addRegexBtn.style.color = "#fff";
addRegexBtn.style.border = "none";
addRegexBtn.style.borderRadius = "4px";
addRegexBtn.style.cursor = "pointer";
addRegexBtn.addEventListener("click", () => {
const regex = regexInput.value.trim();
if (regex && !regexBlacklist.includes(regex)) {
try {
new RegExp(regex);
regexBlacklist.push(regex);
saveBlacklists();
regexInput.value = "";
refreshRegexList();
//BlockCard();
} catch (e) {
alert("无效的正则表达式: " + e.message);
}
}
});
addRegexContainer.appendChild(regexInput);
addRegexContainer.appendChild(addRegexBtn);
regexContent.appendChild(addRegexContainer);
// 精确匹配列表
exactList = document.createElement("ul");
exactList.id = "bilibili-blacklist-exact-list";
exactList.style.listStyle = "none";
exactList.style.padding = "0";
exactList.style.margin = "0";
// 正则匹配列表
regexList = document.createElement("ul");
exactList.id = "bilibili-blacklist-regex-list";
regexList.style.listStyle = "none";
regexList.style.padding = "0";
regexList.style.margin = "0";
// tname列表
tNameList = document.createElement("ul");
exactList.id = "bilibili-blacklist-tname-list";
tNameList.style.listStyle = "none";
tNameList.style.padding = "0";
tNameList.style.margin = "0";
// 配置列表
configList = document.createElement("ul");
exactList.id = "bilibili-blacklist-config-list";
configList.style.listStyle = "none";
configList.style.padding = "0";
configList.style.margin = "0";
// 更新列表
refreshAllTabs();
exactContent.appendChild(exactList);
regexContent.appendChild(regexList);
tnameContent.appendChild(tNameList);
configContent.appendChild(configList);
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;
}
// 添加全局样式
GM_addStyle(`
.bilibili-blacklist-block-container {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 20px;
margin-top: 5px;
padding: 0 5px;
font-size: 12px;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 3px;
z-index: 9999;
pointer-events: none;
text-align:center;
}
.bili-video-card:hover .bilibili-blacklist-block-container,
.card-box:hover .bilibili-blacklist-block-container {
display: flex !important;
pointer-events: none;
}
.card-box .bilibili-blacklist-block-container
{
flex-direction: column;
align-items: flex-start;
height: 100%;
}
.card-box .bilibili-blacklist-tname-group
{
flex-direction: column;
align-items: flex-end;
bottom: 0;
}
.card-box .bilibili-blacklist-tname-group .bilibili-blacklist-tname
{
background-color:rgba(255, 255, 255, 0.87);
color: #9499A0;
border: 1px solid #9499A0;
}
.bilibili-blacklist-block-btn {
position: static;
display: flex;
width: 40px;
height: 20px;
justify-content: center;
align-items: center;
pointer-events: auto !important;
background-color: #fb7299dd;
color: white;
border-radius: 10%;
cursor: pointer;
text-align: center;
}
.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;
color: white;
height: 20px;
padding: 0 5px;
border-radius: 10%;
cursor: pointer;
border-radius: 2px;
pointer-events: auto;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* 修复视频卡片布局 */
.bili-video-card__cover {
contain: layout !important;
}
/* 面板样式 */
#bilibili-blacklist-panel {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* 按钮悬停效果 */
#bilibili-blacklist-panel button {
transition: background-color 0.2s;
}
#bilibili-blacklist-panel button:hover {
opacity: 0.9;
}
/* 管理按钮悬停效果 */
#bilibili-blacklist-manager:hover svg {
transform: scale(1.1);
}
#bilibili-blacklist-manager svg {
transition: transform 0.2s;
}
/* 输入框聚焦效果 */
#bilibili-blacklist-panel input:focus {
outline: none;
border-color: #fb7299 !important;
}
/*灰度效果*/
.bilibili-blacklist-grayscale {
filter: grayscale(95%);
}
`);
//可爱的卡比图标
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>
`;
}
//#endregion
//##########################
//#region 观察者
// MutationObserver 检测动态加载的新内容(仅当节点可见时才触发)
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
if (isVideoPage()) {
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;
}
});
}
// 如果有可见的新内容,延迟 1 秒后执行屏蔽(确保 DOM 完全渲染)
if (shouldCheck) {
processedCards = new WeakSet(); // 重置已处理卡片集合
setTimeout(() => {
BlockCard();
//addBlacklistManagerButton(); // 确保每次都添加黑名单管理按钮
if (isMainPage()) {
BlockMainAD(); // 屏蔽页面广告
}
if (isVideoPage()) {
BlockVideoPageAd(); // 屏蔽视频页面广告
}
if (!document.getElementById("bilibili-blacklist-manager"))
addBlacklistManagerButton(); // 添加黑名单管理按钮
if (!isBlacklistPanelCreated()) createBlacklistPanel();
}, 1000);
}
});
// 初始化观察者(监视 DOM 变化)
let observerError = 0;
function initObserver(container) {
const rootNode =
document.getElementById(container) ||
document.querySelector(container) ||
document.documentElement; // 回退到整个文档
if (rootNode) {
observer.observe(rootNode, {
childList: true, // 监视添加/移除的节点
subtree: true, // 监视所有后代
});
return true;
} else {
// 如果没找到根节点则重试
setTimeout(() => initObserver(container), 500);
console.warn("未找到根节点,正在重试...");
observerError++;
if (observerError > 10) {
console.error("重试次数过多,停止重试。");
return false;
}
}
}
//#endregion
//#region 初始化函数
let isInit = false; // 是否已经初始化
function init() {
// 重置状态
isBlocking = false;
lastBlockTime = 0;
blockedCards = new Set(); // 使用 Set 存储已屏蔽的卡片
processedCards = new WeakSet();
cardSequenceGetJson = new Set();
if (isMainPage()) {
initMainPage(); // 初始化主页
BlockMainAD(); // 屏蔽主页广告
} else if (isSearchPage()) {
initSearchPage(); // 初始化搜索页
} else if (isVideoPage()) {
initVideoPage(); // 初始化播放页
//BlockVideoPageAd(); // 屏蔽视频页面广告
} else if (isCategoryPage()) {
initCategoryPage(); // 初始化分类页
} else if (isUserSpace()) {
initUserSpace(); // 初始化用户空间
//return; // 用户空间不需要屏蔽
} else {
return; // 如果不是已知页面则不执行
}
BlockCard(); // 初始化时立即执行屏蔽
addBlacklistManagerButton(); // 添加黑名单管理按钮
createBlacklistPanel();
isInit = true; // 标记为已初始化
console.log("BiliBili黑名单脚本已加载🥔");
}
// 监听页面加载完成事件
document.addEventListener("DOMContentLoaded", init);
if (
document.readyState === "interactive" ||
document.readyState === "complete"
) {
init();
}
// 检查当前页面是否为B站主页
function isMainPage() {
return location.pathname === "/";
}
function initMainPage() {
initObserver("i_cecream"); // 传入B站主页的主容器ID
console.log("主页已加载🍓");
}
/// -----搜索页----
function isSearchPage() {
return location.hostname === "search.bilibili.com";
}
function initSearchPage() {
initObserver("i_cecream");
console.log("搜索页已加载🍉");
}
/// --- 播放页 ---
function isVideoPage() {
return location.pathname.startsWith("/video/");
}
function initVideoPage() {
initObserver("rcmd-tab");
console.log("播放页已加载🍇");
}
// ---- 分类页 ----
function isCategoryPage() {
// 页面链接 https://www.bilibili.com/c/xxxxxx
// 通过检查路径名是否以 "/c/" 开头来判断是否为分类页
return location.pathname.startsWith("/c/");
}
function initCategoryPage() {
initObserver("app");
console.log("分类页已加载🍊");
}
///---用户空间---
function isUserSpace() {
return location.hostname === "space.bilibili.com";
}
function initUserSpace() {
console.log("用户空间已加载🍎");
// Use a more robust way to find the UP name element, and observe for its presence
const upNameSelector = "#h-name, .nickname";
const observerForUpName = new MutationObserver((mutations, observer) => {
const upNameElement = document.querySelector(upNameSelector);
if (upNameElement) {
observer.disconnect(); // Stop observing once found
addBlockButtonToUserSpace(upNameElement);
}
});
// Start observing the body for changes
observerForUpName.observe(document.body, {
childList: true,
subtree: true,
});
// Also try to find it immediately in case it's already there
const initialUpNameElement = document.querySelector(upNameSelector);
if (initialUpNameElement) {
observerForUpName.disconnect(); // Disconnect if found immediately
addBlockButtonToUserSpace(initialUpNameElement);
}
}
function addBlockButtonToUserSpace(upNameElement) {
const upName = upNameElement.textContent.trim();
//console.log(`当前用户昵称: ${upName}`);
if (upNameElement.querySelector(".bilibili-blacklist-up-block-btn")) {
return;
}
// 设置 inline-flex,让文字和按钮一行显示
upNameElement.style.display = "inline-flex";
upNameElement.style.alignItems = "center";
const button = document.createElement("button");
button.className = "bilibili-blacklist-up-block-btn";
button.textContent = "屏蔽";
button.style.color = "#fff";
button.style.width = "100px";
button.style.height = "30px";
button.style.marginLeft = "10px";
button.style.borderRadius = "5px";
button.style.border = "1px solid #fb7299";
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(); // refresh button state immediately
// No need to re-run initUserSpace(); it causes unnecessary re-initializations
});
refreshButtonStatus(); // Set initial button state
upNameElement.appendChild(button);
}
//#endregion
//#region 额外功能-屏蔽广告
// 屏蔽广告
function BlockMainAD() {
if (!globalConfig.flagAD) return;
const adSelectors = [
".floor-single-card", // 分区推荐
".bili-live-card", // 直播推广
];
adSelectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((adCard) => {
adCard.remove();
});
});
}
// 屏蔽视频页面广告(使用数组优化)
function BlockVideoPageAd() {
if (!globalConfig.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", // 广告地板
];
adSelectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((adCard) => {
adCard.remove();
});
});
}
//#endregion
})();