Bilibili 按标签、标题、时长、UP主屏蔽视频

对Bilibili.com的视频卡片元素,以 标签、标题、时长、UP主名称、UP主UID 来判断匹配,添加一个屏蔽叠加层或者隐藏视频。

// ==UserScript==
// @name            Bilibili 按标签、标题、时长、UP主屏蔽视频
// @namespace       https://github.com/tjxwork
// @version         0.5.5
// @note            v0.5.5 修复 视频播放页右侧推荐视频,按UP主名称屏蔽失效,感谢“雪炭翁” 的指出。
// @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
// ==/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, "秒");

            // videoInfoDict[videoBv].videoWidth = videoApiInfoJson.data.dimension.width;
            // videoInfoDict[videoBv].videoHeight = videoApiInfoJson.data.dimension.height;
            // consoleLogOutput(videoBv, "getDuration() API获取的视频分辨率: ", videoInfoDict[videoBv].videoWidth ,"x", videoInfoDict[videoBv].videoHeight);

            // videoInfoDict[videoBv].videoDesc = videoApiInfoJson.data.desc;
            // consoleLogOutput(videoBv, "getDuration() API获取的视频简介: ", videoInfoDict[videoBv].videoUpName);
            
            // 尝试在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*$/, "");
            videoInfoDict[videoBv].videoUpName = videoLinkElement.querySelector("span").innerText;

            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,
                "秒"
            );

            // videoInfoDict[videoBv].videoWidth = videoApiInfoJson.data.dimension.width;
            // videoInfoDict[videoBv].videoHeight = videoApiInfoJson.data.dimension.height;
            // consoleLogOutput(videoBv, "getUpNameAndUpUid() API获取的视频分辨率: ", videoInfoDict[videoBv].videoWidth ,"x", videoInfoDict[videoBv].videoHeight);

            // videoInfoDict[videoBv].videoDesc = videoApiInfoJson.data.desc;
            // consoleLogOutput(videoBv, "getUpNameAndUpUid() API获取的视频简介: ", videoInfoDict[videoBv].videoUpName);


            // 尝试在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);