Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2024-07-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Bilibili 按标签、标题、时长、UP主屏蔽视频
// @namespace       https://github.com/tjxwork
// @version         1.1.5
// @note
// @note            新版本的视频介绍,来拯救一下我可怜的播放量吧 ●︿●
// @note                   应该是目前B站最强的屏蔽视频插件?【tjxgame】
// @note                   https://www.bilibili.com/video/BV1WJ4m1u79n
// @note
// @note            作者的爱发电:https://afdian.com/a/tjxgame
// @note            欢迎订阅支持、提需求,您的赞助支持就是维护更新的最大动力!
// @note
// @note            v1.1.5 修正导致缓存记录对象的 videoLink 记录出错的部分代码; 修改赞助按钮的跳出连接; (我真的是不知道什么鬼运气,去哪哪崩,刚开的爱发电也崩了。)
// @note            v1.1.4 添加新功能:“屏蔽叠加层的提示只显示类型”,有部分用户可能连命中的屏蔽词都不想看到,但是又倾向使用叠加层模式,所以增加了这个开关。
// @note                   感谢来自爱发电的赞助需求。
// @note            v1.1.3 兼容脚本处理:[bv2av](http://greasyfork.icu/zh-CN/scripts/398535)(此脚本会将视频链接替换为旧的 AV 号链接),感谢 @Henry-ZHR 的提出;
// @note                   不完善功能修复:每次触发运行时,会将屏蔽叠加背景层与父元素尺寸进行同步,解决了页面布局变化时叠加层不跟随变化,感谢 @Henry-ZHR 的建议;
// @note                   “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏 搜索页——综合 下的 直播卡片
// @note            v1.1.2 添加新功能:“按置顶评论屏蔽”;
// @note                   注意:“按置顶评论屏蔽”、“屏蔽精选评论的视频” 这两个功能都用到了获取评论的API,
// @note                   这个API对请求频率非常敏感,频繁刷新或者开启新页面会导致B站拒绝请求,正常浏览一般不会出现拒绝问题。
// @note            v1.1.1 添加新功能:“屏蔽充电专属的视频”;
// @note            v1.1.0 添加新功能:“屏蔽精选评论的视频”,骗子视频大概率会开启精选评论;
// @note                   “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏视频播放页右侧视频相关的游戏推荐;
// @note                   控制台输出日志优化:现在只有发生变化的时候才会输出;
// @note            v1.0.2 “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏视频播放页右侧最下方的“大家围观的直播”
// @note            v1.0.1 修正了B站旧版首页的顶部推荐条失效的Bug;
// @note                   如果用旧版首页只是想要更多的顶部推荐的话,建议使用 bilibili-app-recommend 来获取更多的推荐。
// @note                   现在版本B站首页的推荐卡片有广告的问题,可以通过本脚本的 “隐藏首页等页面的非视频元素” 功能来解决。
// @note            v1.0.0 菜单UI使用Vue3重构,现在不用担心缩放问题挡住UI了,界面更加现代化;
// @note                   改进了判断逻辑,现在可以使用白名单来避免误杀关注的UP了;
// @note                   新增功能:视频分区屏蔽、播放量屏蔽、点赞率屏蔽、竖屏视频屏蔽、UP主名称正则屏蔽、隐藏非视频元素、白名单避免屏蔽指定UP。
// @description     对Bilibili.com的视频卡片元素,以 标签、标题、时长、UP主名称、UP主UID 等信息来进行判断匹配,添加一个屏蔽叠加层或者隐藏视频。
// @author          tjxwork
// @license         CC-BY-NC-SA
// @icon            https://www.bilibili.com/favicon.ico
// @match           https://www.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/music/*
// @exclude         https://www.bilibili.com/v/popular/drama/*
// @match           https://search.bilibili.com/*
// @exclude         https://search.bilibili.com/live
// @grant           GM_registerMenuCommand
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_addStyle
// @require         https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-w/vue/3.2.31/vue.global.min.js
// ==/UserScript==

"use strict";

// --------------------参数变量初始化--------------------

// 初始化屏蔽参数变量,从 油猴扩展存储 读取到 blockedParameter
let blockedParameter = GM_getValue("GM_blockedParameter", {
    // 屏蔽标题
    blockedTitle_Switch: true,
    blockedTitle_UseRegular: true,
    blockedTitle_Array: [],

    // 屏蔽Up主和Uid
    blockedNameOrUid_Switch: true,
    blockedNameOrUid_UseRegular: false,
    blockedNameOrUid_Array: [],

    // 屏蔽视频分区
    blockedVideoPartitions_Switch: true,
    blockedVideoPartitions_UseRegular: false,
    blockedVideoPartitions_Array: [],

    // 屏蔽标签
    blockedTag_Switch: true,
    blockedTag_UseRegular: true,
    blockedTag_Array: [],

    // 屏蔽双重屏蔽标签
    doubleBlockedTag_Switch: true,
    doubleBlockedTag_UseRegular: true,
    doubleBlockedTag_Array: [],

    // 屏蔽短时长视频
    blockedShortDuration_Switch: false,
    blockedShortDuration: 0,

    // 屏蔽低播放量视频
    blockedBelowVideoViews_Switch: false,
    blockedBelowVideoViews: 0,

    // 屏蔽低于指定点赞率的视频
    blockedBelowLikesRate_Switch: false,
    blockedBelowLikesRate: 0,

    // 屏蔽竖屏视频
    blockedPortraitVideo_Switch: false,

    // 屏蔽充电专属的视频
    blockedChargingExclusive_Switch: false,

    // 屏蔽精选评论的视频
    blockedFilteredCommentsVideo_Switch: false,

    // 屏蔽置顶评论
    blockedTopComment_Switch: false,
    blockedTopComment_UseRegular: true,
    blockedTopComment_Array: [],

    // 白名单Up主和Uid
    whitelistNameOrUid_Switch: false,
    whitelistNameOrUid_Array: [],

    // 隐藏非视频元素
    hideNonVideoElements_Switch: true,

    // 屏蔽叠加层的提示只显示类型而不显示命中项
    blockedOverlayOnlyDisplaysType_Switch: false,

    // 隐藏视频而非叠加层模式
    hideVideoMode_Switch: false,

    // 控制台输出日志
    consoleOutputLog_Switch: false,
});

// 旧参数适配
function oldParameterAdaptation(obj) {
    //判断是否为旧参数,是的话就修改为新参数结构
    if (Object.prototype.hasOwnProperty.call(obj, "blockedTitleArray")) {
        // 屏蔽标题
        obj["blockedTitle_Switch"] = true;
        obj["blockedTitle_UseRegular"] = true;
        obj["blockedTitle_Array"] = obj["blockedTitleArray"];
        delete obj["blockedTitleArray"];

        // 屏蔽Up主和Uid
        obj["blockedNameOrUid_Switch"] = true;
        obj["blockedNameOrUid_UseRegular"] = true;
        obj["blockedNameOrUid_Array"] = obj["blockedNameOrUidArray"];
        delete obj["blockedNameOrUidArray"];

        // 屏蔽视频分区
        obj["blockedVideoPartitions_Switch"] = false;
        obj["blockedVideoPartitions_UseRegular"] = false;
        obj["blockedVideoPartitions_Array"] = [];

        // 屏蔽标签
        obj["blockedTag_Switch"] = true;
        obj["blockedTag_UseRegular"] = true;
        obj["blockedTag_Array"] = obj["blockedTagArray"];
        delete obj["blockedTagArray"];

        // 屏蔽双重屏蔽标签
        obj["doubleBlockedTag_Switch"] = true;
        obj["doubleBlockedTag_UseRegular"] = true;
        obj["doubleBlockedTag_Array"] = obj["doubleBlockedTagArray"];
        delete obj["doubleBlockedTagArray"];

        // 屏蔽短时长视频
        obj["blockedShortDuration_Switch"] = true;

        // 白名单Up主和Uid
        obj["whitelistNameOrUid_Switch"] = false;
        obj["whitelistNameOrUid_Array"] = [];

        // 隐藏视频而非叠加层模式
        obj["hideVideoMode_Switch"] = obj["hideVideoModeSwitch"];
        delete obj["hideVideoModeSwitch"];

        // 控制台输出日志
        obj["consoleOutputLog_Switch"] = obj["consoleOutputLogSwitch"];
        delete obj["consoleOutputLogSwitch"];
    }
}
oldParameterAdaptation(blockedParameter);

// --------------------菜单UI部分--------------------

