// ==UserScript== // @name B站投币检测器:我要为ta投币! // @namespace https://space.bilibili.com/496259679 // @version 1.0.1 // @description 该脚本可以在任一B站UP主的所有投稿页面 (https://space.bilibili.com/*/upload/video) 检测并显示投币数量。由DeepSeek协助编写,经多次测试修复明显问题。作者只是想要给一个喜欢的up主的所有视频都投币,但是总有遗漏的,于是有了这个脚本...欢迎批评指正! // @author 凌绝顶览山小 // @tag 哔哩哔哩 bilibili 投币 检测 // @license MIT // @match https://space.bilibili.com/* // @grant GM_addStyle // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ========== 全局状态 ========== let autoDetectEnabled = false; let isDetecting = false; let abortDetection = false; let detectTimeouts = []; let pageCheckInterval = null; let lastPage = null; let pendingPage = null; let stabilizationTimer = null; let pageChangeCount = 0; let widgetBuilt = false; // 标记是否已构建widget // ========== UI 样式 ========== GM_addStyle(` #coin-detector-widget { position: fixed; bottom: 120px; right: 20px; z-index: 999999; font-family: -apple-system, "Helvetica Neue", sans-serif; user-select: none; } #coin-detector-toggle { width: 48px; height: 48px; border-radius: 50%; background: #fb7299; color: #fff; font-size: 20px; font-weight: bold; text-align: center; line-height: 48px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: transform 0.2s; border: none; outline: none; } #coin-detector-toggle:hover { transform: scale(1.05); } #coin-detector-panel { display: none; position: absolute; bottom: 60px; right: 0; width: 200px; background: rgba(30, 30, 40, 0.95); backdrop-filter: blur(8px); border-radius: 12px; padding: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); color: #eee; font-size: 14px; border: 1px solid rgba(255,255,255,0.1); } #coin-detector-panel.open { display: block; } #coin-detector-panel .panel-title { font-weight: bold; margin-bottom: 12px; text-align: center; color: #fff; border-bottom: 1px solid #444; padding-bottom: 8px; } #coin-detector-panel .control-group { display: flex; flex-direction: column; gap: 12px; } #coin-detector-panel .btn { padding: 8px 12px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background 0.2s; font-weight: bold; } #coin-detector-panel .btn-primary { background: #00a1d6; color: #fff; } #coin-detector-panel .btn-primary:hover { background: #0088b0; } #coin-detector-panel .btn-danger { background: #fb7299; color: #fff; } #coin-detector-panel .btn-danger:hover { background: #e05a7e; } #coin-detector-panel .toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; } #coin-detector-panel .toggle-label { color: #ccc; } .switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .3s; border-radius: 24px; } .slider::before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; } .switch input:checked + .slider { background-color: #00a1d6; } .switch input:checked + .slider::before { transform: translateX(20px); } #detect-status { margin-top: 10px; font-size: 12px; color: #aaa; text-align: center; min-height: 18px; border-top: 1px solid #444; padding-top: 8px; } .custom-coin-badge { position: absolute; top: 8px; right: 8px; padding: 2px 12px; border-radius: 30px; color: #fff; font-size: 13px; font-weight: bold; z-index: 9999; pointer-events: none; box-shadow: 0 2px 8px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.2); } `); // ========== 工具函数 ========== function updateStatus(msg) { const statusEl = document.getElementById('detect-status'); if (statusEl) statusEl.textContent = msg; } function getCurrentPage() { const activeBtn = document.querySelector('.vui_pagenation--btn.vui_button--active'); if (activeBtn) { const text = activeBtn.textContent.trim(); const num = parseInt(text); if (!isNaN(num) && num > 0) return num; } const params = new URLSearchParams(location.search); const p = parseInt(params.get('p')); return !isNaN(p) && p > 0 ? p : 1; } function getCurrentCards() { return document.querySelectorAll('div.upload-video-card'); } function clearDetectTimeouts() { detectTimeouts.forEach(id => clearTimeout(id)); detectTimeouts = []; } function resetCardDetectFlags() { document.querySelectorAll('div.upload-video-card').forEach(card => { delete card.dataset.coinDetected; }); } function setDetectButtonState(detecting) { const btn = document.getElementById('detect-now-btn'); if (!btn) return; if (detecting) { btn.textContent = '⏹ 停止检测'; btn.className = 'btn btn-danger'; } else { btn.textContent = '⚡ 立刻检测'; btn.className = 'btn btn-primary'; } } // ========== 核心检测 ========== function detectVideos(cardList, forceReset = false) { if (isDetecting) return; if (forceReset) { document.querySelectorAll('.custom-coin-badge').forEach(el => el.remove()); resetCardDetectFlags(); } const cardsToDetect = []; cardList.forEach(card => { if (!card.dataset.coinDetected) { const link = card.querySelector('a[href*="/video/BV"]'); if (link) { const match = link.href.match(/\/video\/(BV\w+)/); if (match) { cardsToDetect.push({ bvid: match[1], card }); } } } }); if (cardsToDetect.length === 0) { updateStatus('当前页无未检测视频'); return; } isDetecting = true; abortDetection = false; setDetectButtonState(true); const total = cardsToDetect.length; let processed = 0; let errorCount = 0; updateStatus(`检测中 0/${total}`); const timeouts = []; cardsToDetect.forEach((item, index) => { const timeoutId = setTimeout(() => { if (abortDetection) return; const { bvid, card } = item; fetch(`https://api.bilibili.com/x/web-interface/archive/coins?bvid=${bvid}`, { credentials: 'include' }) .then(res => res.json()) .then(coinData => { if (abortDetection) return; processed++; let coinCount = 0; if (coinData.code === 0 && coinData.data) { if (typeof coinData.data === 'number') { coinCount = coinData.data; } else if (typeof coinData.data === 'object') { coinCount = coinData.data.multiply || coinData.data.total || coinData.data.coin || 0; } } const oldBadge = card.querySelector('.custom-coin-badge'); if (oldBadge) oldBadge.remove(); const badge = document.createElement('div'); badge.className = 'custom-coin-badge'; badge.style.backgroundColor = coinCount > 0 ? '#00a1d6' : '#fb7299'; badge.textContent = coinCount > 0 ? `已投${coinCount}币` : '未投'; if (getComputedStyle(card).position === 'static') { card.style.position = 'relative'; } card.appendChild(badge); card.dataset.coinDetected = 'true'; updateStatus(`检测中 ${processed}/${total}`); }) .catch(() => { if (abortDetection) return; processed++; errorCount++; updateStatus(`检测中 ${processed}/${total} (部分失败)`); }) .finally(() => { if (processed === total) { if (abortDetection) return; isDetecting = false; setDetectButtonState(false); if (errorCount > 0) { updateStatus(`完成 (${errorCount}个失败)`); } else { updateStatus(`完成,共检测 ${total} 个视频`); } } }); }, index * 500); timeouts.push(timeoutId); }); detectTimeouts = timeouts; } function stopDetection() { if (isDetecting) { abortDetection = true; clearDetectTimeouts(); isDetecting = false; setDetectButtonState(false); updateStatus('检测已停止'); } } // ========== 翻页处理(带稳定等待) ========== function handlePageChange(newPage) { if (stabilizationTimer) { clearTimeout(stabilizationTimer); stabilizationTimer = null; } pendingPage = newPage; pageChangeCount++; if (isDetecting) { stopDetection(); } resetCardDetectFlags(); updateStatus(`已翻页至第${newPage}页,等待页面稳定...`); let retryCount = 0; const maxRetries = 15; function checkStability() { const cards = getCurrentCards(); if (cards.length > 0 && Array.from(cards).some(c => c.querySelector('a[href*="/video/BV"]'))) { setTimeout(() => { const cards2 = getCurrentCards(); if (cards2.length === cards.length) { if (autoDetectEnabled) { document.querySelectorAll('.custom-coin-badge').forEach(el => el.remove()); resetCardDetectFlags(); detectVideos(cards2, false); updateStatus(`开始检测第${newPage}页`); } else { updateStatus(`第${newPage}页已稳定,但自动检测未开启`); } stabilizationTimer = null; pendingPage = null; return; } stabilizationTimer = setTimeout(checkStability, 500); }, 300); } else { retryCount++; if (retryCount > maxRetries) { updateStatus(`等待超时,请手动刷新页面`); stabilizationTimer = null; pendingPage = null; return; } stabilizationTimer = setTimeout(checkStability, 500); } } stabilizationTimer = setTimeout(checkStability, 500); } function checkPageChange() { const currentPage = getCurrentPage(); if (lastPage === null) { lastPage = currentPage; return; } if (currentPage !== lastPage) { lastPage = currentPage; handlePageChange(currentPage); } } // ========== 自动检测控制 ========== function startAutoDetect() { if (pageCheckInterval) clearInterval(pageCheckInterval); pageCheckInterval = setInterval(checkPageChange, 1000); lastPage = getCurrentPage(); const cards = getCurrentCards(); detectVideos(cards, false); // 不强制清除 } function stopAutoDetect() { if (pageCheckInterval) { clearInterval(pageCheckInterval); pageCheckInterval = null; } if (stabilizationTimer) { clearTimeout(stabilizationTimer); stabilizationTimer = null; } if (isDetecting) { stopDetection(); } updateStatus('自动检测已关闭'); } // ========== 构建悬浮窗 ========== function buildWidget() { if (document.getElementById('coin-detector-widget')) return; // 已存在则跳过 widgetBuilt = true; const widget = document.createElement('div'); widget.id = 'coin-detector-widget'; const toggleBtn = document.createElement('div'); toggleBtn.id = 'coin-detector-toggle'; toggleBtn.textContent = '币'; toggleBtn.title = '点击展开/收起投币检测'; widget.appendChild(toggleBtn); const panel = document.createElement('div'); panel.id = 'coin-detector-panel'; panel.innerHTML = `