// ==UserScript== // @name Bilibili 按标签、标题、时长、UP主屏蔽视频 // @namespace https://github.com/tjxwork // @version 0.5.4 // @note v0.5.4 修复 "综合热门、每周必看、入站必刷" 页面的标题无法正常获取的错误。 // @description 对Bilibili.com的视频卡片元素,以 标签、标题、时长、UP主名称、UP主UID 来判断匹配,添加一个屏蔽叠加层或者隐藏视频。 // @author tjxwork // @license CC-BY-NC-SA // @icon https://www.bilibili.com/favicon.ico // @homepageURL https://greasyfork.org/zh-CN/scripts/481629-bilibili-%E6%8C%89%E6%A0%87%E7%AD%BE-%E6%A0%87%E9%A2%98-%E6%97%B6%E9%95%BF-up%E4%B8%BB%E5%B1%8F%E8%94%BD%E8%A7%86%E9%A2%91 // @supportURL https://greasyfork.org/zh-CN/scripts/481629-bilibili-%E6%8C%89%E6%A0%87%E7%AD%BE-%E6%A0%87%E9%A2%98-%E6%97%B6%E9%95%BF-up%E4%B8%BB%E5%B1%8F%E8%94%BD%E8%A7%86%E9%A2%91/feedback // @match https://www.bilibili.com/* // @match https://search.bilibili.com/* // @match https://www.bilibili.com/v/popular/all/* // @match https://www.bilibili.com/v/popular/weekly/* // @match https://www.bilibili.com/v/popular/history/* // @exclude https://www.bilibili.com/anime/* // @exclude https://www.bilibili.com/movie/* // @exclude https://www.bilibili.com/guochuang/* // @exclude https://www.bilibili.com/variety/* // @exclude https://www.bilibili.com/tv/* // @exclude https://www.bilibili.com/documentary* // @exclude https://www.bilibili.com/mooc/* // @exclude https://www.bilibili.com/v/virtual/* // @exclude https://www.bilibili.com/v/popular/rank/* // @exclude https://www.bilibili.com/v/popular/music/* // @exclude https://www.bilibili.com/v/popular/drama/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== "use strict"; // 初始化屏蔽参数变量 let blockedParameter = GM_getValue("GM_blockedParameter", { // 屏蔽标题数组 blockedTitleArray: [], // 屏蔽UP主或者Uid数组 blockedNameOrUidArray: [], // 屏蔽标签数组 blockedTagArray: [], // 双重屏蔽标签数组 doubleBlockedTagArray: [], // 屏蔽短时长视频(0为不生效) blockedShortDuration: 0, // 隐藏视频模式 hideVideoModeSwitch: false, // 启用日志输出 consoleOutputLogSwitch: false, }); // 配色 // 主窗体背景色 const uiBackgroundColor = "#303030"; // 输入模块背景色 const uiInputContainerBackgroundColor = "#404040"; // 输入框背景色 const uiInputBoxBackgroundColor = "#595959"; // 文字颜色 const uiTextColor = "rgb(250,250,250)"; // 按钮色 const uiButtonColor = "rgb(0, 174, 236)"; // 边框色 const uiBorderColor = "rgba(0, 0, 0, 0)"; // 提醒框背景色 const uiPromptBoxColor = "rgb(42,44,53)"; // 屏蔽叠加层背景色 const blockedOverlayColor = "rgba(60, 60, 60, 0.85)"; // --------------------菜单UI部分-------------------- // 通用的基础样式 const basicsStyles = { boxSizing: "border-box", padding: "0 0.5em", marginBottom: "0.5em", borderStyle: "solid", borderWidth: "1px", borderColor: uiBorderColor, borderRadius: "0.3em", lineHeight: "1.5em", fontSize: "1em", verticalAlign: "middle", color: uiTextColor, fontFamily: '"Cascadia Mono", Monaco, Consolas, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif', }; // 添加基础样式辅助函数 function addStyles(element, styles) { for (const style in styles) { element.style[style] = styles[style]; } } // 屏蔽菜单UI function blockedMenuUi() { // 创建菜单Ui的Div const menuContent = document.createElement("div"); menuContent.id = "blockedMenuUi"; addStyles(menuContent, basicsStyles); menuContent.style.position = "fixed"; menuContent.style.bottom = "5vh"; menuContent.style.right = "1vh"; menuContent.style.zIndex = "1000"; menuContent.style.width = "32em"; menuContent.style.height = "61.3em"; menuContent.style.backgroundColor = uiBackgroundColor; menuContent.style.fontSize = "14px"; menuContent.style.padding = "0.85em"; // 创建标题 const title = document.createElement("div"); title.textContent = "Bilibili按标签/标题/时长/UP主屏蔽视频"; addStyles(title, basicsStyles); title.style.textAlign = "center"; title.style.fontSize = "1.5em"; title.style.padding = "0"; // 创建输入模块的辅助函数 (标签文本,保存参数的对象变量,保存参数的对象变量里面的Key名,输入模块的类型) function createInputModule(label, blockedParameterObject, blockedParameterObjectKey, type) { // 数组类的输入 if (type == "Array") { // 创建父级容器 const container = document.createElement("div"); addStyles(container, basicsStyles); container.style.backgroundColor = uiInputContainerBackgroundColor; // 创建提示标签 const inputLabel = document.createElement("label"); inputLabel.textContent = label; addStyles(inputLabel, basicsStyles); inputLabel.style.display = "block"; inputLabel.style.marginTop = "0.5em"; // 创建输入框 const inputBox = document.createElement("input"); inputBox.type = "text"; inputBox.placeholder = "多项输入请用英文逗号间隔"; inputBox.spellcheck = false; addStyles(inputBox, basicsStyles); inputBox.style.backgroundColor = uiInputBoxBackgroundColor; inputBox.style.width = "25em"; // 创建添加按钮 const addButton = document.createElement("button"); addButton.textContent = "添加"; addButton.addEventListener("click", addButtonClickFunction); addStyles(addButton, basicsStyles); addButton.style.backgroundColor = uiButtonColor; addButton.style.marginLeft = "0.5em"; // 创建已有数据的多行展示框 const inputMultiLineDisplayBox = document.createElement("textarea"); inputMultiLineDisplayBox.id = blockedParameterObjectKey; inputMultiLineDisplayBox.value = blockedParameterObject[blockedParameterObjectKey]; inputMultiLineDisplayBox.rows = "3"; // 设置多行展示框的行数 inputMultiLineDisplayBox.placeholder = "也可以直接编辑该输入框内容,“保存”后生效"; inputMultiLineDisplayBox.spellcheck = false; addStyles(inputMultiLineDisplayBox, basicsStyles); inputMultiLineDisplayBox.style.backgroundColor = uiInputBoxBackgroundColor; inputMultiLineDisplayBox.style.width = "100%"; inputMultiLineDisplayBox.style.resize = "none"; // 禁止拖动 inputMultiLineDisplayBox.style.padding = "0.5em"; // 组合元素 container.appendChild(inputLabel); container.appendChild(inputBox); container.appendChild(addButton); container.appendChild(inputMultiLineDisplayBox); return container; } // 数值类的输入 if (type == "Number") { // 创建父级容器 const container = document.createElement("div"); addStyles(container, basicsStyles); container.style.backgroundColor = uiInputContainerBackgroundColor; container.style.lineHeight = "2em"; // 创建提示标签 const inputLabel = document.createElement("label"); addStyles(inputLabel, basicsStyles); inputLabel.textContent = label; inputLabel.style.marginTop = "0.5em"; // 创建输入框 const inputBox = document.createElement("input"); inputBox.type = "number"; inputBox.id = blockedParameterObjectKey; inputBox.value = blockedParameterObject[blockedParameterObjectKey]; inputBox.spellcheck = false; addStyles(inputBox, basicsStyles); inputBox.style.backgroundColor = uiInputBoxBackgroundColor; inputBox.style.width = "6em"; inputBox.style.margin = "0"; inputBox.style.verticalAlign = "baseline"; // 组合元素 container.appendChild(inputLabel); container.appendChild(inputBox); return container; } // 布尔类的输入 if (type == "Bool") { // 创建父级容器 const container = document.createElement("div"); addStyles(container, basicsStyles); container.style.backgroundColor = uiInputContainerBackgroundColor; container.style.lineHeight = "2em"; // 创建提示标签 const inputLabel = document.createElement("label"); inputLabel.textContent = label; addStyles(inputLabel, basicsStyles); inputLabel.style.marginTop = "0.5em"; // 创建输入框 const inputBox = document.createElement("input"); inputBox.type = "checkbox"; inputBox.id = blockedParameterObjectKey; inputBox.checked = blockedParameterObject[blockedParameterObjectKey]; inputBox.spellcheck = false; addStyles(inputBox, basicsStyles); inputBox.style.backgroundColor = uiInputBoxBackgroundColor; inputBox.style.margin = "0"; // 组合元素 container.appendChild(inputLabel); container.appendChild(inputBox); return container; } } // 创建各个变量参数输入框和按钮 const blockedTitleInput = createInputModule( "按标题屏蔽 (支持正则)", blockedParameter, "blockedTitleArray", "Array" ); const blockedNamesInput = createInputModule( "按UP名称或UID屏蔽", blockedParameter, "blockedNameOrUidArray", "Array" ); const blockedTagsInput = createInputModule("按标签屏蔽 (支持正则)", blockedParameter, "blockedTagArray", "Array"); const doubleBlockedTagsInput = createInputModule( '按双重标签屏蔽 (以"A标签|B标签"的格式来添加,支持正则)', blockedParameter, "doubleBlockedTagArray", "Array" ); const blockedShortDurationInput = createInputModule( "按时间短于指定秒数视频屏蔽 (0为不生效)", blockedParameter, "blockedShortDuration", "Number" ); const hideVideoModeSwitchInput = createInputModule( "隐藏视频而不是使用叠加层覆盖", blockedParameter, "hideVideoModeSwitch", "Bool" ); const consoleOutputLogSwitchInput = createInputModule( "控制台输出日志开关", blockedParameter, "consoleOutputLogSwitch", "Bool" ); // 创建菜单按钮的辅助函数 function createMenuButton(label, clickFunction) { // 创建添加按钮 const menuButton = document.createElement("button"); menuButton.textContent = label; menuButton.addEventListener("click", clickFunction); addStyles(menuButton, basicsStyles); menuButton.style.backgroundColor = uiButtonColor; menuButton.style.padding = "0.5em"; menuButton.style.marginBottom = "0"; menuButton.style.marginRight = "0.5em"; return menuButton; } // 创建菜单按钮 const closeButton = createMenuButton("关闭", closeButtonClickFunction); // 使用 () => 闭包来在 createMenuButton 函数内部传递带参数的 saveButtonClickFunction。 const saveButton = createMenuButton("保存", () => saveButtonClickFunction(blockedParameter)); const refreshButton = createMenuButton("读取", () => refreshButtonClickFunction(blockedParameter)); const saveAndCloseButton = createMenuButton("保存并关闭", () => saveAndCloseButtonClickFunction(blockedParameter)); // 创建菜单按钮父级容器 const menuButtonContainer = document.createElement("div"); menuButtonContainer.id = "menuButtonContainer"; addStyles(menuButtonContainer, basicsStyles); menuButtonContainer.style.padding = "0"; menuButtonContainer.style.margin = "0"; // 把菜单按钮 添加到 菜单按钮父级容器里 menuButtonContainer.appendChild(closeButton); menuButtonContainer.appendChild(saveButton); menuButtonContainer.appendChild(refreshButton); menuButtonContainer.appendChild(saveAndCloseButton); // 将所有元素添加到弹窗中 menuContent.appendChild(title); menuContent.appendChild(blockedTitleInput); menuContent.appendChild(blockedNamesInput); menuContent.appendChild(blockedTagsInput); menuContent.appendChild(doubleBlockedTagsInput); menuContent.appendChild(blockedShortDurationInput); menuContent.appendChild(hideVideoModeSwitchInput); menuContent.appendChild(consoleOutputLogSwitchInput); menuContent.appendChild(menuButtonContainer); // 将弹窗添加到页面 document.body.appendChild(menuContent); } // 添加按钮对应的点击函数 function addButtonClickFunction() { // 使用 event.target 获取触发事件的按钮 const clickedButton = event.target; // 将获取上一兄弟元素的值,既为 inputBox 输入框 的值 const inputBox = clickedButton.previousElementSibling.value; // 将获取下一兄弟元素,既为 inputMultiLineDisplayBox 已有数据的多行展示框 的值 const inputMultiLineDisplayBox = clickedButton.nextElementSibling.value; if (inputMultiLineDisplayBox === "") { // 如果多行展示框为空 clickedButton.nextElementSibling.value = inputBox; } else { // 如果多行展示框不为空,则在前面添加逗号并返回拼接后的字符串 clickedButton.nextElementSibling.value = inputMultiLineDisplayBox.concat(",", inputBox); } // 清空输入框 clickedButton.previousElementSibling.value = ""; } // 关闭按钮对应的点击函数 function closeButtonClickFunction() { // 获取需要删除的元素 let elementToRemove = document.getElementById("blockedMenuUi"); // 确保元素存在再进行删除操作 if (elementToRemove) { // 先获取父元素 var parentElement = elementToRemove.parentNode; // 在父元素删除指定的元素 parentElement.removeChild(elementToRemove); } } // 保存按钮对应的点击函数 (把 多行展示框 里面的东西 写进 blockedParameter) function saveButtonClickFunction(blockedParameterObject) { // 获取在 blockedMenuUi 菜单UI下,所有带有ID的元素 let inputIdItem = document.querySelectorAll("#blockedMenuUi > div >[id]"); // 遍历处理ID元素 for (const item of inputIdItem) { // 双重屏蔽标签 的 多行展示框 内容要特殊处理 if (item.id == "doubleBlockedTagArray") { // 处理特殊的双重屏蔽字符串 function processDoubleBlockedTagString(inputString) { // 使用逗号分割字符串 const items = inputString.split(","); // 过滤并处理每个项 const processedArray = items .map((item) => { // 去除项两端的空格 const trimmedItem = item.trim(); // 判断项中是否包含 "|",且 "|" 的数量为1 (分割后有两份),且不为空值 const secondSplitItem = trimmedItem.split("|").filter((value) => value !== ""); if (secondSplitItem.length === 2) { // 去除空格,并拼接成最终的项 const formattedItem = secondSplitItem.map((str) => str.trim()).join("|"); return formattedItem; } else { // 如果不包含 "|" 或者 "|" 数量不为1,返回 null(后续过滤) return null; } }) .filter((item) => item !== null); // 过滤掉为 null 的项 return processedArray; } blockedParameterObject[item.id] = processDoubleBlockedTagString(item.value); continue; } // 多行展示框 数据 if (item.type == "textarea") { // 多行展示框 的数据存入 blockedParameter blockedParameterObject[item.id] = item.value .split(",") .filter((value) => value !== "") .map((str) => str.trim()); } // 数值类输入框 if (item.type == "number") { function convertToNumber(str) { const parsedNumber = parseInt(str, 10); // 第二个参数表示使用十进制转换 return isNaN(parsedNumber) ? 0 : parsedNumber; } // 多行展示框 的数据存入 blockedParameter blockedParameterObject[item.id] = convertToNumber(item.value); } // 布尔类的输入框 if (item.type == "checkbox") { blockedParameterObject[item.id] = item.checked; } } // 将全局屏蔽参数对象变量 blockedParameter 保存到油猴扩展存储中 GM_setValue("GM_blockedParameter", blockedParameterObject); // 触发刷新(读取)函数,通知为 false refreshButtonClickFunction(blockedParameterObject, false); showFloatingReminder("内容已保存"); } // 读取按钮对应的点击函数(把 油猴扩展存储 读取到 blockedParameter 读取到 多行展示框) function refreshButtonClickFunction(blockedParameterObject, enableMessage = true) { // 油猴扩展存储 读取到 blockedParameter blockedParameterObject = GM_getValue("GM_blockedParameter", { // 屏蔽标题数组 blockedTitleArray: [], // 屏蔽UP主或者Uid数组 blockedNameOrUidArray: [], // 屏蔽标签数组 blockedTagArray: [], // 双重屏蔽标签数组 doubleBlockedTagArray: [], // 屏蔽短时长视频(0为不生效) blockedShortDuration: 0, // 隐藏视频模式 hideVideoModeSwitch: false, // 启用日志输出 consoleOutputLogSwitch: false, }); // 获取在 blockedMenuUi 菜单UI下,所有带有ID的元素 let inputIdItem = document.querySelectorAll("#blockedMenuUi > div >[id]"); // 把 blockedParameter 的数据 写到对应的 UI输入框 for (let item of inputIdItem) { if (item.type == "textarea") { item.value = blockedParameterObject[item.id].join(","); } if (item.type == "number") { item.value = blockedParameterObject[item.id]; } if (item.type == "checkbox") { item.checked = blockedParameterObject[item.id]; } } // 保存按钮 saveButtonClickFunction 保存后,会触发一次 refreshButtonClickFunction,不需要两个都弹出提示框 if (enableMessage) { showFloatingReminder("内容已刷新"); } } // 保存并关闭按钮对应的点击函数,分别触发保存和关闭按钮 function saveAndCloseButtonClickFunction(blockedParameterObject) { saveButtonClickFunction(blockedParameterObject); closeButtonClickFunction(); } // 菜单按钮按下的提醒消息 function showFloatingReminder(message) { const element = document.querySelector("#menuButtonContainer"); // 创建提醒元素 const reminderElement = document.createElement("div"); reminderElement.textContent = message; addStyles(reminderElement, basicsStyles); reminderElement.style.backgroundColor = uiPromptBoxColor; reminderElement.style.padding = "0.5em"; reminderElement.style.marginBottom = "0"; reminderElement.style.float = "right"; reminderElement.style.bottom = "1em"; reminderElement.style.right = "1em"; // 将提醒元素添加到指定的元素内 element.appendChild(reminderElement); // 3秒后移除提醒元素 setTimeout(() => { element.removeChild(reminderElement); }, 3000); } // 在油猴扩展中添加脚本菜单选项 GM_registerMenuCommand("屏蔽参数面板", blockedMenuUi); // -----------------------逻辑处理部分-------------------------- // 视频的临时详细信息对象,以videoBv为键, 用于同窗口内的缓存查询 let videoInfoDict = {}; // 日志输出, 创建一个包装函数,根据 consoleOutputLogSwitch 标志来决定是否输出日志 function consoleLogOutput(...args) { if (blockedParameter.consoleOutputLogSwitch) { // 获取当前时间的时分秒毫秒部分 var now = new Date(); var hours = now.getHours().toString().padStart(2, "0"); var minutes = now.getMinutes().toString().padStart(2, "0"); var seconds = now.getSeconds().toString().padStart(2, "0"); var milliseconds = now.getMilliseconds().toString().padStart(3, "0"); // 将时间信息添加到日志消息中 var logTime = `${hours}:${minutes}:${seconds}.${milliseconds}`; // 合并时间信息和 args 成为一个数组 var logArray = [logTime, ...args]; console.log(...logArray); } } // 获取视频元素 function getVideoElements() { // 获取所有有可能是视频元素的标签 var videoElements = document.querySelectorAll( // div.bili-video-card 首页(https://www.bilibili.com/)、分区首页(https://www.bilibili.com/v/*)、搜索页面(https://search.bilibili.com/*) // div.video-page-card-small 播放页右侧推荐(https://www.bilibili.com/video/BV****) // li.bili-rank-list-video__item 分区首页-子分区右侧热门(https://www.bilibili.com/v/*) // div.video-card 综合热门(https://www.bilibili.com/v/popular/all) 、每周必看(https://www.bilibili.com/v/popular/weekly) 、入站必刷(https://www.bilibili.com/v/popular/history) // div.video-card-reco 旧版首页推送(https://www.bilibili.com/) // div.video-card-common 旧版首页分区(https://www.bilibili.com/) // div.rank-wrap 旧版首页分区右侧排行(https://www.bilibili.com/) "div.bili-video-card, div.video-page-card-small, li.bili-rank-list-video__item, div.video-card, div.video-card-reco, div.video-card-common, div.rank-wrap" ); // 过滤掉没有包含a标签的元素 videoElements = Array.from(videoElements).filter((element) => element.querySelector("a")); // 返回处理后的结果 return videoElements; } // 判断是否为已经屏蔽处理过的子元素 function isAlreadyBlockedChildElement(videoElement) { // 确认是否为已经修改 元素已隐藏 跳过 if (videoElement.style.display == "none") { // consoleLogOutput(operationInfo, "元素已隐藏 跳过剩下主函数步骤"); return true; } // 确认是否为已经修改 元素已透明 延迟处理中 跳过 if (videoElement.style.filter == "blur(5px)") { // consoleLogOutput(operationInfo, "元素已透明 延迟处理中 跳过剩下主函数步骤"); return true; } // 获取子元素,以确认是否为已经修改 if (videoElement.firstElementChild.className == "blockedOverlay") { // consoleLogOutput(videoElement, "获取子元素,确认是已屏蔽处理过,跳过剩下主函数步骤"); return true; } } // 获取视频元素的Bv号和标题 function getBvAndTitle(videoElement) { // 从视频元素中获取所有a标签链接 const videoLinkElements = videoElement.querySelectorAll("a"); // Bv号 let videoBv; // 循环处理所有a标签链接 for (let videoLinkElement of videoLinkElements) { // 如果a标签中没有字符,跳过剩余语句,开始下一次循环 // if (!videoLinkElement.textContent) { // continue; // } // 如果a标签中的字符有"稍后再看",跳过剩余语句,开始下一次循环 if (/稍后再看/.test(videoLinkElement.textContent)) { continue; } // 获取的链接,如果与Bv链接的格式匹配的话 let bvTemp = videoLinkElement.href.match(/\/(BV\w+)/); if (bvTemp) { // 视频Bv号 videoBv = bvTemp[1]; consoleLogOutput(videoBv, "此BV号来源于", videoElement); // 确保 videoInfoDict[videoBv] 已定义 if (!videoInfoDict[videoBv]) { videoInfoDict[videoBv] = {}; } // 视频链接 videoInfoDict[videoBv].videoLink = videoLinkElement.href; consoleLogOutput(videoBv, "网页上获取的链接: ", videoInfoDict[videoBv].videoLink); // // 视频标题 // videoInfoDict[videoBv].videoTitle = videoLinkElement.textContent; // consoleLogOutput(videoBv, "网页上获取的标题: ", videoInfoDict[videoBv].videoTitle); } } // 没有拿到Bv号,提前结束 if (!videoBv) { consoleLogOutput(videoElement, "getBvAndTitle() 没有拿到Bv号 提前结束 跳过剩下主函数步骤"); return false; } // 视频标题 , 从视频元素中获取第一个带 title 属性且不为 span 的标签 videoInfoDict[videoBv].videoTitle = videoElement.querySelector('[title]:not(span)').title; consoleLogOutput(videoBv, "网页上获取的标题: ", videoInfoDict[videoBv].videoTitle); return videoBv; } // 判断处理匹配的屏蔽标题 function handleBlockedTitle(videoElement, videoBv) { // 使用 屏蔽标题数组 与 视频标题 进行匹配 const blockedTitleFind = blockedParameter.blockedTitleArray.find((blockedTitle) => { const blockedTitleRegEx = new RegExp(blockedTitle); if (blockedTitleRegEx.test(videoInfoDict[videoBv].videoTitle)) { return blockedTitle; } }); if (blockedTitleFind) { createOverlay(`屏蔽标题: ${blockedTitleFind}`, videoElement, `${videoBv} handleBlockedTitle()`); consoleLogOutput(videoBv, "handleBlockedTitle() 已屏蔽视频元素 跳过剩下主函数步骤"); return true; } } // 时分秒 转 秒 辅助函数 function timeToSeconds(timeString) { // 将时间字符串按冒号分割 const timeArray = timeString.split(":"); // 从数组的末尾开始计算秒数 let seconds = 0; let multiplier = 1; for (let i = timeArray.length - 1; i >= 0; i--) { seconds += parseInt(timeArray[i]) * multiplier; multiplier *= 60; // 每遍历一个单位,乘以60,转为秒 } return seconds; } // 获取视频元素的时长 function getDuration(videoElement, videoBv) { // 如果已经有 BV号 对应的 记录,跳过 if (videoInfoDict[videoBv].videoDuration) { consoleLogOutput(videoBv, "getDuration() 在 videoInfoDict 中,找到对应的 视频时长 记录"); return; } // 从视频元素中获取视频元素 const timeStringElement = videoElement.querySelector("span.bili-video-card__stats__duration, span.duration"); if (timeStringElement) { const timeStringTemp = timeStringElement.textContent; videoInfoDict[videoBv].videoDuration = timeToSeconds(timeStringTemp); consoleLogOutput( videoBv, "getDuration() 已成功在网页上获取 视频时长", videoInfoDict[videoBv].videoDuration, "秒" ); } else { consoleLogOutput(videoBv, "getDuration() 在没有在网页中找到 视频时长 元素"); } //获取当前时间 const currentTime = new Date(); // 当 lastTimeApiVideoInfo 上次API获取视频标签的时间存在,并且,和当前的时间差小于3秒时,跳过 if ( videoInfoDict[videoBv].lastTimeApiVideoInfo && currentTime - videoInfoDict[videoBv].lastTimeApiVideoInfo < 3000 ) { consoleLogOutput(videoBv, "getDuration() 距离上次 Fetch 获取视频信息还未超过3秒钟"); return; } videoInfoDict[videoBv].lastTimeApiVideoInfo = currentTime; // 通过API获取视频UP信息 fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${videoBv}`) .then((response) => response.json()) .then((videoApiInfoJson) => { let returnMessage = videoApiInfoJson.message; consoleLogOutput(videoBv, "getDuration() Fetch:返回的消息: ", returnMessage); videoInfoDict[videoBv].videoUpUid = videoApiInfoJson.data.owner.mid; consoleLogOutput(videoBv, "getDuration() API获取的UP主Uid: ", videoInfoDict[videoBv].videoUpUid); videoInfoDict[videoBv].videoUpName = videoApiInfoJson.data.owner.name; consoleLogOutput(videoBv, "getDuration() API获取的UP主名称: ", videoInfoDict[videoBv].videoUpName); videoInfoDict[videoBv].videoDuration = videoApiInfoJson.data.duration; consoleLogOutput(videoBv, "getDuration() API获取的视频时长: ", videoInfoDict[videoBv].videoDuration, "秒"); // 尝试在fetch异步结束后直接屏蔽处理 匹配的短时长视频 handleBlockedShortDuration(videoElement, videoBv, true); // 为了减少 API 调用,限制了短时间不能重复调用API,getDuration() 和 getUpNameAndUpUid() 实际是使用的同一个API // 在同时需求API 查询 视频时长 和 视频UP主名称 时,getDuration() 没来得及返回信息到videoInfoDict的情况下, // getUpNameAndUpUid() 没有从videoInfoDict里拿到信息,会重新发起 API ,但是这没有必要,getDuration() 的信息是一样的。 // getDuration() 和 getUpNameAndUpUid() 会共用同一个时间标记 lastTimeApiVideoInfo 来限制同Bv号的查询频率, // 并将 handleBlockedUpNameOrUpUid() 操作复制到一份到 getDuration() 下,等待 fetch 结束后运行。 // 尝试在fetch异步结束后直接屏蔽处理 匹配的屏蔽Up主名称或Up主Uid handleBlockedUpNameOrUpUid(videoElement, videoBv, true); }) .catch((error) => console.error(videoBv, "getDuration() Fetch错误:", error)); // 判断是否成功拿到 BV号 对应的 Up主名称 Up主Uid 记录 if (videoInfoDict[videoBv].videoUpUid && videoInfoDict[videoBv].videoUpName) { consoleLogOutput(videoBv, "getDuration() 已成功在API上获取 UP主名称 UP主UID 视频时长"); return; } else { consoleLogOutput(videoBv, "getDuration() 暂时未获取 UP主名称 UP主UID 视频时长"); return; } } // 判断处理匹配的短时长视频 function handleBlockedShortDuration(videoElement, videoBv, withinFetch = false) { if (!videoInfoDict[videoBv].videoDuration) { consoleLogOutput(videoBv, "handleBlockedShortDuration() 未获取到 视频时长,放弃执行"); return; } // 匹配短时长视频 if (videoInfoDict[videoBv].videoDuration < blockedParameter.blockedShortDuration) { createOverlay( `屏蔽短于${blockedParameter.blockedShortDuration}秒时长视频`, videoElement, `${videoBv} handleBlockedShortDuration()` ); if (withinFetch) { consoleLogOutput( videoBv, "在 getDuration() 的 Fetch 内执行 handleBlockedShortDuration() 已屏蔽 短时长视频" ); } else { consoleLogOutput(videoBv, "handleBlockedShortDuration() 已屏蔽 短时长视频,跳过剩下主函数步骤"); } return true; } } // 获取视频元素的Up主名称 Up主Uid function getUpNameAndUpUid(videoElement, videoBv) { // 如果已经有 BV号 对应的 Up主名称 Up主Uid 记录,跳过 if (videoInfoDict[videoBv].videoUpUid && videoInfoDict[videoBv].videoUpName) { consoleLogOutput(videoBv, "getUpNameAndUpUid() 在 videoInfoDict 中,已找到对应的 UP主名称 UP主UID 记录"); return; } // 从视频元素中获取所有a标签链接 const videoLinkElements = videoElement.querySelectorAll("a"); // 循环处理所有a标签链接 for (let videoLinkElement of videoLinkElements) { // 获取的链接,如果与 Uid 的链接格式匹配的话 const uidTemp = videoLinkElement.href.match(/space\.bilibili\.com\/(\d+)/); if (uidTemp) { // 视频UpUid videoInfoDict[videoBv].videoUpUid = uidTemp[1]; consoleLogOutput(videoBv, "网页上获取的UP主UID: ", videoInfoDict[videoBv].videoUpUid); // 视频Up名称 videoInfoDict[videoBv].videoUpName = videoLinkElement.textContent; videoInfoDict[videoBv].videoUpName = videoInfoDict[videoBv].videoUpName.replace(/\s?·\s\S*$/, ""); consoleLogOutput(videoBv, "网页上获取的UP主名称: ", videoInfoDict[videoBv].videoUpName); } } // 判断是否成功拿到 BV号 对应的 Up主名称 Up主Uid 记录,成功就提前退出 if (videoInfoDict[videoBv].videoUpUid && videoInfoDict[videoBv].videoUpName) { consoleLogOutput(videoBv, "getUpNameAndUpUid() 已成功在网页上获取 UP主名称 UP主UID"); return; } //获取当前时间 const currentTime = new Date(); // 当 lastTimeApiVideoInfo 上次API获取视频信息的时间存在,并且,和当前的时间差小于3秒时,跳过 if ( videoInfoDict[videoBv].lastTimeApiVideoInfo && currentTime - videoInfoDict[videoBv].lastTimeApiVideoInfo < 3000 ) { consoleLogOutput(videoBv, "getUpNameAndUpUid() 距离上次 Fetch 获取视频信息还未超过5秒钟"); return; } videoInfoDict[videoBv].lastTimeApiVideoInfo = currentTime; // 如果前面的通过网页提取视频UP信息失败,则通过API获取视频UP信息 fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${videoBv}`) .then((response) => response.json()) .then((videoApiInfoJson) => { let returnMessage = videoApiInfoJson.message; consoleLogOutput(videoBv, "getUpNameAndUpUid() Fetch:返回的消息: ", returnMessage); videoInfoDict[videoBv].videoUpUid = videoApiInfoJson.data.owner.mid; consoleLogOutput(videoBv, "getUpNameAndUpUid() API获取的UP主UID: ", videoInfoDict[videoBv].videoUpUid); videoInfoDict[videoBv].videoUpName = videoApiInfoJson.data.owner.name; consoleLogOutput(videoBv, "getUpNameAndUpUid() API获取的UP主名称: ", videoInfoDict[videoBv].videoUpName); videoInfoDict[videoBv].videoDuration = videoApiInfoJson.data.duration; consoleLogOutput( videoBv, "getUpNameAndUpUid() API获取的视频时长: ", videoInfoDict[videoBv].videoDuration, "秒" ); // 尝试在fetch异步结束后直接屏蔽处理 匹配的屏蔽Up主名称或Up主Uid handleBlockedUpNameOrUpUid(videoElement, videoBv, true); }) .catch((error) => console.error(videoBv, "getUpNameAndUpUid() Fetch错误:", error)); // 判断是否成功拿到 BV号 对应的 Up主名称 Up主Uid 记录 if (videoInfoDict[videoBv].videoUpUid && videoInfoDict[videoBv].videoUpName) { consoleLogOutput(videoBv, "getUpNameAndUpUid() 已成功在API上获取 Up主名称 Up主Uid 视频时长"); return; } else { consoleLogOutput(videoBv, "getUpNameAndUpUid() 暂时未获取 Up主Uid Up主名称 视频时长"); return; } } // 判断处理匹配的屏蔽Up主名称或Up主Uid function handleBlockedUpNameOrUpUid(videoElement, videoBv, withinFetch = false) { if (!videoInfoDict[videoBv].videoUpUid) { consoleLogOutput(videoBv, "handleBlockedUpNameOrUpUid() 未获取到 UP主名称 UP主UID 信息,放弃执行"); return; } // 使用 屏蔽Up名称和Uid数组 与 视频UpUid 和 视频Up名称 进行匹配 const blockedNameOrUidFind = blockedParameter.blockedNameOrUidArray.find((blockedNameOrUid) => { if (blockedNameOrUid == videoInfoDict[videoBv].videoUpUid) { return videoInfoDict[videoBv].videoUpUid; } if (blockedNameOrUid == videoInfoDict[videoBv].videoUpName) { return videoInfoDict[videoBv].videoUpName; } }); if (blockedNameOrUidFind) { createOverlay(`屏蔽UP: ${blockedNameOrUidFind}`, videoElement, `${videoBv} handleBlockedUpNameOrUpUid()`); if (withinFetch) { consoleLogOutput( videoBv, "在 getUpNameAndUpUid() 的 Fetch 内执行 handleBlockedUpNameOrUpUid() 已屏蔽对应的 UP主名称 UP主UID" ); } else { consoleLogOutput( videoBv, "handleBlockedUpNameOrUpUid() 已屏蔽对应的 UP主名称 UP主UID ,跳过剩下主函数步骤" ); } return true; } } // 获取视频元素的视频标签 function getVideoTags(videoElement, videoBv) { // 如果已经有 BV号 对应的 Up主名称 Up主Uid 视频时长 记录,跳过 if (videoInfoDict[videoBv].videoTags) { consoleLogOutput(videoBv, "getVideoTags() 在 videoInfoDict 中,已找到对应的 视频标签 记录"); return; } //获取当前时间 const currentTime = new Date(); // 当 lastTimeApiVideoTag 上次API获取视频标签的时间存在,并且,和当前的时间差小于3秒时,跳过 if (videoInfoDict[videoBv].lastTimeApiVideoTag && currentTime - videoInfoDict[videoBv].lastTimeApiVideoTag < 3000) { consoleLogOutput(videoBv, "getVideoTags 距离上次 Fetch 获取视频标签还未超过3秒钟"); return; } videoInfoDict[videoBv].lastTimeApiVideoTag = currentTime; // 获取视频标签 fetch(`https://api.bilibili.com/x/web-interface/view/detail/tag?bvid=${videoBv}`) .then((response) => response.json()) .then((videoApiInfoJson) => { let returnMessage = videoApiInfoJson.message; consoleLogOutput(videoBv, "getVideoTags() Fetch:返回的消息: ", returnMessage); // 提取标签名字数组 videoInfoDict[videoBv].videoTags = videoApiInfoJson.data.map((tagInfoItem) => tagInfoItem.tag_name); consoleLogOutput(videoBv, "getVideoTags() API获取的视频标签: ", videoInfoDict[videoBv].videoTags); // 尝试在fetch异步结束后直接屏蔽处理 匹配的屏蔽视频标签 handleBlockedVideoTag(videoElement, videoBv, true); }) .catch((error) => console.error(videoBv, "getVideoTags() Fetch错误:", error)); // 如果已经有 BV号 对应的 Up主名称 Up主Uid 视频时长 记录,跳过 if (!videoInfoDict[videoBv].videoTags) { consoleLogOutput(videoBv, "getVideoTags() 暂时未获取 视频标签 ,跳过剩下主函数步骤"); return true; } } // 判断处理匹配的屏蔽视频标签 function handleBlockedVideoTag(videoElement, videoBv, withinFetch = false) { if (!videoInfoDict[videoBv].videoTags) { consoleLogOutput(videoBv, "handleBlockedVideoTag() 未获取到 视频标签 信息,放弃执行"); return; } // 使用 屏蔽标签数组 与 视频标签 进行匹配 const blockedTagFind = blockedParameter.blockedTagArray.find((blockedTag) => { const blockedTagRegEx = new RegExp(blockedTag); return videoInfoDict[videoBv].videoTags.find((videoTag) => blockedTagRegEx.test(videoTag)); }); if (blockedTagFind) { createOverlay(`屏蔽标签: ${blockedTagFind}`, videoElement, `${videoBv} handleBlockedVideoTag()`); if (withinFetch) { consoleLogOutput(videoBv, "在 getUpNameAndUpUid() 的 Fetch 内执行 handleBlockedVideoTag() 已屏蔽 屏蔽标签"); } else { consoleLogOutput(videoBv, "handleBlockedVideoTag() 已屏蔽 屏蔽标签,跳过剩下主函数步骤"); } return; } // 使用 双重屏蔽标签数组 与 视频标签 进行匹配 const doubleBlockedTagFind = blockedParameter.doubleBlockedTagArray.find((doubleBlockedTag) => { // 以 "|" 分割成数组,同时都能匹配上才是符合 const doubleBlockedTagSplitArray = doubleBlockedTag.split("|"); const doubleBlockedTagRegEx0 = new RegExp(doubleBlockedTagSplitArray[0]); const doubleBlockedTagRegEx1 = new RegExp(doubleBlockedTagSplitArray[1]); if ( videoInfoDict[videoBv].videoTags.find((videoTag) => doubleBlockedTagRegEx0.test(videoTag)) && videoInfoDict[videoBv].videoTags.find((videoTag) => doubleBlockedTagRegEx1.test(videoTag)) ) { return doubleBlockedTag; } }); if (doubleBlockedTagFind) { createOverlay(`双重屏蔽标签: ${doubleBlockedTagFind}`, videoElement, `${videoBv} doubleBlockedTagFind()`); if (withinFetch) { consoleLogOutput( videoBv, "在 getUpNameAndUpUid() 的 Fetch 内执行 handleBlockedVideoTag() 已屏蔽 双重屏蔽标签" ); } else { consoleLogOutput(videoBv, "handleBlockedVideoTag() 已屏蔽 双重屏蔽标签,跳过剩下主函数步骤"); } return; } } // 创建屏蔽叠加层(屏蔽操作) function createOverlay(text, videoElement, operationInfo, setTimeoutStatu = false) { // 获取元素状态,确认是否为已经隐藏 if (videoElement.style.display == "none") { consoleLogOutput(operationInfo, "隐藏视频元素 出现重复处理 跳过"); return; } // 获取子元素,确认是否为已经修改 if (videoElement.firstElementChild.className == "blockedOverlay") { consoleLogOutput(operationInfo, "创建屏蔽叠加层 出现重复处理 跳过"); return; } // 如果启用了隐藏视频模式,直接隐藏元素,跳过剩下的操作 if (blockedParameter.hideVideoModeSwitch == true) { // 判断当前页面URL是否以 https://search.bilibili.com/ 开头 if (window.location.href.startsWith("https://search.bilibili.com/")) { videoElement.parentNode.style.display = "none"; } else { videoElement.style.display = "none"; } return; } // Bug记录: // 位置: 视频播放页面 (即 https://www.bilibili.com/video/BVxxxxxx 页面下) // 行为: 添加屏蔽叠加层 这个操作 只因为 屏蔽标签 的方式来触发时 (如果还触发了 屏蔽标题 屏蔽短时长 这一类,是不会出现这个Bug的。) // 症状: 渲染异常,右侧视频推荐列表的封面图片不可见;评论区丢失;页面头部的搜索框丢失 (div.center-search__bar 丢失); // 处理: 延迟添加 overlay 可解决,先暂时把元素变成透明/模糊的,等3秒,页面完全加载完了,再创建创建屏蔽叠加层,再把元素改回正常。 // 猜测: 我一开始以为是使用 fetch 获取API造成的,因为只有 屏蔽标签 这个操作必须通过 fetch 获取标签信息的。 // 但是出现 屏蔽标题 屏蔽短时长 多种触发的情况下,又不会触发这个Bug了,想不懂,我也不会调试这种加载过程。 // 在 视频播放页面 "card-box" 创建屏蔽叠加层操作作延迟处理 if (videoElement.firstElementChild.className == "card-box" && setTimeoutStatu == false) { // 元素先改模糊 // videoElement.style.opacity = "0"; videoElement.style.filter = "blur(5px)"; // 延迟3秒 setTimeout(() => { // 创建屏蔽叠加层 createOverlay(text, videoElement, `${operationInfo} 延迟处理`, true); //元素再改回正常 // videoElement.style.opacity = "1"; videoElement.style.filter = "none"; }, 3000); return; } // 获取 videoElement 的尺寸 const elementRect = videoElement.getBoundingClientRect(); // 叠加层参数(背景) let overlay = document.createElement("div"); overlay.className = "blockedOverlay"; overlay.style.position = "absolute"; overlay.style.width = elementRect.width + "px"; // 使用 videoElement 的宽度 overlay.style.height = elementRect.height + "px"; // 使用 videoElement 的高度 overlay.style.backgroundColor = blockedOverlayColor; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; overlay.style.zIndex = "10"; overlay.style.backdropFilter = "blur(6px)"; overlay.style.borderRadius = "6px"; let overlayText = document.createElement("div"); if (videoElement.firstElementChild.className == "card-box") { overlayText.style.fontSize = "1.25em"; } overlayText.innerText = text; overlayText.style.color = uiTextColor; overlay.appendChild(overlayText); // 添加叠加层为最前面的子元素 videoElement.insertAdjacentElement("afterbegin", overlay); } // -----------------主函数---------------------- // 屏蔽Bilibili上的符合屏蔽条件的视频 function FuckYouBilibiliRecommendationSystem() { // 获取所有包含B站视频相关标签的视频元素 const videoElementArray = getVideoElements(); // 输出整个视频信息字典 consoleLogOutput(Object.keys(videoInfoDict).length, "个视频信息: ", videoInfoDict); // 遍历每个视频元素 for (let videoElement of videoElementArray) { // 是否为已经屏蔽处理过的子元素 if (isAlreadyBlockedChildElement(videoElement)) { // 如果是已经屏蔽处理过的子元素,跳过后续操作 continue; } // 获取视频元素的Bv号和标题 let videoBv = getBvAndTitle(videoElement); if (!videoBv) { // 如果没有拿到Bv号,跳过后续操作 continue; } // 判断处理匹配的屏蔽标题 if (handleBlockedTitle(videoElement, videoBv)) { // 如果匹配成功并屏蔽,跳过后续操作 continue; } // 是否处理短时长视频 if (blockedParameter.blockedShortDuration > 0) { // 获取视频元素的视频时长 getDuration(videoElement, videoBv); // 判断处理匹配的短时长视频 if (handleBlockedShortDuration(videoElement, videoBv)) { // 如果匹配成功并屏蔽,跳过后续操作 continue; } } // 是否处理屏蔽Up主名称或Up主Uid if (blockedParameter.blockedNameOrUidArray.length > 0) { // 获取视频元素的Up主名称和Up主Uid getUpNameAndUpUid(videoElement, videoBv); // 判断处理匹配的屏蔽Up主名称或Up主Uid if (handleBlockedUpNameOrUpUid(videoElement, videoBv)) { // 如果匹配成功并屏蔽,跳过后续操作 continue; } } // 是否处理屏蔽视频标签 if (blockedParameter.blockedTagArray.length > 0 || blockedParameter.doubleBlockedTagArray.length > 0) { // 获取视频元素的视频标签 if (getVideoTags(videoElement, videoBv)) { // 如果没有拿到视频元素的视频标签,跳过后续操作 continue; } // 判断处理匹配的屏蔽视频标签 if (handleBlockedVideoTag(videoElement, videoBv)) { // 如果匹配成功并屏蔽,跳过后续操作 continue; } } } // 输出整个视频信息字典 consoleLogOutput(Object.keys(videoInfoDict).length, "个视频信息: ", videoInfoDict); } // 页面加载完成后运行脚本 window.addEventListener("load", FuckYouBilibiliRecommendationSystem); // 定义 MutationObserver 的回调函数 function mutationCallback(mutationsList, observer) { // 在这里运行你的脚本 FuckYouBilibiliRecommendationSystem(); } // 创建一个 MutationObserver 实例,观察 body 元素的子节点变化 let observer = new MutationObserver(mutationCallback); let targetNode = document.body; // 配置观察器的选项 let config = { childList: true, subtree: true }; // 启动观察器并传入回调函数和配置选项 observer.observe(targetNode, config);