// 菜单UI的CSS,使用 GM_addStyle 注入 CSS
GM_addStyle(`
:root {
    /* 主窗体背景色 */
    --uiBackgroundColor: rgb(48, 48, 48);
    /* 输入模块背景色 */
    --uiInputContainerBackgroundColor: rgb(64, 64, 64);
    /* 输入框背景色 */
    --uiInputBoxBackgroundColor: rgb(89, 89, 89);
    /* 滚动条背景色 */
    --uiScrollbarBackgroundColor: rgb(141, 141, 141);
    /* 文字颜色 */
    --uiTextColor: rgb(250, 250, 250);
    /* 按钮色 */
    --uiButtonColor: rgb(0, 174, 236);
    /* 边框色 */
    --uiBorderColor: rgba(0, 0, 0, 0);
    /* 提醒框背景色 */
    --uiPromptBoxColor: rgb(42, 44, 53);
    /* 屏蔽叠加层背景色 */
    --blockedOverlayColor: rgba(60, 60, 60, 0.85);
    /* 字体大小 */
    --fontSize: 14px;
    /* 行高 */
    --lineHeight: 24px;
    /* 圆角 */
    --borderRadius: 4px;
}

/* 菜单UI */
#blockedMenuUi {
    font-size: var(--fontSize);
    position: fixed;
    bottom: 4vh;
    right: 2vw;
    z-index: 1005;
    width: 460px;
    max-height: 90vh;
    overflow-y: auto;
    background-color: var(--uiBackgroundColor);
}

#blockedMenuUi,
#blockedMenuUi * {
    color: var(--uiTextColor);
    box-sizing: border-box;
    border-style: solid;
    border-width: 0px;
    border-color: var(--uiBorderColor);
    border-radius: var(--borderRadius);
    line-height: var(--lineHeight);
    vertical-align: middle;
    font-family: "Cascadia Mono", Monaco, Consolas, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif;
}

/* 滚动条 */
#blockedMenuUi::-webkit-scrollbar,
#blockedMenuUi ul::-webkit-scrollbar {
    width: 7px;
}

/* 滚动条 轨道*/
#blockedMenuUi::-webkit-scrollbar-track,
#blockedMenuUi ul::-webkit-scrollbar-track {
    background: var(--uiScrollbarBackgroundColor);
    border-radius: 7px;
}

/* 滚动条 滑块*/
#blockedMenuUi::-webkit-scrollbar-thumb,
#blockedMenuUi ul::-webkit-scrollbar-thumb {
    background: var(--uiInputContainerBackgroundColor);
    border-radius: 7px;
}

/* 滚动条 滑块 鼠标经过 */
#blockedMenuUi::-webkit-scrollbar-thumb:hover,
#blockedMenuUi ul::-webkit-scrollbar-thumb:hover {
    background: var(--uiInputBoxBackgroundColor);
    border-radius: 7px;
}

/* 滚动条 滑块 鼠标点击 */
#blockedMenuUi::-webkit-scrollbar-thumb:active,
#blockedMenuUi ul::-webkit-scrollbar-thumb:active {
    background: var(--uiButtonColor);
    border-radius: 7px;
}

#menuTitle {
    font-size: 17px;
    text-align: center;
    margin: 10px;
}

.menuOptions {
    background-color: var(--uiInputContainerBackgroundColor);
    padding: 10px;
    margin: 0 10px;
    margin-bottom: 10px;
}

.titleLabelLeft {
    display: inline-block;
    width: 275px;
    margin-bottom: 5px;
}

.titleLabelRight {
    display: inline-block;
    margin-bottom: 5px;
}

#blockedMenuUi label {
    font-size: 16px;
    vertical-align: middle;
}

#blockedMenuUi input {
    background-color: var(--uiInputBoxBackgroundColor);
    font-size: var(--fontSize);
    line-height: var(--lineHeight);
    border-radius: var(--borderRadius);
    padding: 0 5px;
    margin-bottom: 5px;
    width: 360px;
    vertical-align: middle;
}

#blockedMenuUi input[type="number"] {
    width: 4em;
    margin: 0 5px;
    padding: 0 5px;
    text-align: right;
    appearance: none;
}

#blockedMenuUi input[type="number"]::-webkit-inner-spin-button,
#blockedMenuUi input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

#blockedMenuUi input[type="checkbox"] {
    width: 16px;
    height: 16px;
    margin: 0;
    margin-bottom: 2.5px;
    margin-right: 5px;
    appearance: none;
    border: 1.5px solid var(--uiTextColor);
    border-radius: 8px;
}

#blockedMenuUi input[type="checkbox"]:checked {
    border: 3px solid;
    background-color: var(--uiButtonColor);
}

#blockedMenuUi button {
    line-height: var(--lineHeight);
    border-radius: var(--borderRadius);
    padding: 0;
    margin-bottom: 5px;
    margin-left: 5px;
    width: 45px;
    vertical-align: middle;
    background-color: var(--uiButtonColor);
    transition: background-color 0.1s ease;
}

#blockedMenuUi button:hover {
    background-color: rgb(17, 154, 204);
}

#blockedMenuUi button:active {
    background-color: rgb(62, 203, 255);
}

#blockedMenuUi ul {
    background-color: var(--uiInputBoxBackgroundColor);
    font-size: 14px;
    padding: 5px 5px 0px 0px;
    margin-inline: 0px;
    margin: 0;
    width: 100%;
    min-height: 34px;
    max-height: 92px;
    overflow-y: auto;
}

#blockedMenuUi li {
    line-height: var(--lineHeight);
    border-radius: var(--borderRadius);
    display: inline-block;
    padding: 0 5px;
    margin-bottom: 5px;
    margin-left: 5px;
    vertical-align: middle;
    background-color: var(--uiButtonColor);
}


#blockedMenuUi li button {
    width: 20px;
    margin: 0px;
    padding: 0 0 3px 0;
    font-size: 24px;
    line-height: 18px;
    border: 0px;
}

#blockedMenuUi li button:hover {
    background-color: var(--uiButtonColor);
    color: rgb(221, 221, 221);
}

#blockedMenuUi li button:active {
    background-color: var(--uiButtonColor);
    color: var(--uiButtonColor);
}

#blockedMenuUi textarea {
    background-color: var(--uiInputBoxBackgroundColor);
    font-size: 14px;
    padding: 0 5px;
    width: 100%;
    resize: none;
}

#menuButtonContainer {
    position: sticky;
    right: 0;
    bottom: 0;
    width: 100%;
    background-color: var(--uiBackgroundColor);
    margin-top: -10px;
}

#menuButtonContainer button {
    line-height: var(--lineHeight);
    border-radius: var(--borderRadius);
    font-size: 16px;
    border: 0;
    padding: 0;
    margin-top: 10px;
    margin-bottom: 10px;
    margin-left: 10px;
    height: 45px;
    width: 45px;
    vertical-align: middle;
    background-color: var(--uiButtonColor);
}

#menuButtonContainer label {
    line-height: 45px;
    border-radius: var(--borderRadius);
    display: inline-block;
    border: 0;
    padding: 0;
    margin: 10px 20px;
    height: 45px;
    width: 130px;
    vertical-align: middle;
    text-align: center;
    background-color: var(--uiInputBoxBackgroundColor);
    transition: opacity 1s;
}

/* 支付宝微信二维码 */
#alipayWeChatQrCode {
    position: fixed;
    top: 52%;
    left: 16%;
    transform: translate(0%, -50%);
    box-shadow: 0 8px 8px rgb(85 85 85 / 85%);
}

`);

