Greasy Fork

来自缓存

Greasy Fork is available in English.

Bilibili - 未登录自由看

v3.4:未登录无限试用最高画质 + 拦截miniLogin加载 + 保护顶部工具栏/搜索栏 + 真正可用的评论解锁

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bilibili - 未登录自由看
// @namespace    https://bilibili.com/
// @version      3.4
// @description  v3.4:未登录无限试用最高画质 + 拦截miniLogin加载 + 保护顶部工具栏/搜索栏 + 真正可用的评论解锁
// @license      GPL-3.0
// @author       zhikanyeye
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @match        https://www.bilibili.com/festival/*
// @icon         https://www.bilibili.com/favicon.ico
// @require      https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js
// @require      https://update.greasyfork.icu/scripts/512574/1464548/inject-bilibili-comment-style.js
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(async function () {
  'use strict';

  /* ========== 0. 公共配置 ========== */
  const CONFIG = {
    QUALITY_CHECK_INTERVAL: 1500,
    PLAYER_CHECK_INTERVAL: 300,
    QUALITY_SWITCH_DELAY: 5000,
    BUTTON_CLICK_DELAY: 800,
    TOAST_CHECK_INTERVAL: 100,
    CLICK_TIMEOUT: 800,
    AUTO_RESUME_INTERVAL: 1200,
    TRIAL_TIMEOUT: 3e8
  };

  const options = {
    preferQuality: GM_getValue('preferQuality', '1080'),
    isWaitUntilHighQualityLoaded: GM_getValue('isWaitUntilHighQualityLoaded', false),
    enableCommentUnlock: GM_getValue('enableCommentUnlock', true),
    enableReplyPagination: GM_getValue('enableReplyPagination', false)
  };

  /* ========== 工具函数 ========== */
  // 等待元素出现
  function waitForElement(selector, timeout = 10000) {
    return new Promise((resolve, reject) => {
      const element = document.querySelector(selector);
      if (element) return resolve(element);
      
      const observer = new MutationObserver((mutations, obs) => {
        const element = document.querySelector(selector);
        if (element) {
          obs.disconnect();
          resolve(element);
        }
      });
      
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
      
      setTimeout(() => {
        observer.disconnect();
        reject(new Error(`等待元素超时: ${selector}`));
      }, timeout);
    });
  }

  // 延迟函数
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 获取视频AID
  function getVideoOid() {
    try {
      const initialState = unsafeWindow.__INITIAL_STATE__;
      if (initialState?.aid) return initialState.aid;
      if (initialState?.videoData?.aid) return initialState.videoData.aid;
    } catch (e) {}
    
    const aidElement = document.querySelector('[data-aid]');
    if (aidElement) return aidElement.dataset.aid;
    
    return null;
  }

  /* ========== 评论模块 ========== */
  let commentOid, commentType, commentCreatorID;
  let commentCurrentSortType = 2;
  let commentIsLoading = false;
  let commentIsEnd = false;
  let commentNextOffset = '';
  let commentPageOffsets = [''];
  let commentCurrentPage = 0;
  const COMMENT_SORT = { LATEST: 0, HOT: 2 };

  async function getWbiQueryString(params) {
    const { img_url, sub_url } = await fetch('https://api.bilibili.com/x/web-interface/nav')
      .then(res => res.json())
      .then(json => json.data.wbi_img);
    const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.'));
    const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.'));
    const originKey = imgKey + subKey;
    const mixinKeyEncryptTable = [46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];
    const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join('').slice(0, 32);
    const query = Object.keys(params).sort().map(key => {
      const value = params[key].toString().replace(/[!'()*]/g, '');
      return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
    }).join('&');
    const wbiSign = SparkMD5.hash(query + mixinKey);
    return query + '&w_rid=' + wbiSign;
  }

  function b2a(bvid) {
    const XOR_CODE = 23442827791579n;
    const MASK_CODE = 2251799813685247n;
    const BASE = 58n;
    const ALPHABET = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf'.split('');
    const DIGIT_MAP = [0,1,2,9,7,5,6,4,8,3,10,11];
    const BV_LEN = 12;
    let r = 0n;
    for (let i = 3; i < BV_LEN; i++) {
      r = r * BASE + BigInt(ALPHABET.indexOf(bvid[DIGIT_MAP[i]]));
    }
    return `${r & MASK_CODE ^ XOR_CODE}`;
  }

  function commentFormatTime(ts) {
    const d = new Date(ts * 1000);
    const now = new Date();
    const diff = (now - d) / 1000;
    if (diff < 60) return '刚刚';
    if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
    if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
    if (diff < 86400 * 30) return `${Math.floor(diff / 86400)}天前`;
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  }

  function escapeHtml(str) {
    return String(str || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function renderReplyContent(reply) {
    let text = escapeHtml(reply.content?.message || '');
    if (reply.content?.emote) {
      Object.entries(reply.content.emote).forEach(([key, emote]) => {
        const esc = escapeHtml(key);
        text = text.replace(
          new RegExp(esc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
          `<img class="reply-emote" src="${escapeHtml(emote.url)}" alt="${esc}" />`
        );
      });
    }
    text = text.replace(/@([^\s,,::@\n]+)/g, '<a class="reply-mention" href="javascript:void(0)">@$1</a>');
    return text;
  }

  function appendCommentItem(replyData, isTop) {
    const list = document.getElementById('bili-comment-list');
    if (!list) return;

    const isVip = replyData.member?.vip?.vipStatus === 1;
    const isUp = Number(replyData.mid) === Number(commentCreatorID);
    const level = replyData.member?.level_info?.current_level || 0;
    const nameStyle = isVip ? ' style="color:#fb7299"' : '';
    const upBadge = isUp ? '<span class="reply-up-badge">UP主</span>' : '';
    const subReplies = (replyData.replies || []).slice(0, 3);
    const subCount = replyData.rcount || 0;

    let subHtml = '';
    if (subReplies.length > 0) {
      const subItems = subReplies.map(sub => {
        const sVip = sub.member?.vip?.vipStatus === 1;
        const sUp = Number(sub.mid) === Number(commentCreatorID);
        return `<div class="sub-reply-item">
          <a class="sub-reply-avatar" href="https://space.bilibili.com/${sub.mid}" target="_blank"><img src="${escapeHtml(sub.member?.avatar || '')}" alt="" loading="lazy" /></a>
          <div class="sub-reply-main">
            <a class="sub-reply-username" href="https://space.bilibili.com/${sub.mid}" target="_blank"${sVip ? ' style="color:#fb7299"' : ''}>${escapeHtml(sub.member?.uname || '')}</a>${sUp ? '<span class="reply-up-badge reply-up-badge-sm">UP主</span>' : ''}:<span class="sub-reply-text">${renderReplyContent(sub)}</span>
            <span class="sub-reply-time">${commentFormatTime(sub.ctime)}</span>
          </div>
        </div>`;
      }).join('');
      const moreBtn = subCount > 3
        ? `<div class="sub-reply-more" data-rpid="${replyData.rpid}" data-count="${subCount}">共 ${subCount} 条回复,点击查看全部 &gt;</div>`
        : '';
      subHtml = `<div class="sub-reply-list">${subItems}${moreBtn}</div>`;
    }

    const wrapper = document.createElement('div');
    wrapper.innerHTML = `<div class="reply-item${isTop ? ' reply-top' : ''}" data-rpid="${replyData.rpid}">
      <a class="reply-avatar" href="https://space.bilibili.com/${replyData.mid}" target="_blank"><img src="${escapeHtml(replyData.member?.avatar || '')}" alt="" loading="lazy" /></a>
      <div class="reply-main">
        <div class="reply-header">
          <a class="reply-username" href="https://space.bilibili.com/${replyData.mid}" target="_blank"${nameStyle}>${escapeHtml(replyData.member?.uname || '')}</a>${upBadge}<span class="reply-level lv-${level}">Lv.${level}</span>
        </div>
        <div class="reply-text">${renderReplyContent(replyData)}</div>
        <div class="reply-footer">
          <span class="reply-time">${commentFormatTime(replyData.ctime)}</span>
          <span class="reply-likes">👍 ${replyData.like || 0}</span>
        </div>
        ${subHtml}
      </div>
    </div>`;

    const item = wrapper.firstElementChild;
    const moreEl = item.querySelector('.sub-reply-more');
    if (moreEl) {
      moreEl.addEventListener('click', () => {
        loadSubReplies(replyData.rpid, item.querySelector('.sub-reply-list'), parseInt(moreEl.dataset.count), 1);
      });
    }
    list.appendChild(item);
  }

  async function loadSubReplies(rootReplyID, container, totalCount, pageNum) {
    if (!container) return;
    const loadEl = document.createElement('div');
    loadEl.className = 'sub-reply-loading';
    loadEl.textContent = '加载中...';
    container.appendChild(loadEl);
    try {
      const res = await fetch(
        `https://api.bilibili.com/x/v2/reply/reply?oid=${commentOid}&root=${rootReplyID}&pn=${pageNum}&ps=10&type=${commentType}`
      );
      const data = await res.json();
      loadEl.remove();
      if (data.code === 0 && data.data?.replies) {
        if (pageNum === 1) container.innerHTML = '';
        data.data.replies.forEach(sub => {
          const sVip = sub.member?.vip?.vipStatus === 1;
          const sUp = Number(sub.mid) === Number(commentCreatorID);
          const el = document.createElement('div');
          el.className = 'sub-reply-item';
          el.innerHTML = `
            <a class="sub-reply-avatar" href="https://space.bilibili.com/${sub.mid}" target="_blank"><img src="${escapeHtml(sub.member?.avatar || '')}" alt="" loading="lazy" /></a>
            <div class="sub-reply-main">
              <a class="sub-reply-username" href="https://space.bilibili.com/${sub.mid}" target="_blank"${sVip ? ' style="color:#fb7299"' : ''}>${escapeHtml(sub.member?.uname || '')}</a>${sUp ? '<span class="reply-up-badge reply-up-badge-sm">UP主</span>' : ''}:<span class="sub-reply-text">${renderReplyContent(sub)}</span>
              <span class="sub-reply-time">${commentFormatTime(sub.ctime)}</span>
            </div>`;
          container.appendChild(el);
        });
        const loaded = pageNum * 10;
        if (loaded < totalCount) {
          const nextBtn = document.createElement('div');
          nextBtn.className = 'sub-reply-more';
          nextBtn.textContent = `继续加载(还有 ${totalCount - loaded} 条)`;
          nextBtn.addEventListener('click', () => { nextBtn.remove(); loadSubReplies(rootReplyID, container, totalCount, pageNum + 1); });
          container.appendChild(nextBtn);
        }
      }
    } catch(e) {
      loadEl.remove();
      console.error('[评论模块] 子评论加载失败:', e);
    }
  }

  async function getCommentPaginationData(offset) {
    const mode = commentCurrentSortType === COMMENT_SORT.HOT ? 3 : 2;
    const paginationStr = JSON.stringify({ offset: offset || '' });
    const wts = Math.floor(Date.now() / 1000);
    const qs = await getWbiQueryString({ oid: commentOid, type: commentType, mode, pagination_str: paginationStr, wts });
    const res = await fetch(`https://api.bilibili.com/x/v2/reply/wbi/main?${qs}`);
    return res.json();
  }

  async function loadCommentPage(offset, appendToList) {
    if (commentIsLoading) return;
    commentIsLoading = true;
    const loader = document.getElementById('bili-comment-loader');
    if (loader) loader.style.display = 'block';
    try {
      const data = await getCommentPaginationData(offset);
      if (data.code !== 0) {
        console.error('[评论模块] API错误:', data.code, data.message);
        return;
      }
      const list = document.getElementById('bili-comment-list');
      if (!appendToList && list) list.innerHTML = '';
      if (!appendToList) {
        const topReply = data.data?.top?.upper;
        if (topReply) appendCommentItem(topReply, true);
      }
      (data.data?.replies || []).forEach(r => appendCommentItem(r, false));
      const nextOffset = data.data?.cursor?.pagination_reply?.next_offset || '';
      const isEnd = !nextOffset || !!data.data?.cursor?.is_end;
      commentIsEnd = isEnd;
      commentNextOffset = nextOffset;
      if (options.enableReplyPagination) {
        if (!isEnd) commentPageOffsets[commentCurrentPage + 1] = nextOffset;
        updatePaginationControls();
      } else {
        if (isEnd) {
          const endEl = document.getElementById('bili-comment-end');
          if (endEl) endEl.style.display = 'block';
        }
      }
    } catch(e) {
      console.error('[评论模块] 加载评论失败:', e);
    } finally {
      commentIsLoading = false;
      if (loader) loader.style.display = 'none';
    }
  }

  function updatePaginationControls() {
    const pageInfo = document.getElementById('bili-page-info');
    const prevBtn = document.getElementById('bili-prev-page');
    const nextBtn = document.getElementById('bili-next-page');
    if (pageInfo) pageInfo.textContent = `第 ${commentCurrentPage + 1} 页`;
    if (prevBtn) prevBtn.disabled = commentCurrentPage === 0;
    if (nextBtn) nextBtn.disabled = commentIsEnd;
  }

  async function initCommentModule() {
    if (!options.enableCommentUnlock) return;

    commentCurrentSortType = COMMENT_SORT.HOT;
    commentIsLoading = false;
    commentIsEnd = false;
    commentNextOffset = '';
    commentPageOffsets = [''];
    commentCurrentPage = 0;

    GM_addStyle(`
#bili-custom-comments{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB",sans-serif;font-size:14px;color:#222;padding:16px 0}
.bili-comment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e3e5e7}
.bili-comment-title{font-size:18px;font-weight:700;color:#18191c}
.bili-comment-sort{display:flex;gap:16px}
.sort-btn{cursor:pointer;color:#9499a0;font-size:14px;padding:4px 8px;border-radius:4px;transition:color .2s}
.sort-btn.active{color:#00aeec;font-weight:700}
.sort-btn:hover{color:#00aeec}
.reply-item{display:flex;gap:12px;padding:16px 0;border-bottom:1px solid #e3e5e7}
.reply-item.reply-top{background:#fef9f0;border-radius:8px;padding:16px;margin-bottom:8px;border-bottom:none}
.reply-avatar img{width:40px;height:40px;border-radius:50%;object-fit:cover;background:#e3e5e7}
.reply-main{flex:1;min-width:0}
.reply-header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}
.reply-username{color:#61666d;font-weight:600;text-decoration:none}
.reply-username:hover{color:#00aeec}
.reply-level{font-size:11px;padding:1px 4px;border-radius:3px;background:#e3e5e7;color:#9499a0}
.lv-3,.lv-4{background:#e8ffe8;color:#52c41a}
.lv-5,.lv-6{background:#fff7e6;color:#fa8c16}
.reply-up-badge{font-size:11px;padding:1px 4px;border-radius:3px;background:#fb7299;color:#fff}
.reply-up-badge-sm{font-size:10px;padding:0 3px}
.reply-text{color:#18191c;line-height:1.7;word-break:break-word;margin-bottom:8px}
.reply-emote{height:20px;vertical-align:middle}
.reply-mention{color:#00aeec;text-decoration:none}
.reply-footer{display:flex;align-items:center;gap:16px;color:#9499a0;font-size:12px}
.sub-reply-list{margin-top:10px;background:#f6f7f8;border-radius:8px;padding:8px 12px}
.sub-reply-item{display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #e3e5e7}
.sub-reply-item:last-child{border-bottom:none}
.sub-reply-avatar img{width:24px;height:24px;border-radius:50%;object-fit:cover;background:#e3e5e7}
.sub-reply-main{flex:1;min-width:0;font-size:13px;line-height:1.6}
.sub-reply-username{color:#61666d;font-weight:600;text-decoration:none;margin-right:2px}
.sub-reply-username:hover{color:#00aeec}
.sub-reply-text{color:#18191c;word-break:break-word}
.sub-reply-time{color:#9499a0;font-size:11px;margin-left:6px}
.sub-reply-more{cursor:pointer;color:#00aeec;font-size:13px;padding:8px 0;text-align:center}
.sub-reply-more:hover{opacity:.8}
.sub-reply-loading{color:#9499a0;font-size:13px;padding:6px 0;text-align:center}
#bili-comment-loader{text-align:center;padding:16px;color:#9499a0}
#bili-comment-end{text-align:center;padding:16px;color:#9499a0;font-size:13px}
#bili-scroll-anchor{height:1px}
#bili-comment-pagination{display:flex;justify-content:center;align-items:center;gap:16px;padding:16px 0}
#bili-comment-pagination button{padding:6px 16px;border:1px solid #ddd;border-radius:4px;cursor:pointer;background:#fff;color:#555;transition:all .2s}
#bili-comment-pagination button:hover:not(:disabled){border-color:#00aeec;color:#00aeec}
#bili-comment-pagination button:disabled{opacity:.5;cursor:not-allowed}
#bili-page-info{color:#555;font-size:14px}
`);

    let commentSection;
    try {
      commentSection = await waitForElement('bili-comments, .comment-container, #commentapp', 20000);
    } catch(e) {
      console.warn('[评论模块] 未找到评论容器:', e.message);
      return;
    }

    try {
      const state = unsafeWindow.__INITIAL_STATE__;
      commentOid = String(state?.aid || state?.videoData?.aid || '');
      if (!commentOid || commentOid === 'undefined') {
        const bvMatch = location.pathname.match(/BV[\w]+/i);
        if (bvMatch) commentOid = b2a(bvMatch[0]);
      }
      commentType = 1;
      commentCreatorID = state?.upData?.mid || state?.videoData?.owner?.mid || 0;
    } catch(e) {
      console.error('[评论模块] 获取视频信息失败:', e);
      return;
    }

    if (!commentOid) {
      console.warn('[评论模块] 无法获取视频AID');
      return;
    }

    console.log(`[评论模块] 初始化,oid=${commentOid}, type=${commentType}, creator=${commentCreatorID}`);

    const customEl = document.createElement('div');
    customEl.id = 'bili-custom-comments';
    customEl.innerHTML = `
      <div class="bili-comment-header">
        <span class="bili-comment-title">评论</span>
        <div class="bili-comment-sort">
          <span class="sort-btn active" data-sort="2">最热</span>
          <span class="sort-btn" data-sort="0">最新</span>
        </div>
      </div>
      <div id="bili-comment-list"></div>
      <div id="bili-comment-loader" style="display:none">加载中...</div>
      <div id="bili-comment-end" style="display:none">没有更多评论了</div>
      ${options.enableReplyPagination
        ? `<div id="bili-comment-pagination"><button id="bili-prev-page" disabled>上一页</button><span id="bili-page-info">第 1 页</span><button id="bili-next-page">下一页</button></div>`
        : `<div id="bili-scroll-anchor"></div>`}`;

    commentSection.parentNode.insertBefore(customEl, commentSection.nextSibling);
    commentSection.style.display = 'none';

    // 守护自定义评论容器,防止被官方组件重新挂载时覆盖
    const commentGuard = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        // 如果自定义容器被移出 DOM,重新插入
        if (!document.getElementById('bili-custom-comments')) {
          console.log('[评论模块] 检测到自定义评论容器被移除,重新插入...');
          const commentSectionNow = document.querySelector('bili-comments, .comment-container, #commentapp');
          if (commentSectionNow && commentSectionNow.parentNode) {
            // 使用 insertBefore 而不是 appendChild,保持原有位置关系
            commentSectionNow.parentNode.insertBefore(customEl, commentSectionNow.nextSibling);
            commentSectionNow.style.display = 'none';
          }
        }
        // 如果有新的 bili-comments 被插入,立刻隐藏它
        for (const node of mutation.addedNodes) {
          if (node.nodeType === 1 && (node.tagName === 'BILI-COMMENTS' || node.tagName === 'BILI-COMMENT-CONTAINER')) {
            node.style.display = 'none';
            console.log('[评论模块] 守护:隐藏新出现的 bili-comments');
          }
        }
      }
    });
    commentGuard.observe(document.body, { childList: true, subtree: true });

    customEl.querySelectorAll('.sort-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        const sort = parseInt(btn.dataset.sort);
        if (sort === commentCurrentSortType) return;
        commentCurrentSortType = sort;
        customEl.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        commentNextOffset = '';
        commentPageOffsets = [''];
        commentCurrentPage = 0;
        commentIsEnd = false;
        document.getElementById('bili-comment-end').style.display = 'none';
        await loadCommentPage('', false);
      });
    });

    if (options.enableReplyPagination) {
      document.getElementById('bili-next-page').addEventListener('click', async () => {
        if (commentIsEnd || commentIsLoading) return;
        commentCurrentPage++;
        await loadCommentPage(commentPageOffsets[commentCurrentPage] || '', false);
      });
      document.getElementById('bili-prev-page').addEventListener('click', async () => {
        if (commentCurrentPage <= 0 || commentIsLoading) return;
        commentCurrentPage--;
        commentIsEnd = false;
        await loadCommentPage(commentPageOffsets[commentCurrentPage] || '', false);
      });
      await loadCommentPage('', false);
    } else {
      const anchor = document.getElementById('bili-scroll-anchor');
      const scrollObserver = new IntersectionObserver(async () => {
        if (!commentIsLoading && !commentIsEnd) {
          await loadCommentPage(commentNextOffset, commentNextOffset !== '');
        }
      }, { rootMargin: '300px' });
      scrollObserver.observe(anchor);
    }

    /* ========== 监听视频切换,自动更新评论区 ========== */
    let lastOid = commentOid;
    setInterval(() => {
      let newOid;
      try {
        const state = unsafeWindow.__INITIAL_STATE__;
        if (state?.aid) newOid = String(state.aid);
        else if (state?.videoData?.aid) newOid = String(state.videoData.aid);
        else {
          const bvMatch = location.pathname.match(/BV[\w]+/i);
          if (bvMatch) newOid = b2a(bvMatch[0]);
        }
      } catch (e) {}
      
      if (newOid && newOid !== lastOid) {
        lastOid = newOid;
        commentOid = newOid;
        const state = unsafeWindow.__INITIAL_STATE__;
        commentCreatorID = state?.upData?.mid || state?.videoData?.owner?.mid || 0;
        
        console.log(`[评论模块] 检测到视频切换,新 oid=${newOid}, creator=${commentCreatorID}`);
        
        // 重置评论区状态
        commentCurrentSortType = COMMENT_SORT.HOT;
        document.querySelectorAll('#bili-custom-comments .sort-btn').forEach(b => b.classList.remove('active'));
        document.querySelector('#bili-custom-comments .sort-btn[data-sort="2"]')?.classList.add('active');
        commentNextOffset = '';
        commentPageOffsets = [''];
        commentCurrentPage = 0;
        commentIsEnd = false;
        
        // 清空评论列表
        const list = document.getElementById('bili-comment-list');
        if (list) list.innerHTML = '';
        const endEl = document.getElementById('bili-comment-end');
        if (endEl) endEl.style.display = 'none';
        
        // 重新加载评论
        loadCommentPage('', false);
      }
    }, 1500);
  }

  /* ========== 初始化评论模块(无论是否登录都执行) ========== */
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initCommentModule);
  } else {
    initCommentModule();
  }

  /* ========== 1. 如果已登录直接退出 ========== */
  if (document.cookie.includes('DedeUserID')) return;

  /* ========== 2. 阻止登录弹窗 / 自动暂停 ========== */

  /* 2-0 从源头拦截 miniLogin.js 加载(参考 DD1969 方案) */
  const originAppendChild = Node.prototype.appendChild;
  Node.prototype.appendChild = function (child) {
    if (child && child.tagName === 'SCRIPT' && child.src && child.src.includes('miniLogin')) {
      console.log('[Bilibili脚本] 已拦截 miniLogin.js 加载');
      return child;                   // 不执行 appendChild,返回原节点即可
    }
    return originAppendChild.call(this, child);
  };

  /* 2-1 登录遮罩 / 弹窗选择器 */
  // —— 全屏遮罩 / 登录弹窗:直接挂在 body 上,覆盖整个页面 ——
  const GLOBAL_LOGIN_SELECTOR = [
    '.bili-mini-mask',
    '.bili-mini-login',
    '.mini-login',
    '.mini-login-mask',
    '.bili-login-v2-mask',
    '.bili-login-v2-container'
  ].join(',');

  // —— 播放器内部登录提示 ——
  const PLAYER_LOGIN_SELECTOR = [
    '.passport-login-tip-container',
    '.login-tip'
  ].join(',');

  /* 2-2 CSS 保护顶部导航栏,确保工具栏/搜索栏始终可见可交互 */
  GM_addStyle(`
    /* 顶部导航区域始终保持最高层级 */
    .bili-header,
    #bili-header-container,
    .bili-header__bar,
    #biliMainHeader,
    .fixed-header {
      position: relative !important;
      z-index: 100001 !important;
      pointer-events: auto !important;
      visibility: visible !important;
      opacity: 1 !important;
    }
    /* 全局登录遮罩直接干掉 */
    body > .bili-mini-mask,
    body > .bili-mini-login,
    body > .mini-login,
    body > .mini-login-mask,
    body > .bili-login-v2-mask,
    body > .bili-login-v2-container {
      display: none !important;
      pointer-events: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
    }
  `);

  /* 2-3 DOM 级别隐藏:处理动态插入的登录弹窗 */
  const hideElement = (el) => {
    if (!el || el.nodeType !== 1) return;
    el.style.setProperty('display', 'none', 'important');
    el.style.setProperty('pointer-events', 'none', 'important');
    el.style.setProperty('opacity', '0', 'important');
    el.style.setProperty('visibility', 'hidden', 'important');
  };

  const hideLoginLayersInNode = (node) => {
    if (!node || node.nodeType !== 1) return;

    // 全局级登录遮罩 / 弹窗 —— 无论在哪都隐藏
    if (node.matches?.(GLOBAL_LOGIN_SELECTOR)) hideElement(node);
    node.querySelectorAll?.(GLOBAL_LOGIN_SELECTOR)?.forEach(hideElement);

    // 播放器内部登录提示 —— 仅隐藏播放器区域内的
    const isInPlayer = (el) => !!el.closest(
      '.bpx-player-container, .bpx-player-video-area, .bpx-player-video-wrap, #bilibili-player'
    );
    if (node.matches?.(PLAYER_LOGIN_SELECTOR) && isInPlayer(node)) hideElement(node);
    node.querySelectorAll?.(PLAYER_LOGIN_SELECTOR)?.forEach((el) => {
      if (isInPlayer(el)) hideElement(el);
    });
  };

  // 初始扫描
  if (document.documentElement) hideLoginLayersInNode(document.documentElement);

  const startLoginLayerGuard = () => {
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        mutation.addedNodes.forEach((node) => hideLoginLayersInNode(node));
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  };

  if (document.body) {
    startLoginLayerGuard();
  } else {
    document.addEventListener('DOMContentLoaded', startLoginLayerGuard, { once: true });
  }

  (function blockLoginAndAutoPause() {
    /* 2-1 等待播放器就绪后屏蔽 getMediaInfo 返回值 */
    const waitPlayer = () => new Promise((resolve, reject) => {
      const maxAttempts = 50; // 最多等待15秒
      let attempts = 0;
      const checkPlayer = setInterval(() => {
        if (unsafeWindow.player && unsafeWindow.player.getMediaInfo) {
          clearInterval(checkPlayer);
          resolve();
        } else if (++attempts >= maxAttempts) {
          clearInterval(checkPlayer);
          reject(new Error('Player initialization timeout'));
        }
      }, CONFIG.PLAYER_CHECK_INTERVAL);
    });

    waitPlayer().then(() => {
      const player = unsafeWindow.player;
      const originGet = player.getMediaInfo;
      player.getMediaInfo = function () {
        const info = originGet.call(this);
        return { absolutePlayTime: 0, relativePlayTime: info.relativePlayTime, playUrl: info.playUrl };
      };

      let lastTrustedActionTime = 0;
      let allowInternalPause = false;
      let currentMedia = null;
      let isUserPaused = false;

      const markTrustedAction = (event) => {
        if (event && event.isTrusted === false) return;
        lastTrustedActionTime = Date.now();
      };

      const canPauseNow = () => {
        return allowInternalPause || Date.now() - lastTrustedActionTime <= CONFIG.CLICK_TIMEOUT;
      };

      document.addEventListener('pointerdown', markTrustedAction, { passive: true, capture: true });
      document.addEventListener('keydown', (e) => {
        if (!e.isTrusted) return;
        if (e.code === 'Space' || e.code === 'KeyK' || e.key === ' ' || e.key === 'k' || e.key === 'K') {
          markTrustedAction(e);
        }
      }, { passive: true, capture: true });

      const originPause = typeof player.pause === 'function' ? player.pause.bind(player) : null;
      if (originPause) {
        player.pause = function () {
          if (!canPauseNow()) return;
          return originPause(...arguments);
        };
      }

      const bindMediaGuard = (media) => {
        if (!media || media.dataset.bfqPauseGuardBound) return;
        const originMediaPause = media.pause.bind(media);

        media.pause = function () {
          if (!canPauseNow()) return;
          return originMediaPause();
        };

        media.addEventListener('pause', () => {
          if (allowInternalPause) return;

          if (Date.now() - lastTrustedActionTime <= CONFIG.CLICK_TIMEOUT) {
            isUserPaused = true;
            return;
          }

          isUserPaused = false;
          Promise.resolve().then(() => media.play()).catch(() => {});
        }, true);

        media.addEventListener('play', () => {
          isUserPaused = false;
        }, true);

        media.dataset.bfqPauseGuardBound = '1';
      };

      const trackCurrentMedia = () => {
        try {
          const media = unsafeWindow.player?.mediaElement?.();
          if (media && media !== currentMedia) {
            currentMedia = media;
            bindMediaGuard(media);
          }
        } catch (e) {}
      };

      trackCurrentMedia();
      setInterval(trackCurrentMedia, 500);

      if (player && typeof player.mediaElement === 'function') {
        const originMediaElementGetter = player.mediaElement.bind(player);
        player.mediaElement = function () {
          const media = originMediaElementGetter();
          bindMediaGuard(media);
          currentMedia = media || currentMedia;
          return media;
        };
      }

      setInterval(() => {
        const media = currentMedia;
        if (!media || media.ended || document.hidden || allowInternalPause || isUserPaused) return;
        if (media.paused) {
          media.play().catch(() => {});
        }
      }, CONFIG.AUTO_RESUME_INTERVAL);

      unsafeWindow.__BFQ_ALLOW_INTERNAL_PAUSE__ = () => {
        allowInternalPause = true;
        setTimeout(() => {
          allowInternalPause = false;
        }, CONFIG.CLICK_TIMEOUT);
      };
    }).catch(err => {
      console.warn('[Bilibili脚本] 播放器初始化失败:', err);
    });
  })();

  /* ========== 3. 无限试用核心 ========== */
  /* 3-1 放行试用标识 */
  const originDef = Object.defineProperty;
  Object.defineProperty = function (obj, prop, desc) {
    if (prop === 'isViewToday' || prop === 'isVideoAble') {
      desc = { get: () => true, enumerable: false, configurable: true };
    }
    return originDef.call(this, obj, prop, desc);
  };

  /* 3-2 把试用倒计时延长到 3 亿秒 */
  const originSetTimeout = unsafeWindow.setTimeout;
  const originSetInterval = unsafeWindow.setInterval;
  const shouldExtendTrialTimer = (fn, delayNum) => {
    if (delayNum === 30000) return true;
    if (delayNum !== 60000 && delayNum !== 62000 && delayNum !== 90000) return false;
    const fnText = typeof fn === 'function' ? String(fn) : String(fn || '');
    return (
      fnText.includes('miniLogin') ||
      fnText.includes('试看') ||
      fnText.includes('trial') ||
      fnText.includes('isViewToday') ||
      fnText.includes('isVideoAble') ||
      fnText.includes('absolutePlayTime')
    );
  };

  unsafeWindow.setTimeout = (fn, delay) => {
    const delayNum = Number(delay);
    if (shouldExtendTrialTimer(fn, delayNum)) {
      delay = CONFIG.TRIAL_TIMEOUT;
    }
    return originSetTimeout.call(unsafeWindow, fn, delay);
  };
  unsafeWindow.setInterval = (fn, delay) => {
    const delayNum = Number(delay);
    if (shouldExtendTrialTimer(fn, delayNum)) {
      delay = CONFIG.TRIAL_TIMEOUT;
    }
    return originSetInterval.call(unsafeWindow, fn, delay);
  };

  /* 3-3 自动点击试用按钮 + 画质切换 */
  const QUALITY_MAP = { 1080: 80, 720: 64, 480: 32, 360: 16 };
  
  // 使用 MutationObserver 而不是 setInterval 来监听按钮出现,性能更好
  const observeTrialButton = () => {
    const observer = new MutationObserver((mutations) => {
      const btn = document.querySelector('.bpx-player-toast-confirm-login');
      if (!btn) return;
      
      // 防抖:避免重复点击
      if (btn.dataset.clicked) return;
      btn.dataset.clicked = 'true';
      
      setTimeout(() => {
        btn.click();
        
        /* 可选:暂停→切画质→继续播放 */
        if (options.isWaitUntilHighQualityLoaded && unsafeWindow.player?.mediaElement) {
          const media = unsafeWindow.player.mediaElement();
          const wasPlaying = !media.paused;
          if (wasPlaying) {
            unsafeWindow.__BFQ_ALLOW_INTERNAL_PAUSE__?.();
            media.pause();
          }

          const checkToast = setInterval(() => {
            const toastTexts = document.querySelectorAll('.bpx-player-toast-text');
            if ([...toastTexts].some(el => el.textContent.endsWith('试用中'))) {
              if (wasPlaying) media.play().catch(err => console.warn('[Bilibili脚本] 播放失败:', err));
              clearInterval(checkToast);
            }
          }, CONFIG.TOAST_CHECK_INTERVAL);
          
          // 超时保护:最多等待10秒
          setTimeout(() => clearInterval(checkToast), 10000);
        }

        /* 画质切换 */
        const target = QUALITY_MAP[options.preferQuality] || 80;
        setTimeout(() => {
          try {
            if (unsafeWindow.player?.getSupportedQualityList?.()?.includes(target)) {
              unsafeWindow.player.requestQuality(target);
            }
          } catch (err) {
            console.warn('[Bilibili脚本] 画质切换失败:', err);
          }
        }, CONFIG.QUALITY_SWITCH_DELAY);
        
        // 重置点击标记
        setTimeout(() => delete btn.dataset.clicked, 2000);
      }, CONFIG.BUTTON_CLICK_DELAY);
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  };
  
  // 等待 DOM 加载完成后启动观察器
  if (document.body) {
    observeTrialButton();
  } else {
    document.addEventListener('DOMContentLoaded', observeTrialButton);
  }

  /* ========== 4. 设置面板 ========== */
  GM_addStyle(`
#qp-panel{position:fixed;inset:0;z-index:999999;display:none;place-items:center;background:rgba(0,0,0,.6);backdrop-filter:blur(2px)}
.qp-wrapper{width:90%;max-width:420px;padding:20px;background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.3);display:flex;flex-direction:column;gap:16px;font-size:14px;font-family:sans-serif}
.qp-title{margin:0 0 8px;font-size:22px;font-weight:600;color:#333;border-bottom:2px solid #00aeec;padding-bottom:8px}
.qp-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0}
.qp-label{color:#555;font-weight:500}
select{padding:6px 10px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer;transition:border-color .2s}
select:hover{border-color:#00aeec}
.switch{cursor:pointer;display:inline-block;width:44px;height:24px;background:#ccc;border-radius:12px;position:relative;transition:background .3s}
.switch[data-status='on']{background:#00aeec}
.switch:after{content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:left .3s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
.switch[data-status='on']:after{left:23px}
.qp-close-btn{padding:10px;background:#00aeec;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;transition:background .2s}
.qp-close-btn:hover{background:#0098d1}
.qp-section-divider{height:1px;background:#e0e0e0;margin:8px 0}
`);

  const panel = document.createElement('div');
  panel.id = 'qp-panel';
  panel.innerHTML = `
    <div class="qp-wrapper">
      <div class="qp-title">🎬 画质设置</div>
      <div class="qp-row">
        <span class="qp-label">偏好分辨率</span>
        <select data-key="preferQuality">
          <option value="1080" ${options.preferQuality === '1080' ? 'selected' : ''}>1080p 高清</option>
          <option value="720" ${options.preferQuality === '720' ? 'selected' : ''}>720p 清晰</option>
          <option value="480" ${options.preferQuality === '480' ? 'selected' : ''}>480p 流畅</option>
          <option value="360" ${options.preferQuality === '360' ? 'selected' : ''}>360p 省流</option>
        </select>
      </div>
      <div class="qp-row">
        <span class="qp-label">切换时暂停播放</span>
        <span class="switch" data-key="isWaitUntilHighQualityLoaded" data-status="${options.isWaitUntilHighQualityLoaded ? 'on' : 'off'}"></span>
      </div>
      <div class="qp-section-divider"></div>
      <div class="qp-title">💬 评论设置</div>
      <div class="qp-row">
        <span class="qp-label">解锁全部评论</span>
        <span class="switch" data-key="enableCommentUnlock" data-status="${options.enableCommentUnlock ? 'on' : 'off'}"></span>
      </div>
      <div class="qp-row">
        <span class="qp-label">分页加载评论</span>
        <span class="switch" data-key="enableReplyPagination" data-status="${options.enableReplyPagination ? 'on' : 'off'}"></span>
      </div>
      <button class="qp-close-btn" onclick="this.parentElement.parentElement.style.display='none'">✓ 保存并关闭</button>
    </div>`;
  
  // 等待 body 加载完成再添加面板
  const addPanel = () => {
    if (document.body) {
      document.body.appendChild(panel);
    } else {
      document.addEventListener('DOMContentLoaded', () => document.body.appendChild(panel));
    }
  };
  addPanel();

  /* 注册 GM 菜单 & 播放器入口 */
  GM_registerMenuCommand('🎬 画质设置', () => (panel.style.display = 'flex'));
  
  // 使用 MutationObserver 而不是 setInterval 来添加设置入口
  let entryAdded = false;
  const addSettingsEntry = () => {
    if (entryAdded) return;
    
    const others = document.querySelector('.bpx-player-ctrl-setting-others-content');
    if (!others) return;
    
    const entry = document.createElement('div');
    entry.textContent = '🎬 脚本设置 >';
    entry.style.cssText = 'cursor:pointer;height:20px;line-height:20px;padding:4px 8px;transition:background .2s';
    entry.onmouseenter = () => { entry.style.background = 'rgba(0,174,236,0.1)'; };
    entry.onmouseleave = () => { entry.style.background = ''; };
    entry.onclick = () => { panel.style.display = 'flex'; };
    others.appendChild(entry);
    entryAdded = true;
  };
  
  // 监听设置面板的出现
  const settingsObserver = new MutationObserver(() => {
    if (!entryAdded) addSettingsEntry();
  });
  
  const startObserving = () => {
    const settingsPanel = document.querySelector('.bpx-player-ctrl-setting');
    if (settingsPanel) {
      settingsObserver.observe(settingsPanel, { childList: true, subtree: true });
    }
  };
  
  if (document.body) {
    startObserving();
  } else {
    document.addEventListener('DOMContentLoaded', startObserving);
  }

  /* 事件绑定:即时存储 */
  panel.querySelectorAll('[data-key]').forEach(el => {
    if (el.tagName === 'SELECT') {
      el.onchange = e => {
        const value = e.target.value;
        options.preferQuality = value;
        GM_setValue(el.dataset.key, value);
      };
    } else {
      el.onclick = () => {
        const newStatus = el.dataset.status === 'on' ? 'off' : 'on';
        el.dataset.status = newStatus;
        const isOn = newStatus === 'on';
        const key = el.dataset.key;
        
        // 更新对应的选项
        if (key === 'isWaitUntilHighQualityLoaded') {
          options.isWaitUntilHighQualityLoaded = isOn;
        } else if (key === 'enableCommentUnlock') {
          options.enableCommentUnlock = isOn;
        } else if (key === 'enableReplyPagination') {
          options.enableReplyPagination = isOn;
        }
        
        GM_setValue(key, isOn);
      };
    }
  });
  
  // 支持 ESC 键关闭面板
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && panel.style.display === 'flex') {
      panel.style.display = 'none';
    }
  });
  
  // 点击背景关闭面板
  panel.addEventListener('click', (e) => {
    if (e.target === panel) {
      panel.style.display = 'none';
    }
  });
})();