Bilibili-BlackList

Bilibili UP屏蔽插件 - 屏蔽UP主视频卡片,支持精确匹配和正则匹配,支持视频页面、分类页面、搜索页面等。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Bilibili-BlackList
// @namespace    https://github.com/HeavenTTT/bilibili-blacklist
// @version      1.2.4
// @author       HeavenTTT
// @description  Bilibili UP屏蔽插件 - 屏蔽UP主视频卡片,支持精确匹配和正则匹配,支持视频页面、分类页面、搜索页面等。
// @match        *://*.bilibili.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @icon         https://www.bilibili.com/favicon.ico
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  /*
   * Bilibili-BlackList -- Bilibili UP屏蔽插件
   * 脚本大部分代码由AI生成,作者一点都不懂JavaScript,出现bug请联系Gemini / ChatGPT / DeepSeek
   * this script is mainly generated by AI, the author doesn't know JavaScript at all, if there are bugs, please contact Gemini / ChatGPT / DeepSeek
   * 感谢你的使用
   * Thank you for using this script
   *
   * 本段注释为VS code 自动生成 this is a comment generated by VS code
   */


  // 从存储中获取黑名单
  // 默认精确匹配黑名单(区分大小写)
  let exactMatchBlacklist = GM_getValue("exactBlacklist", [
    "绝区零",
    "崩坏星穹铁道",
    "崩坏3",
    "原神",
    "米哈游miHoYo",
  ]);
  // 默认正则匹配黑名单(不区分大小写)
  let regexMatchBlacklist = GM_getValue("regexBlacklist", [
    "王者荣耀",
    "和平精英",
    "PUBG",
    "绝地求生",
    "吃鸡",
  ]);
  // 默认标签名黑名单
  let tagNameBlacklist = GM_getValue("tNameBlacklist", []);

  // 从存储中获取全局配置,并为旧版本配置补充新增字段
  const defaultGlobalPluginConfig = {
    flagInfo: true, // 启用/禁用按UP主名/标题屏蔽
    flagAD: true, // 启用/禁用屏蔽一般广告
    flagTName: true, // 启用/禁用按标签名屏蔽(需要API调用)
    flagCM: true, // 启用/禁用屏蔽cm.bilibili.com软广
    flagKirby: true, // 启用/禁用被屏蔽视频的卡比覆盖模式
    flagHoverReveal: false, // 启用/禁用悬停后临时显示被遮挡视频
    hoverRevealDelaySeconds: 1, // 悬停显示延迟(秒)
    processQueueInterval: 200, // 处理队列中单个卡片的延迟时间(毫秒)
    blockScanInterval: 200, // BlockCard扫描新卡片的间隔时间(毫秒)
    flagHideOnLoad: true, // 启用/禁用页面加载时自动隐藏
    flagVertical: true, // 启用/禁用屏蔽竖屏视频
    verticalScaleThreshold: 0.7, // 竖屏视频的宽高比阈值(0-1)
    // 自动连播遇到被屏蔽视频时的处理方式(三态):
    //  "skip" = 切换到未屏蔽视频;"stop" = 停止播放;"off" = 不处理(B站默认行为,继续播放被屏蔽视频)
    flagSkipBlockedAutoplay: "off",
  };
  let globalPluginConfig = {
    ...defaultGlobalPluginConfig,
    ...(GM_getValue("globalConfig", {}) || {}),
  };

  // 防止旧配置或手动修改写入超出允许范围的悬停延迟
  const storedHoverRevealDelay = Number(
    globalPluginConfig.hoverRevealDelaySeconds
  );
  globalPluginConfig.hoverRevealDelaySeconds = Number.isFinite(
    storedHoverRevealDelay
  )
    ? Math.min(5, Math.max(0.1, storedHoverRevealDelay))
    : defaultGlobalPluginConfig.hoverRevealDelaySeconds;

  // 校验/修复自动连播处理方式,只允许 "skip" / "stop" / "off"
  const AUTOPLAY_SKIP_MODES = ["skip", "stop", "off"];
  if (!AUTOPLAY_SKIP_MODES.includes(globalPluginConfig.flagSkipBlockedAutoplay)) {
    globalPluginConfig.flagSkipBlockedAutoplay =
      defaultGlobalPluginConfig.flagSkipBlockedAutoplay;
  }

  // 将黑名单保存到存储中
  function saveBlacklistsToStorage() {
    GM_setValue("exactBlacklist", exactMatchBlacklist);
    GM_setValue("regexBlacklist", regexMatchBlacklist);
    GM_setValue("tNameBlacklist", tagNameBlacklist);
  }

  // 将全局配置保存到存储中
  function saveGlobalConfigToStorage() {
    GM_setValue("globalConfig", globalPluginConfig);
  }

  // 标签名列表:存储ID到名称的映射
  let tagNameList = GM_getValue("tagNameList", []); // 默认为空数组,每个条目为 { id, name , name_v2}
  let tagListLastTime = GM_getValue("tLastTime", 0);
  // 将标签名列表保存到存储中
  function saveTagNameListToStorage() {
    GM_setValue("tagNameList", tagNameList);
    GM_setValue("tLastTime", Date.now());
  }

  // 根据ID查找标签名
  function getTagNameById(id) {
    if (id === null || id === undefined) return null;
    // 支持字符串或数字ID
    const entry = tagNameList.find(entry => entry.id == id); // 使用宽松相等以匹配类型
    return entry ? { name: entry.name, name_v2: entry.name_v2 } : null;
  }
  // 根据name_v2查找标签名
  function getTagNameByV2(name_v2) {
    if (name_v2 === null || name_v2 === undefined) return null;
    // 支持字符串或数字ID
    const entry = tagNameList.find(entry => entry.name_v2 == name_v2); // 使用宽松相等以匹配类型
    return entry ? entry.name: null;
  }

  // UI元素(稍后初始化)
  let tempUnblockButton;
  let managerPanel;
  let exactMatchListElement;
  let regexMatchListElement;
  let tagNameListElement;
  let configListElement;
  let blockCountTitleElement;
  let blockCountDisplayElement = null;

  // 内部状态变量
  let isShowAllVideos = false; // 是否显示全部视频卡片
  let isBlockingOperationInProgress = false; // 是否正在执行BlockCard扫描操作
  let lastBlockScanExecutionTime = 0; // 上次执行BlockCard扫描的时间戳
  let blockedVideoCards = new Set(); // 存储已屏蔽的视频卡片元素
  let processedVideoCards = new WeakSet(); // 记录已处理过的卡片(避免重复处理,包括 UP主/标题检查和 tname 获取)
  let videoCardProcessQueue = new Set(); // 存储待处理的卡片,用于统一的队列处理
  let isVideoCardQueueProcessing = false; // 是否正在处理队列
  let isPageCurrentlyActive = true; // 页面是否可见
  let countBlockInfo = 0; // 已屏蔽视频计数
  let countBlockAD = 0; // 已屏蔽广告计数
  let countBlockTName = 0; // 已屏蔽标签名计数
  let countBlockCM = 0; // 已屏蔽cm.bilibili.com软广计数

  // 用于不同页面UP主名称选择器
  const UP_NAME_SELECTORS = [
    ".bili-video-card__info--author", // 主页
    ".bili-video-card__author", // 分类页面 -> span title
    ".name", // 视频播放页
    ".upname a span", // 视频播放页“接下来播放/相关推荐”卡片(新版结构)
    ".upname a",
    ".upname",
  ];

  // 用于不同页面视频标题选择器
  const VIDEO_TITLE_SELECTORS = [
    ".bili-video-card__info--tit", // 主页
    ".bili-video-card__title", // 分类页面 -> span title
    ".title", // 视频播放页
  ];

  // 屏蔽类型对应的原因文案
  const BLOCK_REASON_MAP = {
    info: "标题/UP主名",
    ad: "广告",
    tname: "分类标签",
    cm: "软广",
    vertical: "竖屏视频",
  };

  /**
   * 获取视频卡片上容器应挂载的宿主元素,并确保宿主可被绝对定位。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLElement} 容器宿主元素。
   */
  function getBlockContainerHost(cardElement) {
    // 视频播放页面的视频卡片结构特殊,需要调整位置
    if (isCurrentPageVideo()) {
      const cardBox = cardElement.querySelector(".card-box");
      if (cardBox) {
        cardBox.style.position = "relative";
        cardBox.classList.add("bilibili-blacklist-block-container-host");
        return cardBox;
      }
    } else if (isCurrentPageCategory()) {
      // 分类页面的视频卡片结构特殊,需要调整位置
      const biliVideoCard = cardElement.querySelector(".bili-video-card");
      if (biliVideoCard) {
        biliVideoCard.classList.add("bilibili-blacklist-block-container-host");
        return biliVideoCard;
      }
    }
    // 默认宿主:确保可被绝对定位的子元素正常显示
    const hostStyle = getComputedStyle(cardElement);
    if (hostStyle.position === "static" || !hostStyle.position) {
      cardElement.style.position = "relative";
    }
    cardElement.classList.add("bilibili-blacklist-block-container-host");
    return cardElement;
  }

  /**
   * 确保视频卡片上存在屏蔽容器,不存在则创建(用于广告等未走扫描流程的卡片)。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLElement} 已存在的或新创建的容器元素。
   */
  function ensureBlockContainerOnCard(cardElement) {
    const existing = cardElement.querySelector(
      ".bilibili-blacklist-block-container"
    );
    if (existing) return existing;
    const container = document.createElement("div");
    container.classList.add("bilibili-blacklist-block-container");
    const host = getBlockContainerHost(cardElement);
    host.appendChild(container);
    return container;
  }

  /**
   * 为视频卡片添加屏蔽按钮容器。
   * @param {string} upName - UP主名称。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLElement} 创建的容器元素。
   */
  function addBlockContainerToCard(upName, cardElement) {
    const container = ensureBlockContainerOnCard(cardElement);
    if (!container.querySelector(".bilibili-blacklist-block-btn")) {
      const blockButton = createBlockUpButton(upName, cardElement);
      container.appendChild(blockButton);
    }
    return container;
  }

  /**
   * 隐藏给定的视频卡片。
   * @param {HTMLElement} cardElement - 要隐藏的视频卡片元素。
   * @param {string} type - 隐藏类型,默认为"none"。
   * @returns {void}
   *
   */
  function hideVideoCard(cardElement, type = "none") {
    const realCardToBlock = getRealVideoCardElement(cardElement);
    if (!realCardToBlock) {
      console.warn(
        "[bililili-blacklist] hideVideoCard: realCardToBlock is null"
      );
      return;
    }
    if (blockedVideoCards.has(realCardToBlock)) {
      return;
    }
    blockedVideoCards.add(realCardToBlock);
    if (type === "info") {
      countBlockInfo++;
    }
    if (type === "ad") {
      countBlockAD++;
    }
    if (type === "tname") {
      countBlockTName++;
    }
    if (type === "cm") {
      countBlockCM++;
    }
    if (type === "vertical") {
      countBlockTName++;
    }

    if (globalPluginConfig.flagKirby) {
      addKirbyOverlayToCard(cardElement);
    } else {
      realCardToBlock.style.display = "none";
    }

    setBlockReasonOnCard(cardElement, type);
  }

  /**
   * 在卡片的屏蔽按钮容器中设置屏蔽原因。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @param {string} type - 屏蔽类型。
   */
  function setBlockReasonOnCard(cardElement, type) {
    const reasonText = BLOCK_REASON_MAP[type];
    if (!reasonText) return;
    // 广告等卡片未经过 scanAndBlockVideoCards 流程,可能不存在容器,需确保创建
    const container = ensureBlockContainerOnCard(cardElement);
    let reasonElement = container.querySelector(
      ".bilibili-blacklist-block-reason"
    );
    if (!reasonElement) {
      reasonElement = document.createElement("span");
      reasonElement.className = "bilibili-blacklist-block-reason";
      // 位于"屏蔽"按钮之后、标签组之前
      const tnameGroup = container.querySelector(
        ".bilibili-blacklist-tname-group"
      );
      if (tnameGroup) {
        container.insertBefore(reasonElement, tnameGroup);
      } else {
        container.appendChild(reasonElement);
      }
    }
    reasonElement.textContent = `屏蔽原因: ${reasonText}`;
  }

  /**
   * 移除卡片上的屏蔽原因显示。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   */
  function removeBlockReason(cardElement) {
    const container = cardElement.querySelector(
      ".bilibili-blacklist-block-container"
    );
    if (!container) return;
    const reasonElement = container.querySelector(
      ".bilibili-blacklist-block-reason"
    );
    if (reasonElement) {
      reasonElement.remove();
    }
  }

  /**
   * 获取应该被屏蔽的卡片的真正父元素。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLElement} 应用显示更改的实际元素。
   */
  function getRealVideoCardElement(cardElement) {
    // 搜索页面的视频卡片父元素是上一级
    if (isCurrentPageSearch()) {
      return cardElement.parentElement;
    }
    // 主页视频卡片可能有多层父元素
    if (isCurrentPageMain()) {
      if (cardElement.parentElement.classList.contains("bili-feed-card")) {
        cardElement = cardElement.parentElement;
        if (cardElement.parentElement.classList.contains("feed-card")) {
          cardElement = cardElement.parentElement;
        }
      }
    }
    return cardElement;
  }

  /**
   * 根据当前页面选择所有视频卡片。
   * @returns {NodeListOf<HTMLElement> | null} 视频卡片元素的NodeList,如果不是识别的页面则返回null。
   */
  function queryAllVideoCards() {
    if (isCurrentPageMain()) {
      return document.querySelectorAll(".bili-video-card");
    } else if (isCurrentPageVideo()) {
      return document.querySelectorAll(".video-page-card-small");
    } else if (isCurrentPageCategory()) {
      return document.querySelectorAll(".feed-card");
    } else if (isCurrentPageSearch()) {
      return document.querySelectorAll(".bili-video-card");
    }
    return null;
  }

  /**
   * 扫描并处理视频卡片进行屏蔽。
   */
  function scanAndBlockVideoCards() {
    const now = Date.now();
    // 限制扫描频率,防止性能问题
    if (
      isBlockingOperationInProgress ||
      now - lastBlockScanExecutionTime < globalPluginConfig.blockScanInterval
    ) {
      return;
    }

    isBlockingOperationInProgress = true;
    lastBlockScanExecutionTime = now;

    try {
      const videoCards = queryAllVideoCards();
      if (!videoCards) return;

      videoCards.forEach((card) => {
        // 如果卡片已经处理过,则跳过
        if (processedVideoCards.has(card)) {
          return;
        }
        const { upName, videoTitle } = getVideoCardInfo(card);
        // 如果获取到UP主名称和视频标题,则添加屏蔽按钮
        if (upName && videoTitle) {
          addBlockContainerToCard(upName, card);

          // --- 根据 flagHideOnLoad 开关决定是否立即隐藏卡片 ---
          const realCard = getRealVideoCardElement(card);
          if (globalPluginConfig.flagHideOnLoad && !isShowAllVideos) {
            // 只有在"显示全部"模式关闭时才执行
            if (globalPluginConfig.flagKirby) {
              addKirbyOverlayToCard(card); // 卡比模式下添加遮罩
              realCard.style.display = "block"; // 确保卡片本身是显示的
            } else {
              realCard.style.display = "none"; // 非卡比模式下直接隐藏
            }
          }
        }
        // --- 立即隐藏卡片的逻辑结束 ---

        // 将卡片添加到处理队列
        videoCardProcessQueue.add(card);
      });

      // 如果队列中有待处理的卡片且当前未在处理中,则开始处理队列
      if (videoCardProcessQueue.size > 0 && !isVideoCardQueueProcessing) {
        processVideoCardQueue();
      }

      // 刷新屏蔽计数显示
      refreshBlockCountDisplay();
      // 修正主页布局
      fixMainPageLayout();
    } finally {
      isBlockingOperationInProgress = false;
    }
  }

  /**
   * 修正主页在屏蔽后的布局。
   */
  function fixMainPageLayout() {
    if (!isCurrentPageMain()) return;
    const container = document.querySelector(
      ".recommended-container_floor-aside .container"
    );

    if (container) {
      const children = container.children;
      let visibleIndex = 0;
      // 调整可见卡片的边距,使布局更紧凑
      for (let i = 0; i < children.length; i++) {
        const child = children[i];
        if (child.style.display !== "none") {
          if (visibleIndex <= 6) {
            child.style.marginTop = "0px";
          } else if (visibleIndex < 12) {
            child.style.marginTop = "24px";
          } else {
            break;
          }
          visibleIndex++;
        }
      }
    }
  }

  /**
   * 切换所有被屏蔽视频卡片的显示。
   */
  function toggleShowAllBlockedVideos() {
    isShowAllVideos = !isShowAllVideos;
    blockedVideoCards.forEach((card) => {
      if (globalPluginConfig.flagKirby) {
        const kirbyOverlay = card.querySelector("#bilibili-blacklist-kirby");
        if (kirbyOverlay) {
          if (isShowAllVideos) {
            kirbyOverlay.style.display = "none";
          } else {
            fadeInKirbyOverlay(kirbyOverlay);
          }
        }
        card.style.display = "block";
      } else {
        card.style.display = isShowAllVideos ? "block" : "none";
      }
    });
    tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽";
    tempUnblockButton.style.background = isShowAllVideos
      ? "#dddddd"
      : "#fb7299";
  }

  /**
   * 从视频卡片中检索UP主名称和视频标题。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {{upName: string, videoTitle: string}} 包含UP主名称和视频标题的对象。
   */
  function getVideoCardInfo(cardElement) {
    let upName = "";
    let videoTitle = "";

    const upNameElements = cardElement.querySelectorAll(
      UP_NAME_SELECTORS.join(", ")
    );
    if (upNameElements.length > 0) {
      upName = upNameElements[0].textContent.trim();
      if (isCurrentPageCategory()) {
        // 分类页面的UP主名称可能包含其他信息,需要进一步处理
        upName = upName.split(" · ")[0].trim();
      }
    }

    const titleElements = cardElement.querySelectorAll(
      VIDEO_TITLE_SELECTORS.join(", ")
    );
    if (titleElements.length > 0) {
      videoTitle = titleElements[0].textContent.trim();
    }
    return { upName, videoTitle };
  }

  /**
   * 检查UP主名称或标题是否在黑名单中。
   * @param {string} upName - 要检查的UP主名称。
   * @param {string} title - 要检查的视频标题。
   * @returns {boolean} 如果在黑名单中则返回true,否则返回false。
   */
  function isBlacklisted(upName, title) {
    const lowerCaseUpName = upName.toLowerCase();
    // 检查精确匹配黑名单
    if (
      exactMatchBlacklist.some((item) => item.toLowerCase() === lowerCaseUpName)
    ) {
      return true;
    }

    // 检查正则匹配黑名单
    const regexList = regexMatchBlacklist.map(
      (regex) => new RegExp(regex, "i")
    );
    if (regexList.some((regex) => regex.test(upName))) {
      return true;
    }
    if (regexList.some((regex) => regex.test(title))) {
      return true;
    }
    return false;
  }

  /**
   * 将UP主名称添加到精确匹配黑名单并刷新。
   * @param {string} upName - 要添加的UP主名称。
   * @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。
   */
  function addToExactBlacklist(upName, cardElement = null) {
    try {
      if (!upName) return;
      if (!exactMatchBlacklist.includes(upName)) {
        exactMatchBlacklist.push(upName);
        saveBlacklistsToStorage();
        refreshAllPanelTabs();
        if (cardElement) {
          hideVideoCard(cardElement, "info");
        }
        hideAllCardsByUpName(upName);
      }
    } catch (e) {
      console.error("[bilibili-blacklist] 添加黑名单出错:", e);
    }
  }

  /**
   * 从精确匹配黑名单中移除UP主名称。
   * @param {string} upName - 要移除的UP主名称。
   */
  function removeFromExactBlacklist(upName) {
    try {
      if (exactMatchBlacklist.includes(upName)) {
        const index = exactMatchBlacklist.indexOf(upName);
        exactMatchBlacklist.splice(index, 1);
        saveBlacklistsToStorage();
        refreshExactMatchList();
      }
    } catch (e) {
      console.error("[bilibili-blacklist] 移除黑名单出错:", e);
    }
  }

  /**
   * 将标签名添加到黑名单并刷新。
   * @param {string} tagName - 要添加的标签名。
   * @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。
   */
  function addToTagNameBlacklist(tagName, cardElement = null) {
    try {
      if (!tagName) {
        return;
      }
      if (!tagNameBlacklist.includes(tagName)) {
        tagNameBlacklist.push(tagName);
        saveBlacklistsToStorage();
        refreshAllPanelTabs();
        if (cardElement) {
          hideVideoCard(cardElement, "tname");
        }
        hideAllCardsByTagName(tagName);
      }
    } catch (e) {
      console.error("[bilibili-blacklist] 添加标签黑名单出错:", e);
    }
  }

  /**
   * 从黑名单中移除标签名。
   * @param {string} tagName - 要移除的标签名。
   */
  function removeFromTagNameBlacklist(tagName) {
    try {
      if (tagNameBlacklist.includes(tagName)) {
        const index = tagNameBlacklist.indexOf(tagName);
        tagNameBlacklist.splice(index, 1);
        saveBlacklistsToStorage();
        refreshTagNameList();
      }
    } catch (e) {
      console.error("[bilibili-blacklist] 移除标签黑名单出错:", e);
    }
  }

  /**
   * 隐藏所有匹配指定UP主名称的视频卡片。
   * @param {string} upName - 要匹配的UP主名称。
   */
  function hideAllCardsByUpName(upName) {
    const videoCards = queryAllVideoCards();
    if (!videoCards) return;
    videoCards.forEach(card => {
      const { upName: cardUpName, videoTitle } = getVideoCardInfo(card);
      if (cardUpName && isBlacklisted(cardUpName, videoTitle)) {
        hideVideoCard(card, "info");
      }
    });
  }

  /**
   * 隐藏所有匹配指定标签名的视频卡片。
   * @param {string} tagName - 要匹配的标签名。
   */
  function hideAllCardsByTagName(tagName) {
    const videoCards = queryAllVideoCards();
    if (!videoCards) return;
    videoCards.forEach(card => {
      if (isCardBlacklistedByTagName(card)) {
        hideVideoCard(card, "tname");
      }
    });
  }

  /**
   * 获取视频卡片的链接。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {string|null} 视频链接,如果未找到则返回null。
   */
  function getCardHrefLink(cardElement) {
    const hrefLink = cardElement.querySelector("a");
    if (hrefLink) {
      return hrefLink.getAttribute("href");
    }
    return null;
  }

  function checkLinkCM(link) {
    if (!link) return false;
    // 如果是cm.bilibili.com的链接,且启用了CM广告屏蔽,则隐藏卡片
    if (link.match(/cm.bilibili.com/) && globalPluginConfig.flagCM) {
      return true;
    }
    return false;
  }
  /**
   * 从视频链接中提取BV ID。
   * @param {string} link - 视频链接。
   * @returns {string|null} BV ID,如果未找到则返回null。
   */
  function getLinkBvId(link) {
    try {
      if (!link) {
        return null;
      } else {
        const bv = link.match(/BV\w+/);
        return bv ? bv[0] : null;
      }
    } catch (e) {
      return null;
    }
  }

  /**
   * 使用BV ID从Bilibili API获取视频信息。
   * @param {string} bvid - 视频的BV ID。
   * @returns {Promise<object|null>} 解析为视频数据或null的Promise。
   */
  async function getBilibiliVideoApiData(bvid) {
    if (!bvid || bvid.length >= 24) {
      return null;
    }
    const url = `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`;
    try {
      const response = await fetch(url);
      const json = await response.json();
      if (json.code === 0) {
        return json.data;
      } else {
        return null;
      }
    } catch (error) {
      console.error("[bilibili-blacklist] API 请求失败:", error);
    }
  }
  /**
   * 检查卡片是否包含任何黑名单标签。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {boolean} 如果有任何标签被列入黑名单,则返回true,否则返回false。
   */
  function isCardBlacklistedByTagName(cardElement) {
    const tnameGroup = cardElement.querySelector(
      ".bilibili-blacklist-tname-group"
    );
    if (tnameGroup) {
      const tnameElements = tnameGroup.querySelectorAll(
        ".bilibili-blacklist-tname"
      );
      for (const tnameElement of tnameElements) {
        const tname = tnameElement.textContent.trim();
        if (tagNameBlacklist.includes(tname)) {
          return true;
        }
        // 临时更新,根据V2查找名称
        const name = getTagNameByV2(tname);
        if (name === null) continue;
        if (tagNameBlacklist.includes(name)) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * 处理视频卡片队列进行屏蔽。
   */
  async function processVideoCardQueue() {
    if (isVideoCardQueueProcessing) return;
    isVideoCardQueueProcessing = true;

    while (videoCardProcessQueue.size > 0) {
      // 如果页面不可见,则暂停处理
      if (!isPageCurrentlyActive) {
        await sleep(1000);
        continue;
      }

      const iterator = videoCardProcessQueue.values();
      const card = iterator.next().value;
      videoCardProcessQueue.delete(card);

      if (!card || processedVideoCards.has(card)) {
        continue;
      }

      let shouldHide = false;
      let blockType = "none";
      // 如果启用了标签屏蔽且当前卡片未被隐藏
      const link = getCardHrefLink(card);
      if (checkLinkCM(link)) {
        shouldHide = true;
        blockType = "cm";
      }
      const { upName, videoTitle } = getVideoCardInfo(card);
      if (upName && videoTitle && !shouldHide) {
        // 如果UP主名称或标题在黑名单中,且启用了信息屏蔽
        if (isBlacklisted(upName, videoTitle) && globalPluginConfig.flagInfo) {
          shouldHide = true;
          blockType = "info";
        }
      } else {
        // 如果无法获取UP主名称和标题,但卡片已被隐藏或有Kirby覆盖,则也认为应该隐藏
        if (
          getRealVideoCardElement(card).style.display === "none" &&
          !globalPluginConfig.flagKirby
        ) {
          shouldHide = true;
        } else if (
          getRealVideoCardElement(card).querySelector(
            "#bilibili-blacklist-kirby"
          )
        ) {
          shouldHide = true;
        }
      }

      if (
        (globalPluginConfig.flagTName || globalPluginConfig.flagVertical) &&
        !shouldHide
      ) {
        const bvId = getLinkBvId(link);
        // 如果存在BV ID且卡片尚未添加标签组
        if (bvId && !card.querySelector(".bilibili-blacklist-tname-group")) {
          const data = await getBilibiliVideoApiData(bvId);
          if (data) {
            const container = card.querySelector(
              ".bilibili-blacklist-block-container"
            );
            if (container) {
              const tnameGroup = document.createElement("div");
              tnameGroup.className = "bilibili-blacklist-tname-group";
              let hasTname = false;
              
              if (data.tname) {
                const btn = createTNameBlockButton(data.tname, card);
                tnameGroup.appendChild(btn);
                hasTname = true;
              }
              if (data.tname_v2) {
                const tnameElement = createTNameBlockButton(
                  data.tname_v2,
                  card
                );
                tnameGroup.appendChild(tnameElement);
                hasTname = true;
              }
              //#region 临时修复,仅ID
              if (data.tid_v2) {
                const obj = getTagNameById(data.tid_v2);
                if (obj) {
                  const tnameElement = createTNameBlockButton(
                    obj.name,
                    card
                  );
                  tnameGroup.appendChild(tnameElement);
                  const tnameElement_v2 = createTNameBlockButton(
                    obj.name_v2,
                    card
                  );
                  tnameGroup.appendChild(tnameElement_v2);
                  hasTname = true;
                }
              }
              //#endregion
              if (hasTname) {
                container.appendChild(tnameGroup);
              }
            }

            if (isCardBlacklistedByTagName(card)) {
              shouldHide = true;
              blockType = "tname";
            }
            // 如果启用了垂直视频屏蔽
            if (
              data.dimension.width &&
              data.dimension.height &&
              !shouldHide &&
              globalPluginConfig.flagVertical
            ) {
              const dimension = data.dimension.width / data.dimension.height;
              if (dimension < globalPluginConfig.verticalScaleThreshold) {
                shouldHide = true;
                blockType = "vertical";
              }
            }
          }
        }
      }

      if (shouldHide) {
        hideVideoCard(card, blockType);
      } else {
        const realCardToDisplay = getRealVideoCardElement(card);
        if (blockedVideoCards.has(realCardToDisplay)) {
          blockedVideoCards.delete(realCardToDisplay);
        }
        removeBlockReason(card);
        if (globalPluginConfig.flagKirby) {
          removeKirbyOverlay(card);
        }
        realCardToDisplay.style.display = "block";
      }

      processedVideoCards.add(card); // 标记卡片已处理

      await sleep(globalPluginConfig.processQueueInterval);
    }
    isVideoCardQueueProcessing = false;
    refreshBlockCountDisplay();
  }

  // 异步等待函数
  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  const KIRBY_FADE_DURATION_MS = 800;
  const hoverRevealBoundCards = new WeakSet();
  const hoverRevealTimers = new WeakMap();
  const kirbyFadeTimers = new WeakMap();

  /**
   * 为UP主创建屏蔽按钮,显示在视频卡片上。
   * @param {string} upName - UP主名称。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLDivElement} 创建的按钮元素。
   */
  function createBlockUpButton(upName, cardElement) {
    const button = document.createElement("div");
    button.className = "bilibili-blacklist-block-btn";
    button.innerHTML = "屏蔽";
    button.title = `屏蔽: ${upName}`;

    button.addEventListener("click", (e) => {
      e.stopPropagation(); // 阻止事件冒泡,防止触发视频点击事件
      addToExactBlacklist(upName, cardElement);
    });

    return button;
  }

  /**
   * 为标签名创建屏蔽按钮,显示在视频卡片上。
   * @param {string} tagName - 标签名。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   * @returns {HTMLSpanElement} 创建的按钮元素。
   */
  function createTNameBlockButton(tagName, cardElement) {
    const button = document.createElement("span");
    button.className = "bilibili-blacklist-tname";
    button.innerHTML = `${tagName}`;
    button.title = `屏蔽: ${tagName}`;

    button.addEventListener("click", (e) => {
      e.stopPropagation(); // 阻止事件冒泡
      addToTagNameBlacklist(tagName, cardElement);
    });

    return button;
  }

  /**
   * 将黑名单管理器按钮添加到右侧导航条。
   */
  function addBlacklistManagerButton() {
    const rightEntry = document.querySelector(".right-entry");
    if (!rightEntry) {
      console.warn("[bilibili-blacklist] 未找到右侧导航栏");
      return;
    }
    // 顶栏由Vue延迟渲染,等li数量超过6个(顶栏基本渲染完成)后再插入按钮,避免被重渲染顶掉
    if (rightEntry.querySelectorAll("li").length <= 6) {
      return;
    }
    if (!rightEntry.querySelector("#bilibili-blacklist-manager-button")) {
      const listItem = document.createElement("li");
      listItem.id = "bilibili-blacklist-manager-button";
      listItem.className = "v-popover-wrap";

      const button = document.createElement("div");
      button.className = "right-entry-item";

      const icon = document.createElement("div");
      icon.className = "right-entry__outside";
      icon.innerHTML = getKirbySVG(); // 获取卡比SVG图标

      blockCountDisplayElement = document.createElement("span");
      blockCountDisplayElement.textContent = `0`;

      button.appendChild(icon);
      button.appendChild(blockCountDisplayElement);
      listItem.appendChild(button);

      // 将按钮插入到导航栏的特定位置
      if (rightEntry.children.length > 1) {
        rightEntry.insertBefore(listItem, rightEntry.children[1]);
      } else {
        rightEntry.appendChild(listItem);
      }

      // 点击按钮显示/隐藏管理面板
      listItem.addEventListener("click", () => {
        managerPanel.style.display =
          managerPanel.style.display === "flex" ? "none" : "flex";
      });
    }
  }

  /**
   * 更新已屏蔽视频的显示计数。
   */
  function refreshBlockCountDisplay() {
    if (blockCountDisplayElement) {
      blockCountDisplayElement.textContent = `${blockedVideoCards.size}`;
    }
    if (blockCountTitleElement) {
      blockCountTitleElement.textContent = `已屏蔽视频 (${blockedVideoCards.size} = ${countBlockInfo} + ${countBlockAD} + ${countBlockCM} + ${countBlockTName})`;
    }
  }

  // 辅助函数:创建通用按钮
  function createPanelButton(text, bgColor, onClick) {
    const button = document.createElement("button");
    button.className = "bilibili-blacklist-panel-btn";
    button.textContent = text;
    button.style.background = bgColor;
    button.addEventListener("click", onClick);
    return button;
  }

  // 辅助函数:为黑名单面板创建列表项
  function createBlacklistListItem(contentText, onRemoveClick) {
    const item = document.createElement("li");
    item.className = "bilibili-blacklist-list-item";

    const content = document.createElement("span");
    content.textContent = contentText;
    const removeBtn = createPanelButton("移除", "#f56c6c", onRemoveClick);

    item.appendChild(content);
    item.appendChild(removeBtn);
    return item;
  }

  /**
   * 刷新面板中的精确匹配黑名单显示。
   */
  function refreshExactMatchList() {
    if (!exactMatchListElement) {
      if (!isBlacklistPanelCreated()) {
        return;
      }
      exactMatchListElement = document.querySelector(
        "#bilibili-blacklist-exact-list"
      );
      if (!exactMatchListElement) {
        console.warn("[Bilibili-Blacklist] exactMatchListElement 未定义");
        return;
      }
    }
    exactMatchListElement.innerHTML = "";
    exactMatchBlacklist.forEach((upName) => {
      const item = createBlacklistListItem(upName, () => {
        removeFromExactBlacklist(upName);
      });
      exactMatchListElement.appendChild(item);
    });
    // 反转列表顺序,使最新添加的显示在顶部
    Array.from(exactMatchListElement.children)
      .reverse()
      .forEach((item) => exactMatchListElement.appendChild(item));

    if (exactMatchBlacklist.length === 0) {
      const empty = document.createElement("div");
      empty.className = "bilibili-blacklist-empty";
      empty.textContent = "暂无精确匹配屏蔽UP主";
      exactMatchListElement.appendChild(empty);
    }
  }

  /**
   * 刷新面板中的正则匹配黑名单显示。
   */
  function refreshRegexMatchList() {
    if (!regexMatchListElement) {
      if (!isBlacklistPanelCreated()) {
        return;
      }
      regexMatchListElement = document.querySelector(
        "#bilibili-blacklist-regex-list"
      );
      if (!regexMatchListElement) {
        console.warn("[Bilibili-Blacklist] regexMatchListElement 未定义");
        return;
      }
    }
    regexMatchListElement.innerHTML = "";

    regexMatchBlacklist.forEach((regex, index) => {
      const item = createBlacklistListItem(regex, () => {
        regexMatchBlacklist.splice(index, 1);
        saveBlacklistsToStorage();
        refreshRegexMatchList();
      });
      regexMatchListElement.appendChild(item);
    });

    // 反转列表顺序,使最新添加的显示在顶部
    Array.from(regexMatchListElement.children)
      .reverse()
      .forEach((item) => regexMatchListElement.appendChild(item));

    if (regexMatchBlacklist.length === 0) {
      const empty = document.createElement("div");
      empty.className = "bilibili-blacklist-empty";
      empty.textContent = "暂无正则匹配屏蔽规则";
      regexMatchListElement.appendChild(empty);
    }
  }

  /**
   * 刷新面板中的标签名黑名单显示。
   */
  function refreshTagNameList() {
    if (!tagNameListElement) {
      if (!isBlacklistPanelCreated()) {
        return;
      }
      tagNameListElement = document.querySelector(
        "#bilibili-blacklist-tname-list"
      );
      if (!tagNameListElement) {
        console.warn("[Bilibili-Blacklist] tagNameListElement 未定义");
        return;
      }
    }
    tagNameListElement.innerHTML = "";

    tagNameBlacklist.forEach((tagName) => {
      const item = createBlacklistListItem(tagName, () => {
        removeFromTagNameBlacklist(tagName);
      });
      tagNameListElement.appendChild(item);
    });
    // 反转列表顺序,使最新添加的显示在顶部
    Array.from(tagNameListElement.children)
      .reverse()
      .forEach((item) => tagNameListElement.appendChild(item));

    if (tagNameBlacklist.length === 0) {
      const empty = document.createElement("div");
      empty.className = "bilibili-blacklist-empty";
      empty.textContent = "暂无标签屏蔽规则";
      tagNameListElement.appendChild(empty);
    }
  }

  // 辅助函数:为设置创建切换按钮
  function createSettingToggleButton(labelText, configKey, title = null) {
    const container = document.createElement("div");
    container.className =
      "bilibili-blacklist-panel-row bilibili-blacklist-setting-toggle";
    container.title = title; // 设置鼠标悬停提示

    const label = document.createElement("span");
    label.textContent = labelText;

    const button = document.createElement("button");
    button.className = "bilibili-blacklist-config-btn";

    function refreshButtonAppearance() {
      button.textContent = globalPluginConfig[configKey] ? "开启" : "关闭";
      button.style.backgroundColor = globalPluginConfig[configKey]
        ? "#fb7299"
        : "#909399";
    }

    button.addEventListener("click", () => {
      globalPluginConfig[configKey] = !globalPluginConfig[configKey];
      refreshButtonAppearance();
      saveGlobalConfigToStorage();
      if (configKey === "flagHoverReveal" && !globalPluginConfig[configKey]) {
        restoreAllBlockedVideoOverlays();
      }
    });

    refreshButtonAppearance(); // 初始化按钮外观

    container.appendChild(label);
    container.appendChild(button);

    return container;
  }
  // 辅助函数:为设置创建输入文本
  function createSettingInput(
    labelText,
    configKey,
    title = null,
    constraints = {}
  ) {
    // 卡片扫描间隔设置
    const Container = document.createElement("div");
    Container.className =
      "bilibili-blacklist-panel-row bilibili-blacklist-setting-input-row";
    Container.title = title;

    const Label = document.createElement("span");
    Label.textContent = labelText;

    const Input = document.createElement("input");
    Input.type = "number";
    Input.className = "bilibili-blacklist-number-input";
    const { min = 0, max = null, step = null } = constraints;
    Input.min = `${min}`;
    if (max !== null) Input.max = `${max}`;
    if (step !== null) Input.step = `${step}`;
    Input.value = globalPluginConfig[configKey];

    const Button = document.createElement("button");
    Button.className =
      "bilibili-blacklist-config-btn bilibili-blacklist-config-btn-primary";
    Button.textContent = "保存";

    Button.addEventListener("click", () => {
      const val = Number(Input.value);
      const isInRange =
        Input.value.trim() !== "" &&
        Number.isFinite(val) &&
        val >= min &&
        (max === null || val <= max);
      if (isInRange) {
        globalPluginConfig[configKey] = val;
        saveGlobalConfigToStorage();
      } else {
        const rangeText = max === null ? `不小于 ${min}` : `${min} 到 ${max}`;
        alert(`请输入${rangeText}之间的有效数字!`);
      }
    });
    Container.appendChild(Label);
    Container.appendChild(Input);
    Container.appendChild(Button);

    return Container;
  }

  // 辅助函数:为设置创建下拉选择框
  function createSettingSelect(labelText, configKey, title = null, options = []) {
    const container = document.createElement("div");
    container.className =
      "bilibili-blacklist-panel-row bilibili-blacklist-setting-select-row";
    container.title = title;

    const label = document.createElement("span");
    label.textContent = labelText;

    const select = document.createElement("select");
    select.className = "bilibili-blacklist-select";
    select.addEventListener("change", () => {
      globalPluginConfig[configKey] = select.value;
      saveGlobalConfigToStorage();
    });

    // 按当前配置值选中对应项
    options.forEach((opt) => {
      const option = document.createElement("option");
      option.value = opt.value;
      option.textContent = opt.label;
      if (String(globalPluginConfig[configKey]) === String(opt.value)) {
        option.selected = true;
      }
      select.appendChild(option);
    });

    container.appendChild(label);
    container.appendChild(select);
    return container;
  }

  /**
   * 刷新面板中的配置设置显示。
   */
  function refreshConfigSettings() {
    if (!configListElement) {
      if (!isBlacklistPanelCreated()) {
        return;
      }
      configListElement = document.querySelector(
        "#bilibili-blacklist-config-list"
      );
      if (!configListElement) {
        console.warn("[Bilibili-Blacklist] configListElement 未定义");
        return;
      }
    }
    configListElement.innerHTML = "";

    // 临时开关按钮
    const tempToggleContainer = document.createElement("div");
    tempToggleContainer.className =
      "bilibili-blacklist-panel-row bilibili-blacklist-temp-toggle";

    const tempToggleLabel = document.createElement("span");
    tempToggleLabel.textContent = "临时开关";

    tempUnblockButton = document.createElement("button");
    tempUnblockButton.className = "bilibili-blacklist-config-btn";
    tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽";
    tempUnblockButton.style.background = isShowAllVideos
      ? "#dddddd"
      : "#fb7299";
    tempUnblockButton.addEventListener("click", toggleShowAllBlockedVideos);

    tempToggleContainer.appendChild(tempToggleLabel);
    tempToggleContainer.appendChild(tempUnblockButton);
    configListElement.appendChild(tempToggleContainer);

    const title = document.createElement("h4");
    title.textContent = "全局配置开关(部分功能刷新后生效)";
    configListElement.appendChild(title);

    // 添加配置切换按钮
    configListElement.appendChild(
      createSettingToggleButton(
        "屏蔽标题/Up主名",
        "flagInfo",
        "屏蔽标题/Up主名"
      )
    );
    configListElement.appendChild(
      createSettingToggleButton(
        "屏蔽分类标签",
        "flagTName",
        "通过请求API获取分类标签"
      )
    );

    // 标签缓存数量显示与清除按钮
    const tagNameListControlContainer = document.createElement("div");
    tagNameListControlContainer.className =
      "bilibili-blacklist-panel-row bilibili-blacklist-cache-control";
    tagNameListControlContainer.title = "打开视频播放页面可刷新";

    const tagNameListLabel = document.createElement("span");
    tagNameListLabel.textContent = `分类标签缓存数量: ${tagNameList.length}`;

    const clearTagNameListButton = document.createElement("button");
    clearTagNameListButton.className =
      "bilibili-blacklist-config-btn bilibili-blacklist-config-btn-danger";
    clearTagNameListButton.textContent = "清除";
    clearTagNameListButton.addEventListener("click", () => {
      if (confirm("确定要清除分类标签缓存吗?这不会影响已屏蔽的标签,但会使得下次需要重新从API获取标签信息。")) {
        tagNameList.length = 0;
        if (typeof saveTagNameListToStorage === "function") {
          saveTagNameListToStorage();
        } else {
          GM_setValue("tagNameList", []);
          GM_setValue("tLastTime", 0);
        }
        tagNameListLabel.textContent = `分类标签缓存数量: 0`;
      }
    });

    tagNameListControlContainer.appendChild(tagNameListLabel);
    tagNameListControlContainer.appendChild(clearTagNameListButton);
    configListElement.appendChild(tagNameListControlContainer);

    configListElement.appendChild(
      createSettingToggleButton(
        "屏蔽竖屏视频",
        "flagVertical",
        "通过请求API获取视频分辨率"
      )
    );
    configListElement.appendChild(
      createSettingToggleButton("屏蔽主页推荐", "flagAD", "直播/广告/分区推送")
    );
    configListElement.appendChild(
      createSettingToggleButton(
        "屏蔽主页视频软广",
        "flagCM",
        "cm.bilibili.com软广"
      )
    );

    // 自动连播遇到被屏蔽视频的处理方式
    configListElement.appendChild(
      createSettingSelect(
        "自动连播遇到被屏蔽视频:",
        "flagSkipBlockedAutoplay",
        "播放页开启自动连播并播到被屏蔽视频时:切换为未屏蔽视频 / 停止播放 / 不处理(按B站默认继续播放)。",
        [
          { value: "skip", label: "切换为未屏蔽视频" },
          { value: "stop", label: "停止播放" },
          { value: "off", label: "不处理(默认)" },
        ]
      )
    );

    //分割线
    const hr = document.createElement("hr");
    configListElement.appendChild(hr);

    configListElement.appendChild(
      createSettingToggleButton("遮挡被屏蔽视频", "flagKirby", "更加温和的方式")
    );
    configListElement.appendChild(
      createSettingToggleButton(
        "悬停后显示被遮挡视频",
        "flagHoverReveal",
        "鼠标在被遮挡的视频卡片上停留指定时间后临时显示,移开后重新遮挡。仅在“遮挡被屏蔽视频”开启时生效。"
      )
    );
    configListElement.appendChild(
      createSettingInput(
        "悬停显示延迟 (秒):",
        "hoverRevealDelaySeconds",
        "允许设置 0.1 到 5 秒。",
        { min: 0.1, max: 5, step: 0.1 }
      )
    );
    configListElement.appendChild(
      createSettingToggleButton(
        "加载时立即隐藏卡片",
        "flagHideOnLoad",
        "新卡片加载出来时是否立即隐藏,待处理完成后再决定显示或继续屏蔽。关闭此功能可能会导致卡片先显示后隐藏的闪烁。"
      )
    );

    configListElement.appendChild(
      createSettingInput(
        "卡片扫描间隔 (ms):",
        "blockScanInterval",
        "扫描新卡片的间隔时间,单位 ms。值越小,新卡片隐藏越快,但可能会增加CPU负担。建议值 200ms。"
      )
    );

    configListElement.appendChild(
      createSettingInput(
        "视频信息API请求间隔 (ms):",
        "processQueueInterval",
        "每个视频获取分类标签/视频分辨率时的API请求间隔时间,单位 ms。间隔时间越长,越不容易触发B站API限速。建议值 200ms。"
      )
    );
    configListElement.appendChild(
      createSettingInput(
        "竖屏视频比例阈值:",
        "verticalScaleThreshold",
        "获取的视频API信息后,判断视频是否为竖屏(长 除于 宽)的阈值。建议值 0.7。"
      )
    );
  }

  /**
   * 刷新黑名单管理面板中的所有标签页。
   */
  function refreshAllPanelTabs() {
    refreshExactMatchList();
    refreshRegexMatchList();
    refreshTagNameList();
    refreshConfigSettings();
  }

  /**
   * 检查黑名单管理面板是否已创建并存在于DOM中。
   * 如果找到,则设置全局 `managerPanel` 引用。
   * @returns {boolean} 如果面板存在则返回true,否则返回false。
   */
  function isBlacklistPanelCreated() {
    const panelInDom = document.querySelector(
      "#bilibili-blacklist-manager-panel"
    );
    if (panelInDom) {
      if (!managerPanel) {
        managerPanel = panelInDom;
      }
      return true;
    }
    return false;
  }

  /**
   * 创建黑名单管理面板。
   */
  function createBlacklistPanel() {
    if (isBlacklistPanelCreated()) {
      return;
    }
    managerPanel = document.createElement("div");
    managerPanel.id = "bilibili-blacklist-manager-panel"; // 确保ID唯一

    // 创建标签容器
    const tabContainer = document.createElement("div");
    tabContainer.className = "bilibili-blacklist-tabs";

    // 创建各个标签页的内容区域
    const exactContent = document.createElement("div");
    exactContent.className = "bilibili-blacklist-panel-content";
    exactContent.style.display = "block"; // 默认显示精确匹配

    const regexContent = document.createElement("div");
    regexContent.className = "bilibili-blacklist-panel-content";
    regexContent.style.display = "none";

    const tnameContent = document.createElement("div");
    tnameContent.className = "bilibili-blacklist-panel-content";
    tnameContent.style.display = "none";

    const configContent = document.createElement("div");
    configContent.className = "bilibili-blacklist-panel-content";
    configContent.style.display = "none";

    // 定义标签页数据
    const tabs = [
      { name: "精确匹配(Up名字)", content: exactContent },
      { name: "正则匹配(Up/标题)", content: regexContent },
      { name: "屏蔽分类", content: tnameContent },
      { name: "插件配置", content: configContent },
    ];
    tabs.forEach((tabData) => {
      const tab = document.createElement("div");
      tab.className = "bilibili-blacklist-tab";
      tab.textContent = tabData.name;
      tab.style.borderBottom =
        tabData.content.style.display === "block"
          ? "2px solid #fb7299"
          : "none";

      // 标签点击事件,切换内容显示
      tab.addEventListener("click", () => {
        tabs.forEach(({ tab: t, content: c }) => {
          t.style.borderBottom = "none";
          c.style.display = "none";
        });
        tab.style.borderBottom = "2px solid #fb7299";
        tabData.content.style.display = "block";
      });

      tabData.tab = tab; // 保存对标签元素的引用
      tabContainer.appendChild(tab);
    });

    // 创建面板头部
    const header = document.createElement("div");
    header.className = "bilibili-blacklist-panel-header";

    blockCountTitleElement = document.createElement("h3");
    blockCountTitleElement.title = "总数 =(UP/标题 + 广告 + CM + 分类/竖屏)";

    const closeBtn = document.createElement("button");
    closeBtn.className = "bilibili-blacklist-panel-close";
    closeBtn.textContent = "×";
    closeBtn.addEventListener("click", () => {
      managerPanel.style.display = "none";
    });

    header.appendChild(blockCountTitleElement);
    header.appendChild(closeBtn);

    const contentContainer = document.createElement("div");
    contentContainer.className = "bilibili-blacklist-panel-body";

    // 精确匹配添加输入框和按钮
    const addExactContainer = document.createElement("div");
    addExactContainer.className = "bilibili-blacklist-add-row";

    const exactInput = document.createElement("input");
    exactInput.type = "text";
    exactInput.placeholder = "输入要屏蔽的UP主名称";

    const addExactBtn = document.createElement("button");
    addExactBtn.className = "bilibili-blacklist-primary-btn";
    addExactBtn.textContent = "添加";
    addExactBtn.addEventListener("click", () => {
      const upName = exactInput.value.trim();
      if (upName) {
        addToExactBlacklist(upName);
        exactInput.value = "";
      }
    });
    addExactContainer.appendChild(exactInput);
    addExactContainer.appendChild(addExactBtn);
    exactContent.appendChild(addExactContainer);

    // 正则匹配添加输入框和按钮
    const addRegexContainer = document.createElement("div");
    addRegexContainer.className = "bilibili-blacklist-add-row";

    const regexInput = document.createElement("input");
    regexInput.type = "text";
    regexInput.placeholder = "输入正则表达式 (如: 小小.*Official)";

    const addRegexBtn = document.createElement("button");
    addRegexBtn.className = "bilibili-blacklist-primary-btn";
    addRegexBtn.textContent = "添加";
    addRegexBtn.addEventListener("click", () => {
      const regex = regexInput.value.trim();
      if (regex && !regexMatchBlacklist.includes(regex)) {
        try {
          new RegExp(regex); // 验证正则表达式
          regexMatchBlacklist.push(regex);
          saveBlacklistsToStorage();
          regexInput.value = "";
          refreshRegexMatchList();
        } catch (e) {
          alert("无效的正则表达式: " + e.message);
        }
      }
    });
    addRegexContainer.appendChild(regexInput);
    addRegexContainer.appendChild(addRegexBtn);
    regexContent.appendChild(addRegexContainer);

    // 创建列表元素
    exactMatchListElement = document.createElement("ul");
    exactMatchListElement.id = "bilibili-blacklist-exact-list";

    regexMatchListElement = document.createElement("ul");
    regexMatchListElement.id = "bilibili-blacklist-regex-list";

    tagNameListElement = document.createElement("ul");
    tagNameListElement.id = "bilibili-blacklist-tname-list";

    configListElement = document.createElement("ul");
    configListElement.id = "bilibili-blacklist-config-list";

    refreshAllPanelTabs(); // 初始化所有标签页内容
    exactContent.appendChild(exactMatchListElement);
    regexContent.appendChild(regexMatchListElement);
    tnameContent.appendChild(tagNameListElement);
    configContent.appendChild(configListElement);

    contentContainer.appendChild(exactContent);
    contentContainer.appendChild(regexContent);
    contentContainer.appendChild(tnameContent);
    contentContainer.appendChild(configContent);

    managerPanel.appendChild(tabContainer);
    managerPanel.appendChild(header);
    managerPanel.appendChild(contentContainer);

    document.body.appendChild(managerPanel);
    return managerPanel;
  }

  /**
   * 为插件添加全局CSS样式。
   */
  GM_addStyle(`
    /* ===== 屏蔽按钮容器 ===== */
    .bilibili-blacklist-block-container {
      display: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      padding: 2px;
      font-size: 12px;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      gap: 3px;
      z-index: 9999;
      pointer-events: none;
    }

    .bili-video-card:hover .bilibili-blacklist-block-container,
    .card-box:hover .bilibili-blacklist-block-container,
    .bilibili-blacklist-block-container-host:hover .bilibili-blacklist-block-container {
      display: flex !important;
    }

    .card-box .bilibili-blacklist-block-container {
      flex-direction: column;
      align-items: flex-start;
      justify-content: flex-start;
      height: 100%;
    }

    .card-box .bilibili-blacklist-tname-group {
      flex-direction: column;
      align-items: flex-end;
      margin-top: auto;
    }

    /* btn / reason / tname 共用基础外观 */
    .bilibili-blacklist-block-btn,
    .bilibili-blacklist-block-reason,
    .bilibili-blacklist-tname {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 20px;
      padding: 0 6px;
      box-sizing: border-box;
      font-size: 12px;
      line-height: 1;
      color: white;
      text-align: center;
      white-space: nowrap;
      border: none;
      border-radius: 2px;
    }

    .bilibili-blacklist-block-btn {
      position: static;
      width: 40px;
      pointer-events: auto !important;
      background-color: #fb7299dd;
      cursor: pointer;
    }

    .bilibili-blacklist-block-reason {
      background-color: #f56c6c;
      pointer-events: none;
    }

    .bilibili-blacklist-tname-group {
      display: flex;
      flex-direction: row;
      padding: 0 5px;
      gap: 3px;
      align-items: center;
      margin-left: auto;
      max-width: 80%;
      pointer-events: none;
    }

    .bilibili-blacklist-tname {
      background-color: #fb7299dd;
      text-overflow: ellipsis;
      overflow: hidden;
      pointer-events: auto;
      cursor: pointer;
    }

    /* ===== 修复视频卡片布局 ===== */
    .bili-video-card__cover {
      contain: layout !important;
    }

    /* ===== 管理面板 ===== */
    #bilibili-blacklist-manager-panel {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 500px;
      max-height: 80vh;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      z-index: 99999;
      overflow: hidden;
      display: none;
      flex-direction: column;
      font-size: 15px;
      color: var(--text2, #000);
      background-color: var(--bg1, #fff);
      opacity: 0.85;
    }

    #bilibili-blacklist-manager-panel h3,
    #bilibili-blacklist-manager-panel h4 {
      color: var(--text2, #000);
    }

    #bilibili-blacklist-manager-panel h3 {
      margin: 0;
      font-weight: 500;
    }

    #bilibili-blacklist-manager-panel h4 {
      font-weight: bold;
      margin-bottom: 12px;
    }

    #bilibili-blacklist-manager-panel ul {
      list-style: none;
      padding: 0;
      margin: 0;
    }

    #bilibili-blacklist-manager-panel hr {
      margin: 12px 0;
      border: none;
      border-top: 2px solid #ddd;
    }

    /* 按钮基础交互 */
    #bilibili-blacklist-manager-panel button {
      transition: background-color 0.2s;
    }

    #bilibili-blacklist-manager-panel button:hover {
      opacity: 0.9;
    }

    /* 输入框 */
    #bilibili-blacklist-manager-panel input:focus {
      outline: none;
      border-color: #fb7299 !important;
    }

    #bilibili-blacklist-manager-panel input[type="text"] {
      flex: 1;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }

    #bilibili-blacklist-manager-panel select {
      flex: 1;
      min-width: 120px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      color: var(--text2, #000);
      background-color: var(--bg1, #fff);
    }

    .bilibili-blacklist-setting-select-row {
      margin-bottom: 8px;
    }

    /* 面板结构 */
    .bilibili-blacklist-tabs {
      display: flex;
      border-bottom: 1px solid #f1f2f3;
    }

    .bilibili-blacklist-tab {
      padding: 12px 16px;
      cursor: pointer;
      font-weight: 500;
    }

    .bilibili-blacklist-panel-content {
      padding: 16px;
      overflow-y: auto;
      flex: 1;
    }

    .bilibili-blacklist-panel-header {
      padding: 16px;
      border-bottom: 1px solid #f1f2f3;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .bilibili-blacklist-panel-close {
      background: none;
      border: none;
      cursor: pointer;
      padding: 0 8px;
      color: var(--text2, #000);
    }

    .bilibili-blacklist-panel-body {
      display: flex;
      flex-direction: column;
      flex: 1;
      overflow: hidden;
    }

    /* 布局行 */
    .bilibili-blacklist-panel-row,
    .bilibili-blacklist-add-row {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .bilibili-blacklist-panel-row > span:first-child {
      flex: 1;
    }

    .bilibili-blacklist-add-row {
      margin-bottom: 16px;
    }

    .bilibili-blacklist-setting-toggle {
      margin-bottom: 8px;
    }

    .bilibili-blacklist-setting-input-row {
      margin-top: 16px;
    }

    .bilibili-blacklist-temp-toggle {
      margin: 20px 0;
    }

    .bilibili-blacklist-cache-control {
      margin-bottom: 8px;
    }

    /* 列表项 */
    .bilibili-blacklist-list-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 0;
      border-bottom: 1px solid #f1f2f3;
    }

    .bilibili-blacklist-list-item > span {
      flex: 1;
    }

    .bilibili-blacklist-empty {
      text-align: center;
      padding: 16px;
      color: #999;
    }

    /* 按钮 */
    .bilibili-blacklist-panel-btn,
    .bilibili-blacklist-config-btn,
    .bilibili-blacklist-primary-btn {
      color: #fff;
      border: none;
      cursor: pointer;
    }

    .bilibili-blacklist-panel-btn {
      padding: 4px 8px;
      border-radius: 4px;
    }

    .bilibili-blacklist-config-btn {
      padding: 6px 12px;
      border-radius: 4px;
    }

    .bilibili-blacklist-config-btn-primary {
      background-color: #fb7299;
    }

    .bilibili-blacklist-config-btn-danger {
      background-color: #f56c6c;
    }

    .bilibili-blacklist-primary-btn {
      padding: 8px 16px;
      background: #fb7299;
      border-radius: 4px;
    }

    .bilibili-blacklist-number-input {
      width: 100px;
      padding: 6px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }

    /* ===== 顶栏管理按钮 ===== */
    #bilibili-blacklist-manager-button {
      cursor: pointer;
    }

    #bilibili-blacklist-manager-button .right-entry-item {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }

    #bilibili-blacklist-manager-button .right-entry__outside {
      margin-bottom: -5px;
    }

    #bilibili-blacklist-manager-button:hover svg {
      transform: scale(1.1);
    }

    #bilibili-blacklist-manager-button svg {
      transition: transform 0.2s;
    }

    /* ===== 卡比覆盖层 ===== */
    #bilibili-blacklist-kirby {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      pointer-events: none;
      z-index: 10;
      border-radius: 6px;
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      transition: opacity ${KIRBY_FADE_DURATION_MS / 1000}s ease;
    }

    #bilibili-blacklist-kirby.bilibili-blacklist-kirby-video {
      justify-content: flex-start;
      
    }

    #bilibili-blacklist-kirby svg {
      opacity: 0.15;
      filter: none;
      margin-top: -40px;
    }

    #bilibili-blacklist-kirby.bilibili-blacklist-kirby-video svg {
      margin-top: -10px;
    }

    /* ===== 用户空间页屏蔽按钮 ===== */
    .bilibili-blacklist-up-block-btn-host {
      display: inline-flex;
      align-items: center;
    }

    .bilibili-blacklist-up-block-btn {
      width: 100px;
      height: 30px;
      margin-left: 10px;
      color: #fff;
      border-radius: 5px;
      border: 1px solid #fb7299;
    }

    /* ===== 灰度效果 ===== */
    .bilibili-blacklist-grayscale {
      filter: grayscale(95%);
    }
  `);

  /**
   * 返回卡比图标的SVG代码。
   * @returns {string} SVG字符串。
   */
  function getKirbySVG() {
    return `
        <svg width="35" height="35" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"  >
            <ellipse cx="70" cy="160" rx="30" ry="15" fill="#cc3333" />
            <ellipse cx="130" cy="160" rx="30" ry="15" fill="#cc3333" />
            <ellipse cx="50" cy="120" rx="20" ry="20" fill="#ffb6c1" />
            <ellipse cx="150" cy="120" rx="20" ry="20" fill="#ffb6c1" />
            <circle cx="100" cy="110" r="60" fill="#ffb6c1" />
            <ellipse cx="80" cy="90" rx="10" ry="22" fill="blue" />
            <ellipse cx="80" cy="88" rx="10" ry="15" fill="black" />
            <ellipse cx="80" cy="82" rx="8" ry="12" fill="#ffffff" />
            <ellipse cx="80" cy="90" rx="10" ry="22" fill="#00000000" stroke="#000000" strokeWidth="4" />
            <ellipse cx="120" cy="90" rx="10" ry="22" fill="blue" />
            <ellipse cx="120" cy="88" rx="10" ry="15" fill="black" />
            <ellipse cx="120" cy="82" rx="8" ry="12" fill="#ffffff" />
            <ellipse cx="120" cy="90" rx="10" ry="22" fill="#00000000" stroke="#000000" strokeWidth="4" />
            <ellipse cx="60" cy="110" rx="8" ry="5" fill="#ff4466" />
            <ellipse cx="140" cy="110" rx="8" ry="5" fill="#ff4466" />
            <path d="M 90 118 Q 100 125, 110 118" stroke="black" strokeWidth="3" fill="transparent" />
        </svg>
    `;
  }

  /**
   * 渐显卡比覆盖层(鼠标移开后恢复遮挡)。
   * @param {HTMLElement} overlay - 卡比覆盖层元素。
   */
  function fadeInKirbyOverlay(overlay) {
    if (!overlay) return;
    const pendingTimer = kirbyFadeTimers.get(overlay);
    if (pendingTimer) {
      clearTimeout(pendingTimer);
      kirbyFadeTimers.delete(overlay);
    }
    overlay.style.display = "flex";
    overlay.style.opacity = "0";
    void overlay.offsetHeight; // 强制重排以触发过渡动画
    overlay.style.opacity = "1";
  }

  /**
   * 渐隐卡比覆盖层(悬停临时显示视频)。
   * @param {HTMLElement} overlay - 卡比覆盖层元素。
   */
  function fadeOutKirbyOverlay(overlay) {
    if (!overlay) return;
    const pendingTimer = kirbyFadeTimers.get(overlay);
    if (pendingTimer) clearTimeout(pendingTimer);
    overlay.style.opacity = "0";
    kirbyFadeTimers.set(
      overlay,
      setTimeout(() => {
        kirbyFadeTimers.delete(overlay);
        if (overlay.isConnected && overlay.style.opacity === "0") {
          overlay.style.display = "none";
        }
      }, KIRBY_FADE_DURATION_MS)
    );
  }

  /**
   * 取消卡比覆盖层的渐隐/渐显计时器。
   * @param {HTMLElement} overlay - 卡比覆盖层元素。
   */
  function cancelKirbyFade(overlay) {
    if (!overlay) return;
    const pendingTimer = kirbyFadeTimers.get(overlay);
    if (pendingTimer) {
      clearTimeout(pendingTimer);
      kirbyFadeTimers.delete(overlay);
    }
  }

  /**
   * 恢复所有被悬停临时显示的视频遮罩。
   */
  function restoreAllBlockedVideoOverlays() {
    if (isShowAllVideos) return;
    blockedVideoCards.forEach((card) => {
      const overlay = card.querySelector("#bilibili-blacklist-kirby");
      if (overlay) fadeInKirbyOverlay(overlay);
    });
  }

  /**
   * 为被遮挡的视频卡片绑定悬停临时显示行为。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   */
  function bindHoverRevealToCard(cardElement) {
    if (hoverRevealBoundCards.has(cardElement)) return;
    hoverRevealBoundCards.add(cardElement);

    cardElement.addEventListener("mouseenter", () => {
      const realCard = getRealVideoCardElement(cardElement);
      if (
        !globalPluginConfig.flagHoverReveal ||
        isShowAllVideos ||
        !blockedVideoCards.has(realCard)
      ) {
        return;
      }

      const overlayOnEnter = cardElement.querySelector(
        "#bilibili-blacklist-kirby"
      );
      // 若正在渐隐,先取消,避免悬停期间遮罩消失
      cancelKirbyFade(overlayOnEnter);

      const existingTimer = hoverRevealTimers.get(cardElement);
      if (existingTimer) clearTimeout(existingTimer);

      const delaySeconds = Math.min(
        5,
        Math.max(0.1, Number(globalPluginConfig.hoverRevealDelaySeconds) || 1)
      );
      const timer = setTimeout(() => {
        hoverRevealTimers.delete(cardElement);
        if (!globalPluginConfig.flagHoverReveal || isShowAllVideos) return;

        const overlay = cardElement.querySelector(
          "#bilibili-blacklist-kirby"
        );
        if (overlay && blockedVideoCards.has(realCard)) {
          fadeOutKirbyOverlay(overlay);
        }
      }, delaySeconds * 1000);
      hoverRevealTimers.set(cardElement, timer);
    });

    cardElement.addEventListener("mouseleave", () => {
      const timer = hoverRevealTimers.get(cardElement);
      if (timer) {
        clearTimeout(timer);
        hoverRevealTimers.delete(cardElement);
      }

      if (isShowAllVideos) return;
      const overlay = cardElement.querySelector("#bilibili-blacklist-kirby");
      if (
        overlay &&
        blockedVideoCards.has(getRealVideoCardElement(cardElement))
      ) {
        fadeInKirbyOverlay(overlay);
      }
    });
  }

  /**
   * 为视频卡片添加卡比主题的覆盖层。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   */
  function addKirbyOverlayToCard(cardElement) {
    bindHoverRevealToCard(cardElement);
    // 如果已经有Kirby覆盖层,则不重复添加
    if (cardElement.querySelector("#bilibili-blacklist-kirby") != null) return;
    const kirbyWrapper = document.createElement("div");
    kirbyWrapper.innerHTML = getKirbySVG();
    kirbyWrapper.id = "bilibili-blacklist-kirby";
    if (isCurrentPageVideo()) {
      kirbyWrapper.classList.add("bilibili-blacklist-kirby-video");
    }

    const svg = kirbyWrapper.querySelector("svg");
    if (svg) {
      const cardRect = cardElement.getBoundingClientRect();
      const size = Math.min(cardRect.width, cardRect.height) * 0.8;
      svg.setAttribute("width", `${size}px`);
      svg.setAttribute("height", `${size}px`);
    }

    const hostElement = isCurrentPageCategory()
      ? cardElement.querySelector(".bili-video-card") || cardElement
      : cardElement;

    // 确保宿主元素有position属性以便子元素绝对定位
    const hostStyle = getComputedStyle(hostElement);
    if (hostStyle.position === "static" || !hostStyle.position) {
      hostElement.style.position = "relative";
    }

    hostElement.appendChild(kirbyWrapper);
  }

  /**
   * 从视频卡片中移除卡比覆盖层。
   * @param {HTMLElement} cardElement - 视频卡片元素。
   */
  function removeKirbyOverlay(cardElement) {
    const kirbyWrapper = cardElement.querySelector("#bilibili-blacklist-kirby");
    if (kirbyWrapper) {
      kirbyWrapper.remove();
    }
  }

  // 监听页面可见性变化
  document.addEventListener("visibilitychange", () => {
    isPageCurrentlyActive = !document.hidden;
  });

  // 监听窗口焦点获取 (用户请求停用)
  /*
  window.addEventListener("focus", () => {
    isPageCurrentlyActive = true;
  });
  */

  // 监听窗口焦点失去 (用户请求停用)
  /*
  window.addEventListener("blur", () => {
    isPageCurrentlyActive = false;
  });
  */

  // MutationObserver 检测动态加载的新内容
  const contentObserver = new MutationObserver((mutations) => {
    let shouldCheck = false;
    // 对视频播放页进行优化,只在实际添加了可见元素时触发扫描
    if (isCurrentPageVideo()) {
      mutations.forEach((mutation) => {
        if (mutation.addedNodes.length > 0) {
          shouldCheck = Array.from(mutation.addedNodes).some((node) => {
            if (node.nodeType !== Node.ELEMENT_NODE) return false;
            // 检查节点是否有实际的尺寸,避免不必要的扫描
            const hasVisibleContent =
              node.offsetWidth > 0 ||
              node.offsetHeight > 0 ||
              node.querySelector("[offsetWidth], [offsetHeight]");
            return hasVisibleContent;
          });
        }
      });
    } else {
      // 其他页面只要有节点添加就触发
      mutations.forEach((mutation) => {
        if (mutation.addedNodes.length > 0) {
          shouldCheck = true;
        }
      });
    }

    if (shouldCheck) {
      // 使用setTimeout延迟扫描,避免短时间内多次触发

      setTimeout(() => {
        scanAndBlockVideoCards();
        if (isCurrentPageMain()) {
          blockMainPageAds(); // 主页广告屏蔽
        }
        if (isCurrentPageVideo()) {
          blockVideoPageAds(); // 视频页广告屏蔽
        }
        if (!document.getElementById("bilibili-blacklist-manager-button")) {
         // addBlacklistManagerButton(); // 确保管理按钮存在
        }
        
      }, globalPluginConfig.blockScanInterval);
    }
  });

  /**
   * 在指定容器上初始化MutationObserver。
   * @param {string} containerIdOrSelector - 要观察的容器的ID或CSS选择器。
   */
  function initializeObserver(containerIdOrSelector) {
    const rootNode =
      document.getElementById(containerIdOrSelector) ||
      document.querySelector(containerIdOrSelector) ||
      document.documentElement; // 默认观察整个文档

    contentObserver.observe(rootNode, {
      childList: true,
      subtree: true,
    });
  }

  /**
   * 根据当前页面初始化脚本。
   */
  function initializeScript() {
    if (!isfirstLoad) return;
    isfirstLoad = false;
    // 重置状态变量
    isBlockingOperationInProgress = false;
    lastBlockScanExecutionTime = 0;
    blockedVideoCards = new Set();
    videoCardProcessQueue = new Set();
    processedVideoCards = new WeakSet();

    // 根据当前页面URL判断并初始化
    if (isCurrentPageMain()) {
      initializeMainPage();
      blockMainPageAds();
    } else if (isCurrentPageSearch()) {
      initializeSearchPage();
      blockMainPageAds(); // 搜索页也进行主页广告屏蔽
    } else if (isCurrentPageVideo()) {
      initializeVideoPage();
      updateTNameList();
    } else if (isCurrentPageCategory()) {
      initializeCategoryPage();
      updateTNameList();
    } else if (isCurrentUserSpace()) {
      initializeUserSpace();
    } else {
      return; // 不支持的页面不进行初始化
    }
    createBlacklistPanel(); // 创建管理面板
    addBlacklistManagerButton(); // 立即挂载管理按钮,避免在视频页被迟到的顶栏渲染顶掉前不可见;后续由观察器兜底
    console.log("[bilibili-blacklist] 脚本已加载🥔");
    
  }
  let isfirstLoad = true;
  let rescanVideoPageTimer = null; // 视频页定时补充扫描定时器
  // 监听DOMContentLoaded并检查readyState以进行早期初始化
  // initializeScript 内部已通过 isfirstLoad 保证只执行一次
  document.addEventListener("DOMContentLoaded", initializeScript);
  if (document.readyState === "complete" && isfirstLoad) {
      initializeScript();
  }

  /**
   * 检查当前页面是否为Bilibili主页。
   * @returns {boolean} 如果是主页则返回true,否则返回false。
   */
  function isCurrentPageMain() {
    return location.pathname === "/" || location.pathname === "/index.html";
  }

  /**
   * 初始化主页特有的功能。
   */
  function initializeMainPage() {
    initializeObserver("feedchannel-main"); // 观察主页内容区域
    console.log("[bilibili-blacklist] 主页已加载🍓");
  }

  /**
   * 检查当前页面是否为Bilibili搜索结果页。
   * @returns {boolean} 如果是搜索页则返回true,否则返回false。
   */
  function isCurrentPageSearch() {
    return location.hostname === "search.bilibili.com";
  }

  /**
   * 初始化搜索页特有的功能。
   */
  function initializeSearchPage() {
    initializeObserver("i_cecream"); // 观察搜索结果内容区域
    console.log("[bilibili-blacklist] 搜索页已加载🍉");
  }

  /**
   * 检查当前页面是否为Bilibili视频播放页。
   * @returns {boolean} 如果是视频播放页则返回true,否则返回false。
   */
  function isCurrentPageVideo() {
    return location.pathname.startsWith("/video/");
  }

  /**
   * 初始化视频播放页特有的功能。
   */
  function initializeVideoPage() {
    // **用户修改 2: 延迟 5 秒启动屏蔽功能**
    console.log("[bilibili-blacklist] 播放页已加载,将延迟 5 秒启动功能。🍇");
    const flag = globalPluginConfig.flagSkipBlockedAutoplay;
    globalPluginConfig.flagSkipBlockedAutoplay = "off";
    // 延迟 5 秒执行核心功能
    setTimeout(() => {
      initializeObserver("right-container"); // 观察视频播放页右侧推荐区域
      // 首次手动扫描和广告屏蔽
      scanAndBlockVideoCards();
      blockVideoPageAds();
      // 自动连播遇到被屏蔽视频时的处理(停止/切换/不处理,由用户配置)
      initAutoplaySkip();
      // 视频页在页面内切集后,右侧推荐会原地重建;观察器可能绑定到已替换的节点,
      // 这里定时补充扫描,确保新加载的卡片也能被处理(scanAndBlockVideoCards 内部有节流与去重)。
      rescanVideoPageTimer = setInterval(() => {
        scanAndBlockVideoCards();
        globalPluginConfig.flagSkipBlockedAutoplay= flag; // 第一次打开页面时,无论如何都不做处理
      }, 2500);
      // 顶栏可能有数秒延迟渲染,若在这之前已超过6个li,手动补挂管理按钮
      addBlacklistManagerButton();
      
      console.log("[bilibili-blacklist] 视频播放页屏蔽功能已启动。");
    }, 5000); // 5000 毫秒 = 5 秒
    
  }


  /**
   * 检查当前页面是否为Bilibili分类页。
   * @returns {boolean} 如果是分类页则返回true,否则返回false。
   */
  function isCurrentPageCategory() {
    return location.pathname.startsWith("/c/");
  }

  /**
   * 初始化分类页特有的功能。
   */
  function initializeCategoryPage() {
    initializeObserver("app"); // 观察整个app容器
    console.log("[bilibili-blacklist] 分类页已加载🍊");
  }

  /**
   * 检查当前页面是否为Bilibili用户空间页。
   * @returns {boolean} 如果是用户空间页则返回true,否则返回false。
   */
  function isCurrentUserSpace() {
    return location.hostname === "space.bilibili.com";
  }

  /**
   * 初始化用户空间页特有的功能。
   */
  function initializeUserSpace() {
    console.log("[bilibili-blacklist] 用户空间已加载🍎");
    const upNameSelector = "#h-name, .nickname"; // UP主名称的选择器
    // 创建一个MutationObserver来等待UP主名称元素加载
    const observerForUpName = new MutationObserver((mutations, observer) => {
      const upNameElement = document.querySelector(upNameSelector);
      if (upNameElement) {
        observer.disconnect(); // 找到元素后停止观察
        addBlockButtonToUserSpace(upNameElement);
      }
    });

    observerForUpName.observe(document.body, {
      childList: true,
      subtree: true,
    });
    // 立即检查一次,如果元素已经存在则直接处理
    const initialUpNameElement = document.querySelector(upNameSelector);
    if (initialUpNameElement) {
      observerForUpName.disconnect();
      addBlockButtonToUserSpace(initialUpNameElement);
    }
  }

  /**
   * 在用户空间页面上的UP主名称元素添加屏蔽/取消屏蔽按钮。
   * @param {HTMLElement} upNameElement - 包含UP主名称的元素。
   */
  function addBlockButtonToUserSpace(upNameElement) {
    const upName = upNameElement.textContent.trim();
    // 避免重复添加按钮
    if (upNameElement.querySelector(".bilibili-blacklist-up-block-btn")) {
      return;
    }

    // 调整UP主名称元素的样式,以便容纳按钮
    upNameElement.classList.add("bilibili-blacklist-up-block-btn-host");

    const button = document.createElement("button");
    button.className = "bilibili-blacklist-up-block-btn";
    button.textContent = "屏蔽";

    // 刷新按钮状态和页面灰度效果
    const refreshButtonStatus = () => {
      const blocked = isBlacklisted(upName);
      if (blocked) {
        button.textContent = "已屏蔽";
        button.style.backgroundColor = "#dddddd";
        button.style.border = "1px solid #ccc";
        upNameElement.style.textDecoration = "line-through"; // 添加删除线
        document.body.classList.add("bilibili-blacklist-grayscale"); // 添加灰度滤镜
      } else {
        button.textContent = "屏蔽";
        button.style.backgroundColor = "#fb7299";
        button.style.border = "1px solid #fb7299";
        upNameElement.style.textDecoration = "none"; // 移除删除线
        document.body.classList.remove("bilibili-blacklist-grayscale"); // 移除灰度滤镜
      }
    };

    button.addEventListener("click", (e) => {
      e.stopPropagation();
      const blocked = isBlacklisted(upName);
      if (blocked) {
        removeFromExactBlacklist(upName);
      } else {
        addToExactBlacklist(upName);
      }
      refreshButtonStatus(); // 更新按钮状态
    });

    refreshButtonStatus(); // 设置按钮初始状态

    upNameElement.appendChild(button);
  }

  /**
   * 屏蔽主页上的广告。
   */
  function blockMainPageAds() {
    if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回
    const adSelectors = [
      ".floor-single-card", // 分区推荐
      ".bili-live-card", // 直播推广
      ".btn-ad", // 广告按钮
    ];
    adSelectors.forEach((selector) => {
      document.querySelectorAll(selector).forEach((adCard) => {
        hideVideoCard(adCard, "ad"); // 隐藏广告卡片
      });
    });
  }

  /**
   * 屏蔽视频播放页上的广告。
   */
  function blockVideoPageAds() {
    if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回
    const adSelectors = [
      ".video-card-ad-small", // 右上角推广
      ".slide-ad-exp", // 大推广
      ".video-page-game-card-small", // 游戏推广
      ".activity-m-v1", // 活动推广
      ".video-page-special-card-small", // 特殊卡片推广
      ".ad-floor-exp", // 广告地板
      ".btn-ad", // 广告按钮
      ".video-page-operator-card-small", // 运营推广
      ".ad-report",//广告
    ];

    adSelectors.forEach((selector) => {
      document.querySelectorAll(selector).forEach((adCard) => {
        hideVideoCard(adCard, "ad"); // 隐藏广告卡片
      });
    });
  }

  /// 12-16-2025 临时修复B站API无法获取Tname 问题,使用tid + 保存在本地的列表实现
  // 从Video page 获取 本地资源
  function getTNameListFormVideoPage() {
    try {
      var channelKv = unsafeWindow.__INITIAL_STATE__.channelKv;
      if (!channelKv) return [];

      var result = [];

      // 遍历主频道
      if (Array.isArray(channelKv)) {
        channelKv.forEach(element => {
          // if (!element.channelId || !element.name) {
          //result.push({ id: element.channelId, tname: element.name });

          // }

          // 遍历子频道(sub)
          var subList = element.sub;
          if (Array.isArray(subList)) {
            subList.forEach(subelement => {
              if (element.channelId && element.name && subelement.tid && subelement.name) {
                result.push({ id: subelement.tid, name: element.name, name_v2: subelement.name });
              }
            });
          }
        });
      }
      return result;
    } catch (e) {
      console.error("[bilibili-blacklist] 获取频道数据失败:", e);
      return [];
    }
  }
  // 增量更新 Tname list //24小时一次
  function updateTNameList() {
    if (tagNameList.length >= 1000) tagNameList = []; //防止过大时卡顿,清空重建
    if (tagNameList.length === 0) tagListLastTime = 0; //确保初始为空时进行更新

    const now = Date.now();
    if (now - tagListLastTime < 6000) {
      console.log("[bilibili-blacklist] 标签名列表最近已更新,跳过本次更新。");
      return;
    }

    const newList = getTNameListFormVideoPage();
    if (newList.length === 0) {
      console.warn("[bilibili-blacklist] 未能获取到新的标签名列表。");
      return;
    }

    console.log(`[bilibili-blacklist] 获取到 ${newList.length} 个标签名,开始合并更新。`);

    // 构建现有标签的映射以便快速查找(基于id)
    const existingMap = new Map();
    tagNameList.forEach(item => existingMap.set(String(item.id), item));

    let updated = false;
    for (const item of newList) {
      const id = String(item.id);
      const name = item.name; // 注意:getTNameListFormVideoPage 返回的是 tname 属性
      const name_v2 = item.name_v2;
      if (!existingMap.has(id)) {
        // 新增条目
        tagNameList.push({ id: item.id, name, name_v2 });
        existingMap.set(id, { id: item.id, name, name_v2 });
        updated = true;
      } else {
        // 已存在,检查名称是否一致,若不一致则更新
        const existing = existingMap.get(id);
        if (existing.name !== name) {
          existing.name = name;
          updated = true;
        }
      }
    }

    if (updated) {
      saveTagNameListToStorage();
      tagListLastTime = now; // 更新局部变量以保持同步
      console.log("[bilibili-blacklist] 标签名列表已更新并保存。");
    } else {
      console.log("[bilibili-blacklist] 标签名列表无变化,仅更新时间戳。");
      // 即使没有变化,也更新最后更新时间,避免频繁检查
      GM_setValue("tLastTime", now);
      tagListLastTime = now; // 更新局部变量以保持同步
    }
  }

  /**
   * 自动连播处理模块。
   *
   * 背景:B站播放页(/video/BVxx) 开启“自动连播”后,播放器会自己从“相关推荐 / 接下来播放”里挑
   * 下一个视频播放。这个“下一个”来自播放器内部的推荐/播放列表数据,而不是右侧已经
   * 渲染出来的卡片;而且 B 站新版播放器是在页面内原地切换视频,地址栏 URL 不一定同步变。
   * 因此仅靠隐藏卡片(藏 DOM)或监听 URL 变化都不靠谱。
   *
   * 本模块的正确做法:识别“当前正在播放的视频”本身(读取播放器当前 UP 主名 + 标题,
   * 视切集时会原地更新),一旦发现当前播放的视频被屏蔽,就按配置
   * (globalPluginConfig.flagSkipBlockedAutoplay)采取三种行为之一:
   *     "skip"  切换到第一条未屏蔽的视频(避免继续播被屏蔽视频)
   *     "stop"  停止播放
   *     "off"   什么都不做(保留 B 站默认行为,继续播放被屏蔽的视频)
   */

  // 内部运行状态
  let autoplayWatchTimer = null; // 轮询定时器
  let lastSignature = ""; // 上一次检测到的“当前播放视频”特征(UP名 + 标题 + BV)
  let lastHandledBv = ""; // 上一次已经处理过的目标 BV,避免重复处理
  let isHandling = false; // 防止多次处理并发

  // 当前播放视频的标题选择器(B站标题通常为 h1)
  const CURRENT_VIDEO_TITLE_SELECTORS = [
    "h1.video-info-title",
    "h1.video-title",
    "h1",
  ];
  // 当前播放视频的 UP 主名选择器(视频信息栏附近;新版常用 .upname)
  const CURRENT_VIDEO_UP_SELECTORS = [
    ".up-info-container .name",
    ".up-info .name",
    ".video-info .name",
    ".video-info-v2 .up-name",
    ".bili-video-info .up-name",
    ".up-name",
    ".up-info-container .upname a span",
    ".video-info .upname a span",
    ".video-info-container .upname a span",
    ".upname a span",
    ".upname a",
    ".upname",
  ];

  /**
   * 从 URL 提取 BV ID。
   * @returns {string|null}
   */
  function getBvFromUrl() {
    const m = location.pathname.match(/\/video\/(BV\w+)/);
    return m ? m[1] : null;
  }

  /**
   * 尝试从 window.player 读取当前播放视频的 BV(比 URL 更贴近“正在播的视频”)。
   * @returns {string|null}
   */
  function getBvFromPlayer() {
    const p = window.player;
    if (!p) return null;
    const tries = [
      () => p.getVideoID && p.getVideoID(),
      () => p.getVideo && p.getVideo().bvid,
      () => p.getVideoInfo && p.getVideoInfo().bvid,
      () => p.__video && p.__video.bvid,
    ];
    for (const f of tries) {
      try {
        const v = f();
        if (typeof v === "string" && /^BV\w+/.test(v)) return v;
      } catch (e) {
        // 该方法不适用,继续尝试
      }
    }
    return null;
  }

  /**
   * 取当前播放视频的 BV:优先播放器,其次 URL。
   * @returns {string|null}
   */
  function getCurrentBv() {
    return getBvFromPlayer() || getBvFromUrl();
  }

  /**
   * 从页面 DOM 读取“当前正在播放的视频”的标题和 UP 主名。
   * 播放器在切集时会原地更新这些信息,因此它是判断“当前播放视频”最可靠的信号。
   * @returns {{upName: string, title: string}}
   */
  function getPlayingVideoInfo() {
    let title = "";
    for (const sel of CURRENT_VIDEO_TITLE_SELECTORS) {
      const el = document.querySelector(sel);
      if (el && el.textContent.trim()) {
        title = el.textContent.trim();
        break;
      }
    }
    let upName = "";
    for (const sel of CURRENT_VIDEO_UP_SELECTORS) {
      const el = document.querySelector(sel);
      if (el && el.textContent.trim()) {
        upName = el.textContent.trim();
        break;
      }
    }
    return { upName, title };
  }


  /**
   * 从 __INITIAL_STATE__ 读取“bvid -> {upName, title}”映射。
   * 数据源:videoData(当前视频)+ related(推荐列表),二者都带 UP 主名/标题。
   * @returns {Object<string, {upName:string,title:string}>}
   */
  function buildBvInfoMapFromInitialState() {
    const map = {};
    const state =
      typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
    if (!state) return map;
    if (state.videoData && state.videoData.bvid) {
      map[state.videoData.bvid] = {
        upName: (state.videoData.owner && state.videoData.owner.name) || "",
        title: state.videoData.title || "",
      };
    }
    if (Array.isArray(state.related)) {
      for (const item of state.related) {
        if (!item.bvid) continue;
        map[item.bvid] = {
          upName: (item.owner && item.owner.name) || "",
          title: item.title || "",
        };
      }
    }
    return map;
  }

  /**
   * 从 __INITIAL_STATE__ 读取“title -> {upName, bvid}”映射。
   * 自动连播卡片里只显示标题、没有 UP 名,所以用标题去 __INITIAL_STATE__.related 里反查 UP 名最靠谱。
   * @returns {Object<string, {upName:string, bvid:string}>}
   */
  function buildTitleInfoMapFromInitialState() {
    const map = {};
    const state =
      typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
    if (!state) return map;
    const add = (title, upName, bvid) => {
      if (!title || !upName) return;
      if (!map[title] || !map[title].upName) {
        map[title] = { upName, bvid };
      }
    };
    if (state.videoData && state.videoData.bvid) {
      add(state.videoData.title, state.videoData.owner && state.videoData.owner.name, state.videoData.bvid);
    }
    if (Array.isArray(state.related)) {
      for (const item of state.related) {
        add(item.title, item.owner && item.owner.name, item.bvid);
      }
    }
    return map;
  }

  /**
   * 判断视频数据是否命中“分类标签”黑名单(与卡片屏蔽逻辑一致)。
   * @param {object} data - 一个视频的 view 接口数据。
   * @returns {boolean}
   */
  function isVideoTagNameBlacklisted(data) {
    const checkTname = (tname) => {
      if (!tname) return false;
      if (tagNameBlacklist.includes(tname)) return true;
      const mapped = getTagNameByV2(tname); // 若该名字是 V2 名,映射回主名再判断
      if (mapped !== null && tagNameBlacklist.includes(mapped)) return true;
      return false;
    };
    if (checkTname(data.tname)) return true;
    if (checkTname(data.tname_v2)) return true;
    if (data.tid_v2 !== undefined && data.tid_v2 !== null) {
      const obj = getTagNameById(data.tid_v2);
      if (obj) {
        if (checkTname(obj.name)) return true;
        if (obj.name_v2 && checkTname(obj.name_v2)) return true;
      }
    }
    return false;
  }

  /**
   * 判断视频是否为竖屏(与卡片屏蔽逻辑一致)。
   * @param {object} data - 一个视频的 view 接口数据。
   * @returns {boolean}
   */
  function isVerticalVideo(data) {
    if (data.dimension && data.dimension.width && data.dimension.height) {
      const dimension = data.dimension.width / data.dimension.height;
      return dimension < globalPluginConfig.verticalScaleThreshold;
    }
    return false;
  }

  /**
   * 只依据 B 站 view 接口数据判断某 BV 是否“按分类标签 / 竖屏”被屏蔽(不依赖 DOM 是否已渲染标签组)。
   * @param {string} bvid - 视频 BV。
   * @returns {Promise<boolean>}
   */
  async function isBlockedByTagOrVertical(bvid) {
    const cfg = globalPluginConfig;
    if (!cfg.flagTName && !cfg.flagVertical) return false;
    if (!bvid) return false;
    const data = await getBilibiliVideoApiData(bvid);
    if (!data) return false;
    if (cfg.flagTName && isVideoTagNameBlacklisted(data)) return true;
    if (cfg.flagVertical && isVerticalVideo(data)) return true;
    return false;
  }

  /**
   * 判断“当前播放视频”是否被屏蔽,规则与卡片屏蔽完全一致:
   *   flagInfo(UP名/标题)、flagTName(分类标签)、flagVertical(竖屏)。
   * 权威来源是 __INITIAL_STATE__ 的 videoData/related(带 owner.name):
   *   用标题反查 UP 名最靠谱(自动连播卡片只有标题、没有 UP 名),再按 bvid 反查,最后 view 接口兜底。
   * @param {{upName:string,title:string}} info - DOM 读取的信息。
   * @param {string} bv - 当前 BV。
   * @returns {Promise<boolean>}
   */
  async function isPlayingVideoBlacklisted(info, bv) {
    const cfg = globalPluginConfig;
    let upName = info.upName;
    let title = info.title;
    let bvid = bv;
    let resolved = false;

    // 1) __INITIAL_STATE__ 按标题反查 UP 名 + bvid(自动连播卡片只有标题)
    if (title) {
      const byTitle = buildTitleInfoMapFromInitialState()[title];
      if (byTitle && byTitle.upName) {
        upName = byTitle.upName;
        if (byTitle.bvid) bvid = byTitle.bvid;
        resolved = true;
      }
    }
    // 2) __INITIAL_STATE__ 按 bvid 反查
    if (!resolved && bvid) {
      const byBv = buildBvInfoMapFromInitialState()[bvid];
      if (byBv && byBv.upName) {
        upName = upName || byBv.upName;
        title = title || byBv.title;
        resolved = true;
      }
    }

    // 3) 先用反查到的权威 UP名/标题做 UP名屏蔽(避免依赖已过期的 view 接口)
    if (cfg.flagInfo && upName && isBlacklisted(upName, title)) {
      return true;
    }

    // 4) 仅当开启了“标签/竖屏屏蔽”时,才拿 view 接口数据(含 tname/dimension)
    if (cfg.flagTName || cfg.flagVertical) {
      const data = bvid ? await getBilibiliVideoApiData(bvid) : null;
      if (data) {
        // 若还没拿到 UP名,用接口数据补一次
        if (cfg.flagInfo && !upName) {
          const dUpName = (data.owner && data.owner.name) || "";
          if (dUpName && isBlacklisted(dUpName, data.title)) return true;
        }
        if (cfg.flagTName && isVideoTagNameBlacklisted(data)) return true;
        if (cfg.flagVertical && isVerticalVideo(data)) return true;
      }
    }

    return false;
  }


  /**
   * 暂停当前播放。优先 <video>,其次 window.player。
   */
  function pauseCurrentPlayback() {
    const video = document.querySelector(
      "#bilibili-player video, .bilibili-player video, video"
    );
    if (video && !video.paused) {
      try {
        video.pause();
        return;
      } catch (e) {
        // 忽略,继续尝试 player
      }
    }
    if (window.player && typeof window.player.pause === "function") {
      try {
        window.player.pause();
      } catch (e) {
        // 忽略
      }
    }
  }

  /**
   * 取消自动连播:点击 B 站播放器结局面板里的“取消连播”按钮;若按钮不可见则暂停兜底。
   * 当相关推荐全部被屏蔽时调用,比“暂停”更贴近 B 站原生语义。
   */
  function cancelAutoplay() {
    try {
      const btns = document.querySelectorAll(
        ".bpx-player-ending-related-item-cancel"
      );
      for (const btn of btns) {
        // 只点可见的取消连播按钮
        if (btn.getBoundingClientRect().height > 0) {
          btn.click();
          console.log("[bilibili-blacklist] 相关推荐全部被屏蔽,已取消自动连播。");
          return;
        }
      }
    } catch (e) {
      // 忽略,走下方暂停兜底
    }
    pauseCurrentPlayback();
    console.log("[bilibili-blacklist] 相关推荐全部被屏蔽,已停止自动连播。");
  }

  /**
   * 尝试让播放器不刷新地切换到指定 BV(best-effort,方法名不稳定需运行时确认)。
   * @param {string} bvid
   * @returns {boolean} 是否成功触发切换。
   */
  function tryInPageSwitch(bvid) {
    const player = window.player;
    if (!player) return false;

    const trySwitch = (method, arg) => {
      if (typeof player[method] !== "function") return false;
      const ret = player[method](arg);
      return ret !== false;
    };

    const attempts = [
      () => trySwitch("changeVideo", { bvid }),
      () => trySwitch("switchVideo", { bvid }),
      () => trySwitch("loadVideo", { bvid }),
      () => trySwitch("changeVideo", bvid),
      () => trySwitch("switchVideo", bvid),
    ];
    for (const attempt of attempts) {
      try {
        if (attempt()) {
          console.log(
            `[bilibili-blacklist] 自动连播已切换到未屏蔽视频: ${bvid}`
          );
          return true;
        }
      } catch (e) {
        // 该形态不适用,继续尝试下一种
      }
    }
    return false;
  }

  /**
   * 找到页面上指向指定 BV 的链接并点击它(不限右侧列表,覆盖相关推荐/接下来播放等)。
   * @param {string} bvid
   * @returns {boolean}
   */
  function clickRecommendCardByBv(bvid) {
    try {
      const links = document.querySelectorAll("a[href]");
      for (const link of links) {
        const href = link.getAttribute("href") || "";
        const m = href.match(/\/video\/(BV\w+)/);
        if (m && m[1] === bvid) {
          link.click();
          console.log(
            `[bilibili-blacklist] 自动连播已点击未屏蔽推荐卡片: ${bvid}`
          );
          return true;
        }
      }
      return false;
    } catch (e) {
      return false;
    }
  }

  /**
   * 从当前页面“相关推荐 / 接下来播放”卡片里找第一条未被屏蔽的视频 BV(DOM 优先,最贴近屏幕内容)。
   * 标签/竖屏用 B 站 view 接口(getBilibiliVideoApiData)精确判定,不依赖 DOM 是否已渲染标签组。
   * @returns {Promise<string|null>}
   */
  async function getFirstNonBlockedFromDom() {
    const cfg = globalPluginConfig;
    const cards = document.querySelectorAll(
      ".video-page-card-small, .bili-video-card"
    );
    for (const card of cards) {
      // 跳过已被插件整体屏蔽(隐藏 / 卡比遮挡)的卡片
      try {
        const real = getRealVideoCardElement(card);
        if (blockedVideoCards.has(real)) continue;
        if (real && real.style.display === "none") continue;
        if (real && real.querySelector("#bilibili-blacklist-kirby")) continue;
      } catch (e) {
        // 忽略,继续
      }
      const { upName, videoTitle } = getVideoCardInfo(card);
      if (!upName || !videoTitle) continue;
      // UP名/标题屏蔽
      if (cfg.flagInfo && isBlacklisted(upName, videoTitle)) continue;
      const bv = getLinkBvId(getCardHrefLink(card));
      if (!bv) continue;
      // 标签/竖屏屏蔽:用 view 接口精确判定
      if (await isBlockedByTagOrVertical(bv)) continue;
      return bv;
    }
    return null;
  }

  /**
   * 用 B 站相关推荐接口取第一条未被屏蔽的视频 BV(DOM 找不到时兜底)。
   * @param {string} curBv - 当前视频 BV。
   * @returns {Promise<string|null>}
   */
  async function getFirstNonBlockedFromApi(curBv) {
    if (!curBv) return null;
    try {
      const res = await fetch(
        `https://api.bilibili.com/x/web-interface/archive/related?bvid=${curBv}`
      );
      const json = await res.json();
      if (json.code !== 0 || !Array.isArray(json.data)) return null;
      for (const item of json.data) {
        if (!item.bvid || item.bvid === curBv) continue;
        const upName = (item.owner && item.owner.name) || "";
        const title = item.title || "";
        if (!upName) continue;
        // UP名/标题屏蔽
        if (globalPluginConfig.flagInfo && isBlacklisted(upName, title)) {
          continue;
        }
        // 标签/竖屏屏蔽:用 view 接口精确判定
        if (await isBlockedByTagOrVertical(item.bvid)) continue;
        return item.bvid;
      }
      return null;
    } catch (e) {
      console.error("[bilibili-blacklist] 获取相关推荐失败:", e);
      return null;
    }
  }

  /**
   * 读取 __INITIAL_STATE__.availableVideoList(B 站连播真正依据的有序“可播列表”)。
   * @returns {Array<{bvid:string,title:string}>}
   */
  function getAvailableVideoList() {
    const state =
      typeof unsafeWindow !== "undefined" ? unsafeWindow.__INITIAL_STATE__ : null;
    return state && Array.isArray(state.availableVideoList)
      ? state.availableVideoList
      : [];
  }

  /**
   * 从 availableVideoList 里找“当前视频之后”的第一条未屏蔽视频 BV。
   * 这是连播最权威的“下一个”顺序;当前视频为 index 0,其后紧跟 related 推荐。
   * @param {string} curBv - 当前视频 BV。
   * @param {Object<string,{upName:string,title:string}>} infoMap - bvid -> {upName,title}。
   * @returns {string|null}
   */
  async function getFirstNonBlockedFromAvailableList(curBv, infoMap) {
    const list = getAvailableVideoList();
    if (list.length === 0) return null;
    let start = -1;
    for (let i = 0; i < list.length; i++) {
      if (list[i].bvid === curBv) {
        start = i;
        break;
      }
    }
    // 当前视频不在列表里(可能已播到 SSR 列表之外),交给 DOM/API 兜底,避免误取已播视频
    if (start < 0) return null;
    for (let i = start + 1; i < list.length; i++) {
      const item = list[i];
      if (!item || !item.bvid) continue;
      const rel = infoMap[item.bvid] || {};
      if (!rel.upName) continue; // 拿不到 UP 名,跳过,靠后续 DOM/API 兜底
      if (globalPluginConfig.flagInfo && isBlacklisted(rel.upName, rel.title)) {
        continue;
      }
      // 标签/竖屏屏蔽:用 view 接口精确判定
      if (await isBlockedByTagOrVertical(item.bvid)) continue;
      return item.bvid;
    }
    return null;
  }

  /**
   * 取第一条未屏蔽的视频 BV。
   * 优先级:页面 DOM 卡片(实时“接下来播放/相关推荐”,随切集更新)-> availableVideoList ->
   * 相关推荐 API。
   * @param {string} curBv - 当前播放视频 BV。
   * @returns {Promise<string|null>}
   */
  async function getFirstNonBlockedBv(curBv) {
    // DOM 卡片是实时的,切集后会跟着更新;优先用它,避免用到页面加载时的旧缓存
    const fromDom = await getFirstNonBlockedFromDom();
    if (fromDom) return fromDom;
    const infoMap = buildBvInfoMapFromInitialState();
    const fromAvailableList = await getFirstNonBlockedFromAvailableList(
      curBv,
      infoMap
    );
    if (fromAvailableList) return fromAvailableList;
    return await getFirstNonBlockedFromApi(curBv);
  }

  /**
   * 处理“当前播放视频被屏蔽”的逻辑(按配置走三态)。
   * @param {{upName:string,title:string}} info - 当前播放视频信息。
   * @param {string} bv - 当前播放视频 BV。
   */
  async function handleBlockedVideo(info, bv) {
    const mode = globalPluginConfig.flagSkipBlockedAutoplay;
    if (mode === "off") return; // 不处理:保留 B 站默认行为

    const blocked = await isPlayingVideoBlacklisted(info, bv);
    if (!blocked) return;

    if (mode === "stop") {
      pauseCurrentPlayback();
      return;
    }

    // mode === "skip"
    const nextBv = await getFirstNonBlockedBv(bv);
    if (nextBv && nextBv !== bv) {
      lastHandledBv = nextBv; // 记录目标,避免反复处理
      if (tryInPageSwitch(nextBv)) {
        return;
      }
      if (clickRecommendCardByBv(nextBv)) {
        return;
      }
      location.href = `/video/${nextBv}`;
    } else if (!nextBv) {
      // 相关推荐全部被屏蔽:取消自动连播(点击“取消连播”按钮),按钮不可见则暂停兜底
      cancelAutoplay();
    }
  }

  /**
   * 初始化自动连播监听。
   * 每 700ms 读一次“当前播放视频”的信息(标题/UP名/BV),一旦发生变化(无论是切集还是
   * 加载了新视频),就检查其是否被屏蔽,并按配置选择“跳过/停止/不处理”。
   */
  function initAutoplaySkip() {
    if (autoplayWatchTimer) return; // 防止重复初始化

    const check = async () => {
      const bv = getCurrentBv();
      if (!bv) {
        // 不在视频播放页,重置基准
        lastSignature = "";
        return;
      }
      const info = getPlayingVideoInfo();
      const signature = `${info.upName}||${info.title}||${bv}`;
      if (signature === lastSignature) return; // 视频没变

      lastSignature = signature;

      if (isHandling) return;
      isHandling = true;
      try {
        await handleBlockedVideo(info, bv);
      } catch (e) {
        console.error("[bilibili-blacklist] 自动连播处理出错:", e);
      } finally {
        isHandling = false;
      }
    };

    autoplayWatchTimer = setInterval(check, 700);
    window.addEventListener("popstate", check);
    // 捕获阶段监听播放事件(video 事件不冒泡,用 capture 才能捕捉),
    // 切集开始播放新视频时会触发一次检测,比轮询更快、更稳。
    const onPlayback = () => check();
    ["playing", "loadstart", "loadedmetadata", "load", "emptied"].forEach(
      (evt) => document.addEventListener(evt, onPlayback, true)
    );
  }

  /*
   * Bilibili-BlackList -- Bilibili UP屏蔽插件
   * 脚本大部分代码由AI生成,作者一点都不懂JavaScript,出现bug请联系Gemini / ChatGPT / DeepSeek
   * this script is mainly generated by AI, the author doesn't know JavaScript at all, if there are bugs, please contact Gemini / ChatGPT / DeepSeek
   * 感谢你的使用
   * Thank you for using this script
   *
   * 本段注释为VS code 自动生成 this is a comment generated by VS code
   */

  // 启动脚本
  initializeScript();


})();