// 菜单UI的HTML
let menuUiHTML = `

<div id="blockedMenuUi">
    <div id="menuTitle">Bilibili按标签、标题、时长、UP主屏蔽视频 v1.1.4</div>

    <div id="menuOptionsList">
        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTitle_Switch" />按标题屏蔽 </label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTitle_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder="多项输入请用英文逗号间隔" spellcheck="false"
                v-model="tempInputValue.blockedTitle_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'blockedTitle_Array')">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.blockedTitle_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.blockedTitle_Array)">×</button>
                </li>
            </ul>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedNameOrUid_Switch" />按UP名称或Uid屏蔽</label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.blockedNameOrUid_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder="多项输入请用英文逗号间隔" spellcheck="false"
                v-model="tempInputValue.blockedNameOrUid_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'blockedNameOrUid_Array')">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.blockedNameOrUid_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.blockedNameOrUid_Array)">×</button>
                </li>
            </ul>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedVideoPartitions_Switch" />按视频分区屏蔽</label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.blockedVideoPartitions_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder="多项输入请用英文逗号间隔" spellcheck="false"
                v-model="tempInputValue.blockedVideoPartitions_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'blockedVideoPartitions_Array')">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.blockedVideoPartitions_Array">
                    {{value}}<button
                        @click="delArrayButton(index, menuUiSettings.blockedVideoPartitions_Array)">×</button>
                </li>
            </ul>
        </div>


        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTag_Switch" />按标签屏蔽</label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTag_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder="多项输入请用英文逗号间隔" spellcheck="false"
                v-model="tempInputValue.blockedTag_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'blockedTag_Array')">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.blockedTag_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.blockedTag_Array)">×</button>
                </li>
            </ul>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.doubleBlockedTag_Switch" />按双重标签屏蔽</label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.doubleBlockedTag_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder='多项输入请用英文逗号间隔(以"A标签|B标签"格式添加)' spellcheck="false"
                v-model="tempInputValue.doubleBlockedTag_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'doubleBlockedTag_Array' )">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.doubleBlockedTag_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.doubleBlockedTag_Array)">×</button>
                </li>
            </ul>
        </div>


        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTopComment_Switch" />按置顶评论屏蔽 </label>
            </div>

            <div class="titleLabelRight">
                <label><input type="checkbox" v-model="menuUiSettings.blockedTopComment_UseRegular" />启用正则</label>
            </div>

            <input type="text" placeholder="多项输入请用英文逗号间隔" spellcheck="false"
                v-model="tempInputValue.blockedTopComment_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'blockedTopComment_Array')">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.blockedTopComment_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.blockedTopComment_Array)">×</button>
                </li>
            </ul>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox"
                        v-model="menuUiSettings.whitelistNameOrUid_Switch" />按UP名称或Uid避免屏蔽(白名单)</label>
            </div>

            <input type="text" placeholder='多项输入请用英文逗号间隔' spellcheck="false"
                v-model="tempInputValue.whitelistNameOrUid_Array" /><button
                @click="addArrayButton(tempInputValue, menuUiSettings, 'whitelistNameOrUid_Array' )">添加</button>

            <ul>
                <li v-for="(value, index) in menuUiSettings.whitelistNameOrUid_Array">
                    {{value}}<button @click="delArrayButton(index, menuUiSettings.whitelistNameOrUid_Array)">×</button>
                </li>
            </ul>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox" v-model="menuUiSettings.blockedShortDuration_Switch" />屏蔽低于指定时长的视频</label>
            </div>
            <input type="number" spellcheck="false" v-model="menuUiSettings.blockedShortDuration" />
            <label>秒</label>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox"
                        v-model="menuUiSettings.blockedBelowVideoViews_Switch" />屏蔽低于指定播放量的视频</label>
            </div>
            <input type="number" spellcheck="false" v-model="menuUiSettings.blockedBelowVideoViews" />
            <label>次</label>
        </div>

        <div class="menuOptions">
            <div class="titleLabelLeft">
                <label><input type="checkbox"
                        v-model="menuUiSettings.blockedBelowLikesRate_Switch" />屏蔽低于指定点赞率的视频</label>
            </div>
            <input type="number" spellcheck="false" v-model="menuUiSettings.blockedBelowLikesRate" />
            <label>%</label>
        </div>


        <div class="menuOptions">
            <label><input type="checkbox" v-model="menuUiSettings.blockedPortraitVideo_Switch" />屏蔽竖屏视频</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox" v-model="menuUiSettings.blockedChargingExclusive_Switch" />屏蔽充电专属的视频</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox"
                    v-model="menuUiSettings.blockedFilteredCommentsVideo_Switch" />屏蔽精选评论的视频</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox"
                    v-model="menuUiSettings.hideNonVideoElements_Switch" />隐藏首页等页面的非视频元素</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox" v-model="menuUiSettings.blockedOverlayOnlyDisplaysType_Switch" />屏蔽叠加层的提示只显示类型</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox" v-model="menuUiSettings.hideVideoMode_Switch" />隐藏视频而不是使用叠加层覆盖</label>
        </div>

        <div class="menuOptions">
            <label><input type="checkbox" v-model="menuUiSettings.consoleOutputLog_Switch" />控制台输出日志开关</label>
        </div>

    </div>

    <div id="menuButtonContainer">
        <button @click="refreshButton()">读取</button>
        <button @click="saveButton()">保存</button>
        <button @click="closeButton()">关闭</button>
        <button @click="authorButton()">作者</button>
        <button @click="supportButton()">赞助</button>


        <label :style="{ opacity: tempInputValue.promptText_Opacity }"
            v-show="tempInputValue.promptText_Switch">{{tempInputValue.promptText}}</label>
    </div>

    <div id="alipayWeChatQrCode" v-show="tempInputValue.QrCode_Switch">
        <img src="https://tc.dhmip.cn/imgs/2023/12/09/a8e5fff3320dc195.png" alt="感谢赞助">
    </div>

</div>

`;

// 菜单UI
function blockedMenuUi() {
    // 检查页面中是否已经存在这个元素
    if (!document.getElementById("blockedMenuUi")) {
        // 如果不存在,将菜单弹窗添加到页面
        // 创建Div作为菜单容器
        let menuUi = document.createElement("div");
        menuUi.innerHTML = menuUiHTML;
        document.body.appendChild(menuUi);
    } else {
        console.log("菜单 #blockedMenuUi 已存在");
        return;
    }

    // 让油猴脚本的Vue代码能网页中正常工作。
    unsafeWindow.Vue = Vue;

    const { createApp, reactive, toRaw } = Vue;

    createApp({
        setup() {
            // 设置选项数据
            const menuUiSettings = reactive({});

            // 临时存储的各数组对应的输入值
            const tempInputValue = reactive({
                blockedTitle_Array: "",
                blockedNameOrUid_Array: "",
                blockedVideoPartitions_Array: "",
                blockedTag_Array: "",
                doubleBlockedTag_Array: "",
                blockedTopComment_Array: "",
                whitelistNameOrUid_Array: "",
                // 临时提示文本
                promptText_Switch: true,
                promptText_Opacity: 0,
                promptText: "",
                // 二维码显示开关
                QrCode_Switch: false,
            });

            function showPromptText(text) {
                // tempInputValue.promptText_Switch = true; // 显示 label 元素
                tempInputValue.promptText_Opacity = 1;
                tempInputValue.promptText = text;
                // 1.5秒后隐藏 label 元素
                setTimeout(() => {
                    // tempInputValue.promptText_Switch = false;
                    tempInputValue.promptText_Opacity = 0;
                }, 1500);
            }

            // 添加数组项目
            const addArrayButton = (tempInputValue, menuUiSettings, keyName) => {
                // 确保 menuUiSettings[keyName] 是一个数组
                if (!Array.isArray(menuUiSettings[keyName])) {
                    menuUiSettings[keyName] = [];
                }
                // 双重标签的特殊处理 判断是否为空
                if (keyName == "doubleBlockedTag_Array" && tempInputValue[keyName].trim()) {
                    // 使用 split 按逗号分隔,然后映射去除每个标签的首尾空白
                    const items = tempInputValue[keyName]
                        .split(",")
                        .map((item) => item.split("|").map((str) => str.trim()))
                        .filter((subArray) => subArray.length === 2 && subArray.every((str) => str !== ""));

                    items.forEach((secondSplitItem) => {
                        // 将两个标签重新组合成一个字符串,并添加到设置数据中
                        const formattedItem = secondSplitItem.join("|");
                        menuUiSettings[keyName].push(formattedItem);
                    });

                    // 清空输入框内容
                    tempInputValue[keyName] = "";

                    return;
                }

                // 判断是否为空
                if (tempInputValue[keyName].trim()) {
                    // 用逗号分隔值并去除每项的空格后添加到数组
                    const items = tempInputValue[keyName].split(",").map((item) => item.trim());

                    menuUiSettings[keyName].push(...items);

                    // 清空输入框内容
                    tempInputValue[keyName] = "";
                }
            };

            //删除数组项目
            const delArrayButton = (index, array) => {
                //splice(要删除元素的索引位置, 要删除的元素数量)
                array.splice(index, 1);
            };

            // 读取按钮 深拷贝函数,递归处理嵌套对象,普通对象 to 普通对象/响应式对象
            function deepCopy(source, target) {
                for (let key in source) {
                    if (typeof source[key] === "object" && source[key] !== null) {
                        target[key] = Array.isArray(source[key]) ? [] : {}; // 根据类型创建空对象或数组
                        deepCopy(source[key], target[key]); // 递归拷贝子对象
                    } else {
                        target[key] = source[key]; // 复制基本类型和函数等
                    }
                }
            }

            // 读取按钮
            const refreshButton = () => {
                // 使用 deepCopy 函数进行深拷贝
                deepCopy(blockedParameter, menuUiSettings);

                showPromptText("读取数据");
            };

            // 保存按钮 深拷贝函数,递归处理响应式对象,响应式对象 to 普通对象
            function deepCopyReactiveObject(reactiveObj, targetObj) {
                for (let key in reactiveObj) {
                    const rawValue = toRaw(reactiveObj[key]); // 获取属性的原始值

                    if (typeof rawValue === "object" && rawValue !== null) {
                        targetObj[key] = Array.isArray(rawValue) ? [] : {}; // 根据类型创建空对象或数组
                        deepCopyReactiveObject(rawValue, targetObj[key]); // 递归处理嵌套的响应式子对象
                    } else {
                        targetObj[key] = rawValue; // 复制基本类型和函数等
                    }
                }
            }

            // 保存按钮
            const saveButton = () => {
                // 将响应式对象深拷贝到普通对象 blockedParameter
                deepCopyReactiveObject(menuUiSettings, blockedParameter);

                // 将全局屏蔽参数对象变量 blockedParameter 保存到油猴扩展存储中
                GM_setValue("GM_blockedParameter", blockedParameter);

                showPromptText("保存数据");

                // 触发一次主函数,以立刻生效
                FuckYouBilibiliRecommendationSystem();
            };

            // 关闭按钮
            const closeButton = () => {
                // 获取需要删除的元素
                let elementToRemove = document.getElementById("blockedMenuUi");

                // 确保元素存在再进行删除操作
                if (elementToRemove) {
                    // 先获取父元素
                    let parentElement = elementToRemove.parentNode;

                    // 在父元素删除指定的元素
                    parentElement.removeChild(elementToRemove);
                }
            };

            // 作者主页
            const authorButton = () => {
                setTimeout(() => {
                    window.open("https://space.bilibili.com/351422438", "_blank");
                }, 1000);
                showPromptText("欢迎关注!");
            };

            // 赞助作者
            const supportButton = () => {
                if (!tempInputValue.QrCode_Switch) {
                    setTimeout(() => {
                        window.open("https://afdian.com/a/tjxgame", "_blank");
                    }, 1000);
                    tempInputValue.QrCode_Switch = true;
                } else {
                    tempInputValue.QrCode_Switch = false;
                }

                showPromptText("感谢老板!");
            };

            // 打开菜单时,先加载一次数据
            refreshButton();

            return {
                menuUiSettings,
                tempInputValue,
                addArrayButton,
                delArrayButton,
                refreshButton,
                saveButton,
                closeButton,
                supportButton,
                authorButton,
            };
        },
    }).mount("#blockedMenuUi");
}

