B站bili直播间挂机全自动升级粉丝团灯牌等级

分批打开粉丝团灯牌直播间自动挂机升等级,带设置面板、进度显示、倒计时自动关闭(可暂停)、自动点赞300次

// ==UserScript==
// @name         B站bili直播间挂机全自动升级粉丝团灯牌等级
// @namespace    http://tampermonkey.net/
// @version      7.1
// @description  分批打开粉丝团灯牌直播间自动挂机升等级,带设置面板、进度显示、倒计时自动关闭(可暂停)、自动点赞300次
// @match        *://*.bilibili.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.live.bilibili.com
// @license      AGPL License
// ==/UserScript==

(function() {
  'use strict';

  // ===== 工具:配置(GM 存储,跨域共享) =====
  const defaultConfig = {
    BATCH_SIZE: 5,
    BATCH_INTERVAL_MIN: 30,
    AUTO_CLOSE_MIN: 30,
    AUTO_LIKE: true
  };
  function getConfig() {
    return Object.assign({}, defaultConfig, JSON.parse(GM_getValue('medalOpenerConfig', '{}')));
  }
  function saveConfig(cfg) {
    GM_setValue('medalOpenerConfig', JSON.stringify(cfg));
  }

  const isLiveRoom = location.hostname.includes('live.bilibili.com');

  // ===== 直播间页面逻辑 =====
  if (isLiveRoom) {
    // 单次注入保护,避免 SPA 或重复执行导致多份定时器
    if (window.__medalOpener_live_injected) return;
    window.__medalOpener_live_injected = true;

    const cfg = getConfig();

    // ---------- 自动点赞 300 次(可开关) ----------
    if (cfg.AUTO_LIKE) {
      (async function autoLike300() {
        try {
          const roomIdMatch = location.href.match(/live\.bilibili\.com\/(\d+)/);
          if (!roomIdMatch) return;
          const roomId = roomIdMatch[1];

          const infoRes = await fetch(
            `https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByUser?room_id=${roomId}`,
            { credentials: 'include' }
          );
          const infoJson = await infoRes.json();
          if (infoJson.code !== 0) throw new Error(infoJson.message);
          const medalInfo = infoJson.data.medal.curr_weared;
          if (!medalInfo) {
            console.warn('没有佩戴该直播间的粉丝勋章,跳过自动点赞');
            return;
          }

          const csrfMatch = document.cookie.match(/bili_jct=([0-9a-fA-F]{32})/);
          const csrf = csrfMatch ? csrfMatch[1] : '';
          const uidMatch = document.cookie.match(/DedeUserID=(\d+)/);
          const uid = uidMatch ? uidMatch[1] : '';

          const body = new URLSearchParams({
            click_time: '300',
            room_id: roomId,
            anchor_id: medalInfo.target_id,
            uid: uid,
            csrf: csrf
          });

          const likeRes = await fetch(
            'https://api.live.bilibili.com/xlive/app-ucenter/v1/like_info_v3/like/likeReportV3',
            {
              method: 'POST',
              credentials: 'include',
              headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
              body
            }
          );
          const likeJson = await likeRes.json();
          if (likeJson.code === 0) {
            console.log(`已自动为直播间 ${roomId} 点赞 300 次`);
          } else {
            console.warn(`点赞失败:${likeJson.message}`);
          }
        } catch (err) {
          console.error('自动点赞出错:', err);
        }
      })();
    }

    // ---------- 倒计时 + 暂停/恢复按钮 ----------
    // UI
    const Z = 2147483647; // 最大 z-index,确保可点
    const countdownDiv = document.createElement('div');
    countdownDiv.style.cssText = `
      position:fixed;top:60px;right:20px;z-index:${Z};
      padding:8px 12px;background:rgba(0,0,0,0.7);
      color:#fff;font-size:14px;border-radius:4px;
      pointer-events:auto;
    `;
    document.body.appendChild(countdownDiv);

    const toggleBtn = document.createElement('button');
    toggleBtn.textContent = '保持直播间打开(暂停自动关闭)';
    toggleBtn.style.cssText = `
      position:fixed;top:100px;right:20px;z-index:${Z};
      padding:10px;background:#f25d8e;color:#fff;
      border:none;cursor:pointer;border-radius:4px;
      pointer-events:auto;
    `;
    document.body.appendChild(toggleBtn);

    // 状态与逻辑
    let remainingSec = Math.max(1, Number(cfg.AUTO_CLOSE_MIN) || 30) * 60;
    let intervalId = null;
    let paused = false;

    function formatTime(sec) {
      if (!Number.isFinite(sec) || sec < 0) sec = 0;
      const m = Math.floor(sec / 60);
      const s = sec % 60;
      return `${m}分${s.toString().padStart(2, '0')}秒`;
    }

    function renderCountdown() {
      if (paused) {
        countdownDiv.textContent = `自动关闭:已暂停`;
      } else {
        countdownDiv.textContent = `自动关闭倒计时:${formatTime(remainingSec)}`;
      }
    }

    function stopCountdown() {
      if (intervalId) {
        clearInterval(intervalId);
        intervalId = null;
      }
    }

    function startCountdown() {
      stopCountdown();
      paused = false;
      renderCountdown();
      intervalId = setInterval(() => {
        if (paused) return;
        remainingSec -= 1;
        renderCountdown();
        if (remainingSec <= 0) {
          stopCountdown();
          // 仅当页面是由脚本打开时,window.close() 才能成功
          try { window.close(); } catch (e) { console.warn('window.close() 可能被浏览器拦截'); }
        }
      }, 1000);
    }

    function pauseAutoClose() {
      paused = true;
      stopCountdown();
      renderCountdown();
      toggleBtn.textContent = '恢复自动关闭';
      toggleBtn.style.background = '#00a1d6';
    }

    function resumeAutoClose() {
      const freshCfg = getConfig(); // 恢复时读取最新配置
      remainingSec = Math.max(1, Number(freshCfg.AUTO_CLOSE_MIN) || 30) * 60;
      startCountdown();
      toggleBtn.textContent = '保持直播间打开(暂停自动关闭)';
      toggleBtn.style.background = '#f25d8e';
    }

    // 初始启动倒计时
    startCountdown();

    // 点击切换暂停/恢复
    toggleBtn.addEventListener('click', () => {
      if (paused) {
        resumeAutoClose();
      } else {
        pauseAutoClose();
      }
    });

    return; // live 页面逻辑到此结束
  }

  // ===== 非直播间页面(主站)逻辑 =====
  function getMedalList(page = 1, pageSize = 50) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: `https://api.live.bilibili.com/xlive/app-ucenter/v1/fansMedal/panel?page=${page}&page_size=${pageSize}`,
        headers: { 'Cookie': document.cookie },
        onload: res => {
          try {
            const json = JSON.parse(res.responseText);
            if (json.code === 0) {
              resolve([
                ...(json.data.list || []),
                ...(json.data.special_list || [])
              ]);
            } else {
              reject(json.message);
            }
          } catch (e) { reject(e); }
        },
        onerror: reject
      });
    });
  }

  function openLiveRoom(roomId) {
    // 提示:window.open 可能被拦截,需按钮触发
    window.open(`https://live.bilibili.com/${roomId}`, '_blank');
  }

  // ----- 设置面板 -----
  const Z = 2147483647;
  const settingsBtn = document.createElement('button');
  settingsBtn.textContent = '⚙ 设置';
  settingsBtn.style.cssText = `
    position:fixed;top:220px;right:20px;z-index:${Z};
    padding:6px 10px;background:#666;color:#fff;
    border:none;cursor:pointer;border-radius:4px;
  `;
  document.body.appendChild(settingsBtn);

  const settingsPanel = document.createElement('div');
  settingsPanel.style.cssText = `
    position:fixed;top:250px;right:20px;z-index:${Z};
    background:#fff;padding:10px;border:1px solid #ccc;
    display:none;width:240px;border-radius:6px;
    box-shadow:0 6px 18px rgba(0,0,0,0.15);
  `;
  const cfgNow = getConfig();
  settingsPanel.innerHTML = `
    <div style="font-weight:bold;margin-bottom:6px;">批量打开设置</div>
    <label style="display:block;margin:6px 0;">每批数量:
      <input type="number" id="batchSize" value="${cfgNow.BATCH_SIZE}" min="1" style="width:90px;">
    </label>
    <label style="display:block;margin:6px 0;">间隔(分钟):
      <input type="number" id="batchInterval" value="${cfgNow.BATCH_INTERVAL_MIN}" min="1" style="width:90px;">
    </label>
    <label style="display:block;margin:6px 0;">关闭时间(分钟):
      <input type="number" id="autoClose" value="${cfgNow.AUTO_CLOSE_MIN}" min="1" style="width:90px;">
    </label>
    <label style="display:block;margin:6px 0;">
      <input type="checkbox" id="autoLike" ${cfgNow.AUTO_LIKE ? 'checked' : ''}> 进入直播间自动点赞300次
    </label>
    <button id="saveSettings" style="margin-top:8px;padding:6px 10px;background:#00a1d6;color:#fff;border:none;border-radius:4px;cursor:pointer;">保存</button>
  `;
  document.body.appendChild(settingsPanel);

  settingsBtn.addEventListener('click', () => {
    settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
  });

  settingsPanel.querySelector('#saveSettings').addEventListener('click', () => {
    const newCfg = {
      BATCH_SIZE: parseInt(settingsPanel.querySelector('#batchSize').value),
      BATCH_INTERVAL_MIN: parseInt(settingsPanel.querySelector('#batchInterval').value),
      AUTO_CLOSE_MIN: parseInt(settingsPanel.querySelector('#autoClose').value),
      AUTO_LIKE: settingsPanel.querySelector('#autoLike').checked
    };
    // 合法性兜底
    if (!Number.isFinite(newCfg.BATCH_SIZE) || newCfg.BATCH_SIZE < 1) newCfg.BATCH_SIZE = defaultConfig.BATCH_SIZE;
    if (!Number.isFinite(newCfg.BATCH_INTERVAL_MIN) || newCfg.BATCH_INTERVAL_MIN < 1) newCfg.BATCH_INTERVAL_MIN = defaultConfig.BATCH_INTERVAL_MIN;
    if (!Number.isFinite(newCfg.AUTO_CLOSE_MIN) || newCfg.AUTO_CLOSE_MIN < 1) newCfg.AUTO_CLOSE_MIN = defaultConfig.AUTO_CLOSE_MIN;

    saveConfig(newCfg);
    alert('设置已保存');
    settingsPanel.style.display = 'none';
  });

  // ----- 进度显示 -----
  const progressDiv = document.createElement('div');
  progressDiv.style.cssText = `
    position:fixed;top:140px;right:20px;z-index:${Z};
    padding:8px 10px;background:#00a1d6;color:#fff;
    border-radius:4px;
  `;
  progressDiv.textContent = '进度:未开始';
  document.body.appendChild(progressDiv);

  // ----- 执行按钮 -----
  const startBtn = document.createElement('button');
  startBtn.textContent = '批量挂机直播';
  startBtn.style.cssText = `
    position:fixed;top:180px;right:20px;z-index:${Z};
    padding:10px;background:#00a1d6;color:#fff;
    border:none;cursor:pointer;border-radius:4px;
  `;
  document.body.appendChild(startBtn);

  startBtn.addEventListener('click', async () => {
    try {
      const cfg = getConfig(); // 实时读取
      const medals = await getMedalList();
      if (!medals.length) {
        progressDiv.textContent = '进度:无可用勋章';
        return;
      }
      let index = 0;
      const totalBatches = Math.ceil(medals.length / cfg.BATCH_SIZE);

      async function openBatch() {
        const batch = medals.slice(index, index + cfg.BATCH_SIZE);
        batch.forEach(medal => {
          if (medal?.room_info?.room_id) {
            openLiveRoom(medal.room_info.room_id);
          }
        });
        index += cfg.BATCH_SIZE;
        const currentBatch = Math.min(totalBatches, Math.ceil(index / cfg.BATCH_SIZE));
        const percent = Math.min(100, Math.round((currentBatch / totalBatches) * 100));
        progressDiv.textContent = `进度:${currentBatch}/${totalBatches} 批 (${percent}%)`;

        if (index < medals.length) {
          setTimeout(openBatch, cfg.BATCH_INTERVAL_MIN * 60 * 1000);
        }
      }

      openBatch();
    } catch (err) {
      console.error('获取勋章列表失败:', err);
      progressDiv.textContent = '进度:获取勋章失败';
    }
  });

})();