// 在油猴扩展中添加脚本菜单选项
GM_registerMenuCommand("屏蔽参数面板", blockedMenuUi);

// -----------------------逻辑处理部分--------------------------

// 视频的详细信息对象,以videoBv为键, 用于同窗口内的缓存查询
let videoInfoDict = {};

// 上次输出的视频详细信息对象,用于控制台判断是否输出日志
let lastConsoleVideoInfoDict = {};

// videoInfoDict 的参考内容结构
// videoInfoDict = {
//     BV12i4y1e73B: {
//         videoLink: "https://www.bilibili.com/video/BV12i4y1e73B/",
//         videoTitle: "B站按 标签 标题 时长 UP主来屏蔽视频 油猴插件【tjxgame】",
//         videoUpName: "tjxgame",
//         videoUpUid: 351422438,
//         videoPartitions: "软件应用",
//         videoTags: [
//             "科技2023年终总结",
//             "视频",
//             "教程",
//             "tjxwork",
//             "软件分享",
//             "插件",
//             "标签",
//             "屏蔽",
//             "油猴",
//             "tjxgame",
//             "2023热门年度盘点",
//         ],
//         topComment : "大更新,新视频!\nhttps://www.bilibili.com/video/BV1WJ4m1u79n/\n\nv1.0.0 菜单UI使用Vue3重构,现在不用担心缩放问题挡住UI了,界面更加现代化;\n改进了判断逻辑,现在可以使用白名单来避免误杀关注的UP了;\n新增功能:视频分区屏蔽、播放量屏蔽、点赞率屏蔽、竖屏视频屏蔽、UP主名称正则屏蔽、隐藏非视频元素、白名单避免屏蔽指定UP。"
//         whiteListTargets: true,
//         videoDuration: 259,
//         videoView: 9067,
//         videoLike: 507,
//         videoLikesRate: "5.59",
//         videoResolution: {
//             width: 3840,
//             height: 2160,
//         },
//         videoChargingExclusive : false
//         filteredComments: false,
//         blockedTarget: true,
//         triggeredBlockedRules: [
//             "屏蔽短时长视频: 259秒",
//             "屏蔽低播放量: 9067次",
//             "屏蔽低点赞率: 5.59%",
//             "屏蔽标题: tjxgame",
//             "屏蔽UP: tjxgame",
//             "屏蔽分区: 软件应用",
//             "屏蔽标签: 标签",
//             "屏蔽双重标签: 油猴,插件",
//         ],
//         lastVideoInfoApiRequestTime: "2024-06-21T09:17:10.389Z",
//         lastVideoTagApiRequestTime: "2024-06-21T09:17:10.389Z",
//         lastVideoCommentsApiRequestTime: "2024-06-21T09:17:10.389Z",
//     },
// };

// 日志输出,根据 consoleOutputLog_Switch 标志来决定是否输出日志
function consoleLogOutput(...args) {
    // 启用控制台日志输出
    if (blockedParameter.consoleOutputLog_Switch) {
        // 获取当前时间的时分秒毫秒部分
        let now = new Date();
        let hours = now.getHours().toString().padStart(2, "0");
        let minutes = now.getMinutes().toString().padStart(2, "0");
        let seconds = now.getSeconds().toString().padStart(2, "0");
        let milliseconds = now.getMilliseconds().toString().padStart(3, "0");

        // 将时间信息添加到日志消息中
        let logTime = `${hours}:${minutes}:${seconds}.${milliseconds}`;

        // 合并时间信息和 args 成为一个数组
        let logArray = [logTime, ...args];
        console.log(...logArray);
    }
}

// 简单对比对象是否不同
function objectDifferent(obj1, obj2) {
    if (Object.keys(obj1).length !== Object.keys(obj2).length) {
        return true;
    }
    for (const key in obj1) {
        if (obj1[key] !== obj2[key]) {
            return true;
        }
    }
    return false;
}

// 获取视频元素
function getVideoElements() {
    // // 获取所有有可能是视频元素的标签 (BewlyBewly插件的首页特殊处理)
    // let bewlyBewly = document.getElementById("bewly");
    // if (bewlyBewly) {

    //     // BewlyBewly插件使用shadowDOM,要在shadowDOM下面找元素
    //     let shadowRoot = bewlyBewly.shadowRoot;
    //     videoElements = shadowRoot.querySelectorAll("div.video-card.group");

    //     // 过滤掉没有包含a标签的元素
    //     videoElements = Array.from(videoElements).filter((element) => element.querySelector("a"));

    //     // 返回处理后的结果
    //     return videoElements;
    // }
    // BewlyBewly 更新后失效……

    // 获取所有有可能是视频元素的标签
    let 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)
        // li.rank-item 排行榜(https://www.bilibili.com/v/popular/rank/all)
        // 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, li.rank-item, div.video-card-reco, div.video-card-common, div.rank-wrap"
    );

    // 过滤掉没有包含a标签的元素
    videoElements = Array.from(videoElements).filter((element) => element.querySelector("a"));

    // 判断是否存在旧版首页的顶部推荐条,为空的情况下再进行剔除广告元素,因为旧版首页的顶部推荐条,和新版的广告元素的类值一样……
    if (document.querySelector("div.recommend-container__2-line") == null) {
        // 过滤掉 CSS类刚好为 'bili-video-card is-rcmd' 的元素,因为是广告。
        videoElements = Array.from(videoElements).filter(
            (element) => element.classList.value !== "bili-video-card is-rcmd"
        );
    }

    // 返回处理后的结果
    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;
    // }
}

// 标记为屏蔽目标,并记录命中的规则
function markAsBlockedTarget(videoBv, blockedType, blockedItem) {
    // 将该 Bv号 标记为屏蔽目标
    videoInfoDict[videoBv].blockedTarget = true;

    // 确保 videoInfoDict[videoBv].triggeredBlockedRules 已定义为数组
    if (!videoInfoDict[videoBv].triggeredBlockedRules) {
        videoInfoDict[videoBv].triggeredBlockedRules = [];
    }

    let blockedRulesItem;

    // 屏蔽叠加层的提示只显示类型而不显示命中项
    if (blockedParameter.blockedOverlayOnlyDisplaysType_Switch) {
        blockedRulesItem = blockedType;
    } else {
        blockedRulesItem = blockedType + ": " + blockedItem;
    }

    // 检查是否已经这条记录
    if (!videoInfoDict[videoBv].triggeredBlockedRules.includes(blockedRulesItem)) {
        // 将触发屏蔽的原因添加到 videoInfoDict[videoBv].triggeredBlockedRules
        videoInfoDict[videoBv].triggeredBlockedRules.push(blockedRulesItem);
    }
}

// 网页获取视频元素的Bv号和标题
function getBvAndTitle(videoElement) {
    // 从视频元素中获取所有a标签链接
    const videoLinkElements = videoElement.querySelectorAll("a");

    // Bv号
    let videoBv;

    // Av号转Bv号,用于兼容 bv2av (http://greasyfork.icu/zh-CN/scripts/398535),代码来源:https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/bvid_desc.html#bv-av%E7%AE%97%E6%B3%95
    function av2bv(aid) {
        const XOR_CODE = 23442827791579n;
        const MASK_CODE = 2251799813685247n;
        const MAX_AID = 1n << 51n;
        const BASE = 58n;
        const data = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf";
        const bytes = ["B", "V", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0"];
        let bvIndex = bytes.length - 1;
        let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE;
        while (tmp > 0) {
            bytes[bvIndex] = data[Number(tmp % BigInt(BASE))];
            tmp = tmp / BASE;
            bvIndex -= 1;
        }
        [bytes[3], bytes[9]] = [bytes[9], bytes[3]];
        [bytes[4], bytes[7]] = [bytes[7], bytes[4]];
        return bytes.join("");
    }

    // 循环处理所有a标签链接
    for (let videoLinkElement of videoLinkElements) {
        // 已经有Bv号不需要继续了,跳过
        if (videoBv) {
            continue;
        }

        // 处理排行榜的多链接特殊情况,符合就跳过
        if (videoLinkElement.className == "other-link") {
            continue;
        }

        // 获取的链接,如果是Av链接的格式
        let videoAvTemp = videoLinkElement.href.match(/\/(av)(\d+)/);
        if (videoAvTemp) {
            // 从链接中获取Av号 转为 Bv号
            videoBv = av2bv(videoAvTemp[2]);
        }

        // 获取的链接,如果是Bv链接的格式
        let videoBvTemp = videoLinkElement.href.match(/\/(BV\w+)/);
        if (videoBvTemp) {
            // 从链接中获取到 视频Bv号
            videoBv = videoBvTemp[1];
        }

        // 没拿Bv号不需要继续了,跳过
        if (!videoBv) {
            continue;
        }

        // 确保 videoInfoDict[videoBv] 已定义
        if (!videoInfoDict[videoBv]) {
            videoInfoDict[videoBv] = {};
        }

        // 视频链接
        videoInfoDict[videoBv].videoLink = videoLinkElement.href;
    }

    // 没有拿到Bv号,提前结束
    if (!videoBv) {
        consoleLogOutput(videoElement, "getBvAndTitle() 没有拿到Bv号 提前结束 跳过剩下主函数步骤");
        return false;
    }

    // 视频标题 , 从视频元素中获取第一个带 title 属性且不为 span 的标签
    videoInfoDict[videoBv].videoTitle = videoElement.querySelector("[title]:not(span)").title;

    return videoBv;
}

// 处理匹配的屏蔽标题
function handleBlockedTitle(videoBv) {
    // 判断是否拿到视频标题
    if (!videoInfoDict[videoBv].videoTitle) {
        return;
    }

    // 记录触发的规则内容
    // let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.blockedTitle_UseRegular) {
        // 使用 屏蔽标题数组 与 视频标题 进行匹配
        const blockedTitleHitItem = blockedParameter.blockedTitle_Array.find((blockedTitleItem) => {
            // 正则化屏蔽标题
            const blockedTitleRegEx = new RegExp(blockedTitleItem);
            // 判断 正则化的屏蔽标题 是否匹配 视频标题
            if (blockedTitleRegEx.test(videoInfoDict[videoBv].videoTitle)) {
                // blockedRulesItemText = videoInfoDict[videoBv].videoTitle;
                return true;
            }
        });

        if (blockedTitleHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽标题", blockedTitleHitItem);
        }
    } else {
        // 使用 屏蔽标题数组 与 视频标题 进行匹配
        const blockedTitleHitItem = blockedParameter.blockedTitle_Array.find((blockedTitleItem) => {
            // 判断 屏蔽标题 是否匹配 视频标题
            if (blockedTitleItem === videoInfoDict[videoBv].videoTitle) {
                // blockedRulesItemText = videoInfoDict[videoBv].videoTitle;
                return true;
            }
        });

        if (blockedTitleHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽标题", blockedTitleHitItem);
        }
    }
}

// 网页获取视频UP名和UpUid (已经有API获取为什么还要网页获取?因为快……)
function getNameAndUid(videoElement, videoBv) {
    // 如果已经有 BV号 对应的 Up主名称 Up主Uid 记录,跳过
    if (videoInfoDict[videoBv].videoUpName && videoInfoDict[videoBv].videoUpUid) {
        return;
    }

    // 从视频元素中获取所有a标签链接
    const videoLinkElements = videoElement.querySelectorAll("a");

    // 循环处理所有a标签链接
    for (let videoLinkElement of videoLinkElements) {
        // 获取的链接,如果与 Uid 的链接格式匹配的话
        const uidLink = videoLinkElement.href.match(/space\.bilibili\.com\/(\d+)/);
        if (uidLink) {
            // 视频UpUid
            videoInfoDict[videoBv].videoUpUid = uidLink[1];

            // 视频Up名称
            videoInfoDict[videoBv].videoUpName = videoLinkElement.querySelector("span").innerText;
        }
    }
}

// API获取视频信息
function getVideoApiInfo(videoBv) {
    // 如果已经有BV号对应的记录,跳过
    if (videoInfoDict[videoBv].videoDuration) {
        return;
    }

    // 当 lastVideoInfoApiRequestTime 上次API获取视频信息的时间存在,并且,和当前的时间差小于3秒时,跳过
    const currentTime = new Date(); //获取当前时间
    if (
        videoInfoDict[videoBv].lastVideoInfoApiRequestTime &&
        currentTime - videoInfoDict[videoBv].lastVideoInfoApiRequestTime < 3000
    ) {
        // consoleLogOutput(videoBv, "getVideoApiInfo() 距离上次 Fetch 获取视频信息还未超过3秒钟");
        return;
    }
    videoInfoDict[videoBv].lastVideoInfoApiRequestTime = currentTime;

    // 通过API获取视频UP信息
    fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${videoBv}`)
        .then((response) => response.json())
        .then((videoApiInfoJson) => {
            // API获取的UP主名称:
            videoInfoDict[videoBv].videoUpName = videoApiInfoJson.data.owner.name;

            // API获取的UP主Uid:
            videoInfoDict[videoBv].videoUpUid = videoApiInfoJson.data.owner.mid;

            // API获取的视频AVid:
            videoInfoDict[videoBv].videoAVid = videoApiInfoJson.data.aid;

            // API获取的视频时长
            videoInfoDict[videoBv].videoDuration = videoApiInfoJson.data.duration;

            // API获取的视频分区
            videoInfoDict[videoBv].videoPartitions = videoApiInfoJson.data.tname;

            // API获取的视频播放数
            videoInfoDict[videoBv].videoView = videoApiInfoJson.data.stat.view;

            // API获取的视频点赞数
            videoInfoDict[videoBv].videoLike = videoApiInfoJson.data.stat.like;

            // 计算视频点赞率保留2位小数
            videoInfoDict[videoBv].videoLikesRate = (
                (videoInfoDict[videoBv].videoLike / videoInfoDict[videoBv].videoView) *
                100
            ).toFixed(2);

            // // API获取的视频投币数
            // videoInfoDict[videoBv].videoCoin = videoApiInfoJson.data.stat.coin;

            // // API获取的视频收藏数
            // videoInfoDict[videoBv].videoFavorite = videoApiInfoJson.data.stat.favorite;

            // // API获取的视频分享数
            // videoInfoDict[videoBv].videoShare = videoApiInfoJson.data.stat.share;

            // // API获取的视频评论数
            // videoInfoDict[videoBv].videoReply = videoApiInfoJson.data.stat.reply;

            // // API获取的视频弹幕数
            // videoInfoDict[videoBv].videoDanmaku = videoApiInfoJson.data.stat.danmaku;

            // API获取的视频是否为充电专属
            videoInfoDict[videoBv].videoChargingExclusive = videoApiInfoJson.data.is_upower_exclusive;

            // API获取的视频分辨率
            if (!videoInfoDict[videoBv].videoResolution) {
                videoInfoDict[videoBv].videoResolution = {};
            }
            videoInfoDict[videoBv].videoResolution.width = videoApiInfoJson.data.dimension.width;
            videoInfoDict[videoBv].videoResolution.height = videoApiInfoJson.data.dimension.height;

            FuckYouBilibiliRecommendationSystem();
        })
        .catch((error) => consoleLogOutput(videoBv, "getVideoApiInfo() Fetch错误:", error));
}

// 处理匹配短时长视频
function handleBlockedShortDuration(videoBv) {
    // 判断是否拿到视频时长
    if (!videoInfoDict[videoBv].videoDuration) {
        return;
    }

    // 判断设置的屏蔽短时长视频值 是否大于 视频时长
    if (blockedParameter.blockedShortDuration > videoInfoDict[videoBv].videoDuration) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(videoBv, "屏蔽短时长视频", videoInfoDict[videoBv].videoDuration + "秒");
    }
}

// 处理 屏蔽低播放量视频
function handleBlockedBelowVideoViews(videoBv) {
    // 判断是否拿到视频播放量
    if (!videoInfoDict[videoBv].videoView) {
        return;
    }

    // 判断设置的屏蔽视频点赞率值 是否大于 视频的点赞率
    if (blockedParameter.blockedBelowVideoViews > videoInfoDict[videoBv].videoView) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(videoBv, "屏蔽低播放量", videoInfoDict[videoBv].videoView + "次");
    }
}

// 处理匹配屏蔽低于指定点赞率的视频
function handleBlockedBelowLikesRate(videoBv) {
    // 判断是否拿到视频点赞数
    if (!videoInfoDict[videoBv].videoLikesRate) {
        return;
    }

    // 判断设置的屏蔽视频点赞率值 是否大于 视频的点赞率
    if (blockedParameter.blockedBelowLikesRate > videoInfoDict[videoBv].videoLikesRate) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(videoBv, "屏蔽低点赞率", videoInfoDict[videoBv].videoLikesRate + "%");
    }
}

// 处理匹配屏蔽竖屏视频
function handleBlockedPortraitVideo(videoBv) {
    // 判断是否拿到视频分辨率
    if (!videoInfoDict[videoBv].videoResolution.width) {
        return;
    }

    // 横向分辨率小于纵向分辨率就是竖屏
    if (videoInfoDict[videoBv].videoResolution.width < videoInfoDict[videoBv].videoResolution.height) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(
            videoBv,
            "屏蔽竖屏视频",
            `${videoInfoDict[videoBv].videoResolution.width} x ${videoInfoDict[videoBv].videoResolution.height}`
        );
    }
}

// 处理匹配 屏蔽充电专属视频
function handleBlockedChargingExclusive(videoBv) {
    // 判断设置的屏蔽充电专属视频是否有启用标记
    if (videoInfoDict[videoBv].videoChargingExclusive) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(videoBv, "屏蔽充电专属的视频", videoInfoDict[videoBv].videoUpName);
    }
}

// 处理匹配的屏蔽Up主名称或Up主Uid
function handleBlockedNameOrUid(videoBv) {
    // 判断是否拿到Up主名称或Up主Uid
    if (!videoInfoDict[videoBv].videoUpUid) {
        return;
    }

    // 记录触发的规则内容
    let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.blockedNameOrUid_UseRegular) {
        // 使用 屏蔽Up名称和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配
        const blockedNameOrUidHitItem = blockedParameter.blockedNameOrUid_Array.find((blockedNameOrUidItem) => {
            // 正则化屏蔽Up主名称、视频Up主Uid
            const blockedNameOrUidRegEx = new RegExp(blockedNameOrUidItem);

            // 只有UP名称有正则的意义,Uid依然是直接对比
            if (blockedNameOrUidRegEx.test(videoInfoDict[videoBv].videoUpName)) {
                blockedRulesItemText = videoInfoDict[videoBv].videoUpName;
                return true;
            }

            if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpUid) {
                blockedRulesItemText = videoInfoDict[videoBv].videoUpUid;
                return true;
            }
        });

        if (blockedNameOrUidHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽UP", blockedRulesItemText);
        }
    } else {
        // 使用 屏蔽Up名称和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配
        const blockedNameOrUidHitItem = blockedParameter.blockedNameOrUid_Array.find((blockedNameOrUidItem) => {
            if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpName) {
                blockedRulesItemText = videoInfoDict[videoBv].videoUpName;
                return true;
            }

            if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpUid) {
                blockedRulesItemText = videoInfoDict[videoBv].videoUpUid;
                return true;
            }
        });

        if (blockedNameOrUidHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽UP", blockedRulesItemText);
        }
    }
}

// 处理匹配的屏蔽视频分区
function handleBlockedVideoPartitions(videoBv) {
    // 判断是否拿到视频分区
    if (!videoInfoDict[videoBv].videoPartitions) {
        return;
    }

    // 记录触发的规则内容
    let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.blockedVideoPartitions_UseRegular) {
        // 使用 屏蔽视频分区数组 与 视频分区 进行匹配
        const blockedVideoPartitionsHitItem = blockedParameter.blockedVideoPartitions_Array.find(
            (blockedVideoPartitionsItem) => {
                // 正则化屏蔽视频标签
                const blockedVideoPartitionsRegEx = new RegExp(blockedVideoPartitionsItem);

                if (blockedVideoPartitionsRegEx.test(videoInfoDict[videoBv].videoPartitions)) {
                    blockedRulesItemText = videoInfoDict[videoBv].videoPartitions;
                    return true;
                }
            }
        );

        if (blockedVideoPartitionsHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽分区", blockedRulesItemText);
        }
    } else {
        // 使用 屏蔽视频分区数组 与 视频分区 进行匹配
        const blockedVideoPartitionsHitItem = blockedParameter.blockedVideoPartitions_Array.find(
            (blockedVideoPartitionsItem) => {
                if (blockedVideoPartitionsItem == videoInfoDict[videoBv].videoPartitions) {
                    blockedRulesItemText = videoInfoDict[videoBv].videoPartitions;
                    return true;
                }
            }
        );

        if (blockedVideoPartitionsHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽分区", blockedRulesItemText);
        }
    }
}

// API获取视频标签
function getVideoApiTags(videoBv) {
    // 如果已经有BV号对应的记录,跳过
    if (videoInfoDict[videoBv].videoTags) {
        return;
    }

    // 当 lastVideoTagApiRequestTime 上次API获取视频标签的时间存在,并且,和当前的时间差小于3秒时,跳过
    const currentTime = new Date(); //获取当前时间
    if (
        videoInfoDict[videoBv].lastVideoTagApiRequestTime &&
        currentTime - videoInfoDict[videoBv].lastVideoTagApiRequestTime < 3000
    ) {
        // consoleLogOutput(videoBv, "getVideoApiTags() 距离上次 Fetch 获取视频信息还未超过3秒钟");
        return;
    }
    videoInfoDict[videoBv].lastVideoTagApiRequestTime = currentTime;

    // 获取视频标签
    fetch(`https://api.bilibili.com/x/web-interface/view/detail/tag?bvid=${videoBv}`)
        .then((response) => response.json())
        .then((videoApiTagsJson) => {
            // API获取标签对象,提取标签名字数组
            videoInfoDict[videoBv].videoTags = videoApiTagsJson.data.map((tagsArray) => tagsArray.tag_name);

            FuckYouBilibiliRecommendationSystem();
        })
        .catch((error) => consoleLogOutput(videoBv, "getVideoApiTags() Fetch错误:", error));
}

// 处理匹配的屏蔽标签
function handleBlockedTag(videoBv) {
    // 判断是否拿到视频标签
    if (!videoInfoDict[videoBv].videoTags) {
        return;
    }

    // 记录触发的规则内容
    let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.blockedTag_UseRegular) {
        // 使用 屏蔽标签数组 与 视频标题数组 进行匹配
        const blockedTagHitItem = blockedParameter.blockedTag_Array.find((blockedTagItem) => {
            // 正则化屏蔽视频标签
            const blockedTagRegEx = new RegExp(blockedTagItem);
            // 使用 屏蔽标签正则 和 视频标题数组 进行匹配
            const videoTagHitItem = videoInfoDict[videoBv].videoTags.find((videoTagItem) =>
                blockedTagRegEx.test(videoTagItem)
            );

            if (videoTagHitItem) {
                blockedRulesItemText = videoTagHitItem;
                return true;
            }
        });

        if (blockedTagHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽标签", blockedRulesItemText);
        }
    } else {
        // 使用 屏蔽标签数组 与 视频标题数组 进行匹配
        const blockedTagHitItem = blockedParameter.blockedTag_Array.find((blockedTagItem) => {
            // 使用 屏蔽标签 和 视频标题数组 进行匹配
            const videoTagHitItem = videoInfoDict[videoBv].videoTags.find(
                (videoTagItem) => blockedTagItem == videoTagItem
            );

            if (videoTagHitItem) {
                blockedRulesItemText = videoTagHitItem;
                return true;
            }
        });

        if (blockedTagHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽标签", blockedRulesItemText);
        }
    }
}

// 处理匹配屏蔽双重屏蔽标签
function handleDoubleBlockedTag(videoBv) {
    // 判断是否拿到视频标签
    if (!videoInfoDict[videoBv].videoTags) {
        return;
    }

    // 记录触发的规则内容
    let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.doubleBlockedTag_UseRegular) {
        // 使用 双重屏蔽标签数组 与 视频标签 进行匹配
        const doubleBlockedTagHitItem = blockedParameter.doubleBlockedTag_Array.find((doubleBlockedTag) => {
            // 以 "|" 分割成数组,同时都能匹配上才是符合
            const doubleBlockedTagSplitArray = doubleBlockedTag.split("|");
            const doubleBlockedTagRegEx0 = new RegExp(doubleBlockedTagSplitArray[0]);
            const doubleBlockedTagRegEx1 = new RegExp(doubleBlockedTagSplitArray[1]);

            const videoTagHitItem0 = videoInfoDict[videoBv].videoTags.find((videoTagItem) =>
                doubleBlockedTagRegEx0.test(videoTagItem)
            );
            const videoTagHitItem1 = videoInfoDict[videoBv].videoTags.find((videoTagItem) =>
                doubleBlockedTagRegEx1.test(videoTagItem)
            );

            if (videoTagHitItem0 && videoTagHitItem1) {
                blockedRulesItemText = `${videoTagHitItem0},${videoTagHitItem1}`;
                return true;
            }
        });

        if (doubleBlockedTagHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽双重标签", blockedRulesItemText);
        }
    } else {
        // 使用 双重屏蔽标签数组 与 视频标签 进行匹配
        const doubleBlockedTagHitItem = blockedParameter.doubleBlockedTag_Array.find((doubleBlockedTag) => {
            // 以 "|" 分割成数组,同时都能匹配上才是符合
            const doubleBlockedTagSplitArray = doubleBlockedTag.split("|");

            const videoTagHitItem0 = videoInfoDict[videoBv].videoTags.find(
                (videoTagItem) => doubleBlockedTagSplitArray[0] == videoTagItem
            );
            const videoTagHitItem1 = videoInfoDict[videoBv].videoTags.find(
                (videoTagItem) => doubleBlockedTagSplitArray[1] == videoTagItem
            );

            if (videoTagHitItem0 && videoTagHitItem1) {
                blockedRulesItemText = `${videoTagHitItem0},${videoTagHitItem1}`;
                return true;
            }
        });

        if (doubleBlockedTagHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽双重标签", blockedRulesItemText);
        }
    }
}

// API获取视频评论区
let apiRequestDelayTime = 0;
function getVideoApiComments(videoBv) {
    // 如果已经有BV号对应的记录,跳过
    if (videoInfoDict[videoBv].filteredComments === false || videoInfoDict[videoBv].filteredComments === true) {
        return;
    }

    // 当 lastVideoCommentsApiRequestTime 上次API获取视频评论区的时间存在,并且,和当前的时间差小于3秒时,跳过
    const currentTime = new Date(); //获取当前时间
    if (
        videoInfoDict[videoBv].lastVideoCommentsApiRequestTime &&
        currentTime - videoInfoDict[videoBv].lastVideoCommentsApiRequestTime < 3000
    ) {
        // consoleLogOutput(videoBv, "getVideoApiComments() 距离上次 Fetch 获取视频信息还未超过3秒钟");
        return;
    }
    // 获取评论区的API貌似对频繁请求的容忍度很低,只能错开来请求,apiRequestDelayTime 延迟。
    // 所以设置了每次调用 getVideoApiComments() 都会增加延迟,例如:每次加 50ms 再请求下一个请求。
    // lastVideoCommentsApiRequestTime(上次API获取视频评论区的时间) 本质是为了限制每个BV号3秒只能请求一次,
    // 但是加了延迟之后,到后面 apiRequestDelayTime 延迟本身就会超过3秒了。
    // 还是会出现多次请求的问题,可能影响不大,但是还是把延迟值加进了 lastVideoCommentsApiRequestTime 里面。
    // 这也相当于把 lastVideoCommentsApiRequestTime 修正为了正确请求时间。
    let apiRequestDelayTimeData = new Date(apiRequestDelayTime);
    videoInfoDict[videoBv].lastVideoCommentsApiRequestTime = new Date(
        currentTime.getTime() + apiRequestDelayTimeData.getTime()
    );

    // apiRequestDelayTime 的最大值限制问题
    // 如果不做限制的话,这个值可能会无限增大,导致最后加载的视频元素的请求也永远等不到生效时间。
    // 以 videoInfoDict 对象的长度来做最大值限制貌似会比较合理一点。但是这个对象也可能会无限增大从而导致后面的请求等太久。
    // 如果把 videoInfoDict[videoBv].filteredComments 筛选为 null 后的统计数值x延迟时间,做为最大延迟时间比较好?
    // lastVideoCommentsApiRequestTime 也保证了每个Bv号的对应请求3秒只出现一次,这样就不用担心重复请求的问题。
    // 但是本质上这一堆处理只是为了:防止频繁请求 https://api.bilibili.com/x/v2/reply 出现拒绝,同时为了效率的问题,每个Bv号只应该请求一次。

    // 统计 videoInfoDict 中,视频Bv下面的 filteredComments 不存在的数量。
    function filteredCommentsCount() {
        let nullCount = 0;
        for (const video in videoInfoDict) {
            if (videoInfoDict[video].hasOwnProperty("filteredComments") == false) {
                nullCount++;
            }
        }
        return nullCount;
    }

    // 最大的延迟时间上限
    let apiRequestDelayTimeMax = filteredCommentsCount() * 100;
    // consoleLogOutput("最大的延迟时间上限", apiRequestDelayTimeMax);

    // 每次调用增加的延迟 > 最大的延迟时间上限后 重置为0
    if (apiRequestDelayTime > apiRequestDelayTimeMax) {
        apiRequestDelayTime = 0;
    }

    setTimeout(() => {
        // 设置请求的 URL 和参数
        const url = "https://api.bilibili.com/x/v2/reply";
        const params = {
            type: 1, // 评论区类型代码
            oid: videoBv, // 目标评论区 id
            sort: 0, // 排序方式,默认为0,0:按时间,1:按点赞数,2:按回复数
            ps: 1, // 每页项数,默认为20,定义域:1-20
            pn: 1, // 页码,默认为1
            nohot: 0, // 是否不显示热评,默认为0,1:不显示,0:显示
        };
        // 将参数转换为 URL 搜索字符串
        const searchParams = new URLSearchParams(params).toString();

        // 获取视频评论区
        fetch(`${url}?${searchParams}`)
            .then((response) => response.json())
            .then((VideoApiCommentsJson) => {
                // API获取精选评论标记
                videoInfoDict[videoBv].filteredComments = VideoApiCommentsJson.data?.control?.web_selection;

                // API获取置顶评论内容
                videoInfoDict[videoBv].topComment = VideoApiCommentsJson.data.upper.top?.content?.message;

                FuckYouBilibiliRecommendationSystem();
            })
            .catch((error) => consoleLogOutput(videoBv, "getVideoApiComments() Fetch错误:", error));
    }, apiRequestDelayTime);

    // 每次调用增加的延迟
    // consoleLogOutput("本次调用增加延迟", apiRequestDelayTime);
    apiRequestDelayTime = apiRequestDelayTime + 100;
}

// 处理匹配 屏蔽精选评论的视频
function handleBlockedFilteredCommentsVideo(videoBv) {
    // 判断设置的屏蔽精选评论的视频是否有启用标记
    if (videoInfoDict[videoBv].filteredComments) {
        // 标记为屏蔽目标并记录触发的规则
        markAsBlockedTarget(videoBv, "屏蔽精选评论的视频", videoInfoDict[videoBv].videoUpName);
    }
}

// 处理匹配 屏蔽置顶评论内容
function handleBlockedTopComment(videoBv) {
    // 判断是否拿到视频置顶评论
    if (!videoInfoDict[videoBv].topComment) {
        return;
    }

    // 记录触发的规则内容
    // let blockedRulesItemText = "";

    // 是否启用正则
    if (blockedParameter.blockedTopComment_UseRegular) {
        // 使用 屏蔽置顶评论数组 与 置顶评论 进行匹配
        const blockedTopCommentHitItem = blockedParameter.blockedTopComment_Array.find((blockedTopComment) => {
            // 正则化屏蔽置顶评论
            const blockedTitleRegEx = new RegExp(blockedTopComment);
            // 判断 正则化的屏蔽置顶评论 是否匹配 置顶评论
            if (blockedTitleRegEx.test(videoInfoDict[videoBv].topComment)) {
                return true;
            }
        });

        if (blockedTopCommentHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽置顶评论", blockedTopCommentHitItem);
        }
    } else {
        // 使用 屏蔽置顶评论数组 与 置顶评论 进行匹配
        const blockedTopCommentHitItem = blockedParameter.blockedTopComment_Array.find((blockedTopComment) => {
            // 判断 屏蔽置顶评论 是否匹配 置顶评论
            if (blockedTopComment === videoInfoDict[videoBv].topComment) {
                return true;
            }
        });

        if (blockedTopCommentHitItem) {
            // 标记为屏蔽目标并记录触发的规则
            markAsBlockedTarget(videoBv, "屏蔽置顶评论", blockedTopCommentHitItem);
        }
    }
}

// 处理匹配的白名单Up主和Uid
function handleWhitelistNameOrUid(videoBv) {
    // 判断是否拿到Up主名称或Up主Uid
    if (!videoInfoDict[videoBv].videoUpUid) {
        return;
    }

    // 使用 白名单Up主和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配
    const videoNameOrUid = blockedParameter.whitelistNameOrUid_Array.find((whitelistNameOrUidItem) => {
        if (whitelistNameOrUidItem == videoInfoDict[videoBv].videoUpName) {
            return true;
        }

        if (whitelistNameOrUidItem == videoInfoDict[videoBv].videoUpUid) {
            return true;
        }
    });

    if (videoNameOrUid) {
        // 标记为白名单目标
        videoInfoDict[videoBv].whiteListTargets = true;
    }
}

// 隐藏非视频元素
function hideNonVideoElements() {
    // 判断当前页面URL是否以 https://www.bilibili.com/ 开头,即首页
    if (window.location.href.startsWith("https://www.bilibili.com/")) {
        // 隐藏首页的番剧、国创、直播等左上角有标的元素,以及左上角没标的直播
        const adElements_1 = document.querySelectorAll("div.floor-single-card, div.bili-live-card");
        adElements_1.forEach(function (element) {
            element.style.display = "none";
        });
    }

    // 判断当前页面URL是否以 https://search.bilibili.com/all 开头,即搜索页——综合
    if (window.location.href.startsWith("https://search.bilibili.com/all")) {
        // 隐藏 搜索页——综合 下的 直播卡片
        const adElements_2 = document.querySelectorAll("div.bili-video-card:has(div.bili-video-card__info--living)");
        adElements_2.forEach(function (element) {
            element.parentNode.style.display = "none";
            element.style.display = "none";
        });
    }

    // 隐藏首页广告,那些没有“enable-no-interest” CSS类的视频卡片元素
    const adElements_3 = document.querySelectorAll("div.bili-video-card.is-rcmd:not(.enable-no-interest)");
    adElements_3.forEach(function (element) {
        // 检查其父元素是否是 .feed-card
        if (element.closest("div.feed-card") !== null) {
            // 如果是,选择其父元素并应用样式
            element.closest("div.feed-card").style.display = "none";
        } else {
            // 如果不是,直接在视频元素上应用样式
            element.style.display = "none";
        }
    });

    // 隐藏视频播放页右侧广告、视频相关的游戏推荐、视频相关的特殊推荐、大家围观的直播
    const adElements_4 = document.querySelectorAll(
        "div#slide_ad, a.ad-report, div.video-page-game-card-small, div.video-page-special-card-small, div.pop-live-small-mode"
    );
    adElements_4.forEach(function (element) {
        element.style.display = "none";
    });
}

// 屏蔽或者取消屏蔽
function blockedOrUnblocked(videoElement, videoBv, setTimeoutStatu = false) {
    // 是白名单目标,是屏蔽目标,没有隐藏、没有叠加层:跳过
    if (
        videoInfoDict[videoBv].whiteListTargets &&
        videoInfoDict[videoBv].blockedTarget &&
        videoElement.style.display != "none" &&
        videoElement.firstElementChild.className != "blockedOverlay"
    ) {
        return;
    }

    // 是白名单目标,是屏蔽目标, 有隐藏或有叠加层:去除隐藏或叠加层
    if (
        videoInfoDict[videoBv].whiteListTargets &&
        videoInfoDict[videoBv].blockedTarget &&
        (videoElement.style.display == "none" || videoElement.firstElementChild.className == "blockedOverlay")
    ) {
        // 去除叠加层
        removeHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu);
        return;
    }

    // 不是白名单目标,是屏蔽目标, 有隐藏或有叠加层:跳过
    if (
        videoInfoDict[videoBv].whiteListTargets != true &&
        videoInfoDict[videoBv].blockedTarget &&
        (videoElement.style.display == "none" || videoElement.firstElementChild.className == "blockedOverlay")
    ) {
        return;
    }

    // 不是白名单目标,是屏蔽目标, 没有隐藏、没有叠加层:隐藏或添加叠加层
    if (
        videoInfoDict[videoBv].whiteListTargets != true &&
        videoInfoDict[videoBv].blockedTarget &&
        videoElement.style.display != "none" &&
        videoElement.firstElementChild.className != "blockedOverlay"
    ) {
        // 隐藏或添加叠加层
        addHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu);
        return;
    }

    // 隐藏或添加叠加层
    function addHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu) {
        // 是否为隐藏视频模式?
        if (blockedParameter.hideVideoMode_Switch == true) {
            // 隐藏视频

            // 判断当前页面URL是否以 https://search.bilibili.com/ 开头,即搜索页面,修改父元素
            if (window.location.href.startsWith("https://search.bilibili.com/")) {
                videoElement.parentNode.style.display = "none";
                // 为什么改了父元素,还要改元素本身?为了方便上面的判断。
                videoElement.style.display = "none";
            }
            // 如果是父元素是feed-card,修改父元素
            else if (videoElement.closest("div.feed-card") !== null) {
                videoElement.closest("div.feed-card").style.display = "none";
                videoElement.style.display = "none";
            } else {
                videoElement.style.display = "none";
            }
        } else {
            // 添加叠加层

            // 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(() => {
                    // 创建屏蔽叠加层
                    blockedOrUnblocked(videoElement, videoBv, 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 = "rgba(60, 60, 60, 0.85)";
            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";
            }
            // 使用 videoInfoDict[videoBv] 里面的存储的触发规则的第1条来做为提示文字
            overlayText.innerText = videoInfoDict[videoBv].triggeredBlockedRules[0];
            overlayText.style.color = "rgb(250,250,250)";
            overlay.appendChild(overlayText);

            // 添加叠加层为最前面的子元素
            videoElement.insertAdjacentElement("afterbegin", overlay);
        }
    }

    // 去除隐藏或叠加层
    function removeHiddenOrOverlay(videoElement) {
        // 是否为隐藏视频模式?
        if (blockedParameter.hideVideoMode_Switch == true) {
            // 取消隐藏

            // 判断当前页面URL是否以 https://search.bilibili.com/ 开头,即搜索页面
            if (window.location.href.startsWith("https://search.bilibili.com/")) {
                videoElement.parentNode.style.display = "";
                videoElement.style.display = "";
            }
            // 如果是父元素是feed-card
            else if (videoElement.closest("div.feed-card") !== null) {
                videoElement.closest("div.feed-card").style.display = "";
                videoElement.style.display = "";
            } else {
                videoElement.style.display = "";
            }
        } else {
            // 删除叠加层
            if (videoElement.firstElementChild.className == "blockedOverlay") {
                videoElement.removeChild(videoElement.firstElementChild);
            }
        }
    }
}

// 同步屏蔽叠加层与父元素的尺寸
function syncBlockedOverlayAndParentNodeRect() {
    // 获取所有的屏蔽叠加层
    const blockedOverlays = document.querySelectorAll("div.blockedOverlay");

    blockedOverlays.forEach(function (element) {
        // 获取父元素的尺寸
        const parentNodeElementRect = element.parentNode.getBoundingClientRect();
        // 修改屏蔽叠加层的大小
        element.style.width = parentNodeElementRect.width + "px"; // 使用 父元素的尺寸 的宽度
        element.style.height = parentNodeElementRect.height + "px"; // 使用 父元素的尺寸 的高度
    });
}

// -----------------主流程函数----------------------

// 屏蔽Bilibili上的符合屏蔽条件的视频
function FuckYouBilibiliRecommendationSystem() {
    // 是否启用 隐藏非视频元素
    if (blockedParameter.hideNonVideoElements_Switch) {
        // 隐藏非视频元素
        hideNonVideoElements();
    }

    // 判断是否和上次的输出的字典不一样
    if (objectDifferent(lastConsoleVideoInfoDict, videoInfoDict)) {
        // 输出整个视频信息字典
        consoleLogOutput(Object.keys(videoInfoDict).length, "个视频信息: ", videoInfoDict);

        // 将本次输出的视频信息字典保存起来作参考
        lastConsoleVideoInfoDict = Object.assign({}, videoInfoDict);
    }

    // 获取所有包含B站视频相关标签的视频元素
    const videoElements = getVideoElements();

    // 遍历每个视频元素
    for (let videoElement of videoElements) {
        // 判断是否为已经屏蔽处理过的子元素
        if (isAlreadyBlockedChildElement(videoElement)) {
            // 如果是已经屏蔽处理过的子元素,跳过后续操作
            continue;
        }

        // 网页获取视频元素的Bv号和标题
        let videoBv = getBvAndTitle(videoElement);

        // 如果没有拿到Bv号,跳过后续操作
        if (!videoBv) {
            continue;
        }

        // 是否启用 屏蔽标题
        if (blockedParameter.blockedTitle_Switch && blockedParameter.blockedTitle_Array.length > 0) {
            // 判断处理匹配的屏蔽标题
            handleBlockedTitle(videoBv);
        }

        // 网页获取视频Up名和UpUid
        getNameAndUid(videoElement, videoBv);

        // 通过API获取视频信息
        getVideoApiInfo(videoBv);

        // 是否启用 屏蔽Up主名称或Up主Uid
        if (blockedParameter.blockedNameOrUid_Switch && blockedParameter.blockedNameOrUid_Array.length > 0) {
            // 判断处理匹配的屏蔽Up主名称或Up主Uid
            handleBlockedNameOrUid(videoBv);
        }

        // 是否启用 屏蔽视频分区
        if (
            blockedParameter.blockedVideoPartitions_Switch &&
            blockedParameter.blockedVideoPartitions_Array.length > 0
        ) {
            // 判断处理匹配 屏蔽视频分区
            handleBlockedVideoPartitions(videoBv);
        }

        // 是否启用 屏蔽短时长视频
        if (blockedParameter.blockedShortDuration_Switch && blockedParameter.blockedShortDuration > 0) {
            // 判断处理匹配的短时长视频
            handleBlockedShortDuration(videoBv);
        }

        // 是否启用 屏蔽低播放量视频
        if (blockedParameter.blockedBelowVideoViews_Switch && blockedParameter.blockedBelowVideoViews > 0) {
            // 判断处理匹配的低播放量视频
            handleBlockedBelowVideoViews(videoBv);
        }

        // 是否启用 屏蔽低于指定点赞率的视频
        if (blockedParameter.blockedBelowLikesRate_Switch && blockedParameter.blockedBelowLikesRate > 0) {
            // 判断处理 屏蔽低于指定点赞率的视频
            handleBlockedBelowLikesRate(videoBv);
        }

        // 是否启用 屏蔽竖屏视频
        if (blockedParameter.blockedPortraitVideo_Switch) {
            // 判断处理 屏蔽竖屏视频
            handleBlockedPortraitVideo(videoBv);
        }

        // 是否启用 屏蔽充电专属视频
        if (blockedParameter.blockedChargingExclusive_Switch) {
            // 判断处理 蔽充电专属视频
            handleBlockedChargingExclusive(videoBv);
        }

        // 通过API获取视频标签
        getVideoApiTags(videoBv);

        // 是否启用 屏蔽标签
        if (blockedParameter.blockedTag_Switch && blockedParameter.blockedTag_Array.length > 0) {
            // 判断处理 屏蔽标签
            handleBlockedTag(videoBv);
        }

        // 是否启用 屏蔽双重屏蔽标签
        if (blockedParameter.doubleBlockedTag_Switch && blockedParameter.doubleBlockedTag_Array.length > 0) {
            // 判断处理 屏蔽双重屏蔽标签
            handleDoubleBlockedTag(videoBv);
        }

        // API获取视频评论区
        getVideoApiComments(videoBv);

        // 是否启用 屏蔽精选评论的视频
        if (blockedParameter.blockedFilteredCommentsVideo_Switch) {
            // 判断处理 屏蔽精选评论的视频
            handleBlockedFilteredCommentsVideo(videoBv);
        }

        // 是否启用 屏蔽置顶评论
        if (blockedParameter.blockedTopComment_Switch && blockedParameter.blockedTopComment_Array.length > 0) {
            // 判断处理 屏蔽精选评论的视频
            handleBlockedTopComment(videoBv);
        }

        // 是否启用 白名单Up主和Uid
        if (blockedParameter.whitelistNameOrUid_Switch && blockedParameter.whitelistNameOrUid_Array.length > 0) {
            // 判断处理 白名单Up主和Uid
            handleWhitelistNameOrUid(videoBv);
        }

        // 屏蔽或者取消屏蔽
        blockedOrUnblocked(videoElement, videoBv);

        // 同步屏蔽叠加层与父元素的尺寸
        syncBlockedOverlayAndParentNodeRect();
    }
}

// 页面加载完成后运行脚本
window.addEventListener("load", FuckYouBilibiliRecommendationSystem);

// 窗口尺寸变化时运行脚本
window.addEventListener("resize", FuckYouBilibiliRecommendationSystem);

// 定义 MutationObserver 的回调函数
function mutationCallback() {
    // 在这里运行你的脚本
    FuckYouBilibiliRecommendationSystem();
}
// 创建一个 MutationObserver 实例,观察 body 元素的子节点变化
let observer = new MutationObserver(mutationCallback);
let targetNode = document.body;
// 配置观察器的选项
let config = { childList: true, subtree: true };
// 启动观察器并传入回调函数和配置选项
observer.observe(targetNode, config);