您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在B站空间页面便捷地管理您的黑名单,支持分页浏览、一键移除,扫描不活跃用户(3个月未更新、已封禁、已注销)。
// ==UserScript== // @name B站黑名单便捷管理 // @namespace http://tampermonkey.net/ // @version 0.0.1 // @description 在B站空间页面便捷地管理您的黑名单,支持分页浏览、一键移除,扫描不活跃用户(3个月未更新、已封禁、已注销)。 // @author yingming006 // @match https://space.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect api.bilibili.com // @connect app.biliapi.com // @license GPL-3.0 License // ==/UserScript== (function() { 'use strict'; // --- 全局变量 --- let currentPage = 1, pageSize = 10, totalUsers = 0, totalPages = 1, csrfToken = '', isFetchingVideo = {}, intersectionObserver = null; let isScanning = false; // --- CSS样式 --- GM_addStyle(` /* Bilibili Style Color Palette & Variables */ :root { --bili-blue: #00aeec; --bili-blue-hover: #00b5e5; --bili-red: #f44336; --bili-red-hover: #e53935; --bili-orange: #ff9800; --bili-orange-hover: #fb8c00; --bili-text-main: #18191c; --bili-text-light: #61666d; --bili-text-lighter: #9499a0; --bili-bg-light: #fff; --bili-border-color: #e3e5e7; --bili-bg-hover: #f1f2f3; --bili-bg-disabled: #e3e5e7; --bili-text-disabled: #b8c0cc; } /* Modal Base */ #blacklist-modal, #inactive-scan-modal { position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); display: none; justify-content: center; align-items: center; } #blacklist-modal-content, #inactive-scan-modal-content { background-color: var(--bili-bg-light); padding: 24px; width: clamp(500px, 60vw, 800px); max-height: 85vh; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; } /* Modal Header */ #blacklist-modal-header, #inactive-scan-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--bili-border-color); padding-bottom: 16px; margin-bottom: 16px; } #blacklist-modal-header h2, #inactive-scan-modal-header h2 { font-size: 18px; color: var(--bili-text-main); font-weight: 500; } #blacklist-modal-close, #inactive-scan-modal-close { color: var(--bili-text-lighter); font-size: 24px; font-weight: bold; cursor: pointer; transition: color 0.2s; } #blacklist-modal-close:hover, #inactive-scan-modal-close:hover { color: var(--bili-text-light); } /* List Container & Items */ #blacklist-list-container, #inactive-list-container { flex-grow: 1; overflow-y: auto; margin-right: -8px; padding-right: 8px; } .blacklist-item { display: flex; align-items: center; gap: 15px; padding: 12px 8px; border-bottom: 1px solid var(--bili-border-color); transition: background-color 0.2s; } .blacklist-item:last-child { border-bottom: none; } .blacklist-item:hover { background-color: var(--bili-bg-hover); } .blacklist-item img { width: 48px; height: 48px; border-radius: 50%; flex-shrink: 0; } /* User Info */ .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } .info a { text-decoration: none; color: var(--bili-blue); font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; } .info a:hover { color: var(--bili-blue-hover); } .info span { color: var(--bili-text-lighter); font-size: 12px; display: block; } /* Video/Status Info */ .latest-video-info { display: flex; flex-direction: column; justify-content: center; flex: 2; min-width: 0; font-size: 13px; color: var(--bili-text-light); line-height: 1.5; } .latest-video-info a { color: var(--bili-text-light); text-decoration: none; } .latest-video-info a:hover { color: var(--bili-blue); } .latest-video-info .video-title { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; } .user-status { font-weight: bold; text-align: left; font-size: 13px; } .status-banned { color: var(--bili-orange); } .status-cancelled { color: var(--bili-text-lighter); } /* General Button Style */ .btn-remove, #blacklist-pagination button, #stopScanBtn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; color: white; transition: background-color 0.2s; flex-shrink: 0; } .btn-remove:disabled, #blacklist-pagination button:disabled, #stopScanBtn:disabled { background-color: var(--bili-bg-disabled) !important; color: var(--bili-text-disabled); cursor: not-allowed; } .btn-remove { background-color: var(--bili-red); } .btn-remove:hover:not(:disabled) { background-color: var(--bili-red-hover); } /* Pagination & Scan Button */ #blacklist-pagination { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--bili-border-color); text-align: center; display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; color: var(--bili-text-light); } #blacklist-pagination button { background-color: var(--bili-blue); } #blacklist-pagination button:hover:not(:disabled) { background-color: var(--bili-blue-hover); } #scanInactiveBtn { background-color: var(--bili-orange); margin-left: auto; } #scanInactiveBtn:hover:not(:disabled) { background-color: var(--bili-orange-hover); } #stopScanBtn { background-color: var(--bili-red); } #stopScanBtn:hover:not(:disabled) { background-color: var(--bili-red-hover); } #pageJumpInput { width: 50px; text-align: center; border: 1px solid var(--bili-border-color); border-radius: 4px; padding: 5px; font-size: 14px; background-color: var(--bili-bg-light); transition: border-color 0.2s, box-shadow 0.2s; } #pageJumpInput:focus { border-color: var(--bili-blue); outline: none; box-shadow: 0 0 0 2px rgba(0, 174, 236, 0.2); } /* Utility */ .spinner { width: 18px; height: 18px; border: 2px solid var(--bili-bg-hover); border-top: 2px solid var(--bili-blue); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .toast-notification { position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 6px; color: white; font-size: 14px; z-index: 10000; opacity: 0; transition: opacity 0.3s, top 0.3s; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .toast-notification.show { top: 40px; opacity: 1; } .toast-notification.success { background-color: #4CAF50; } .toast-notification.error { background-color: #f44336; } #scan-status { padding: 10px; text-align: center; font-style: italic; color: var(--bili-text-light); } #blacklist-message { padding: 40px; text-align: center; color: var(--bili-text-light); } `); // --- 辅助函数 --- function getCsrfToken() { const cookies = document.cookie.split(';'); for (let c of cookies) { c = c.trim(); if (c.startsWith('bili_jct=')) return c.substring('bili_jct='.length); } return ''; } function showToast(message, type = 'success') { const t = document.createElement('div'); t.className = `toast-notification ${type}`; t.textContent = message; document.body.appendChild(t); setTimeout(() => t.classList.add('show'), 10); setTimeout(() => { t.classList.remove('show'); setTimeout(() => document.body.removeChild(t), 300); }, 3000); } function formatTimestamp(unixTimestamp) { if (!unixTimestamp) return '未知时间'; return new Date(unixTimestamp * 1000).toLocaleDateString(); } // --- API请求 --- async function fetchBlacklist(page = 1) { showLoadingMessage('正在加载黑名单...'); const apiUrl = `https://api.bilibili.com/x/relation/blacks?pn=${page}&ps=${pageSize}`; try { const response = await fetch(apiUrl, { credentials: 'include' }); const data = await response.json(); if (data.code === 0) { totalUsers = data.data.total; totalPages = Math.ceil(totalUsers / pageSize) || 1; currentPage = page; renderBlacklist(data.data.list); renderPagination(); } else { throw new Error(`API错误: ${data.message}`); } } catch (error) { console.error('查询黑名单失败:', error); showLoadingMessage(`查询失败: ${error.message}`); } } async function removeFromBlacklist(mid, uname, onSuccess) { showToast(`正在移除【${uname}】...`, 'success'); const apiUrl = 'https://api.bilibili.com/x/relation/modify'; const formData = new URLSearchParams({ fid: mid, act: 6, re_src: 11, csrf: csrfToken }); try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData, credentials: 'include' }); const data = await response.json(); if (data.code === 0) { showToast(`用户【${uname}】已成功移出黑名单!`, 'success'); if (onSuccess) onSuccess(); } else { throw new Error(`API错误: ${data.message}`); } } catch (error) { console.error('移除失败:', error); showToast(`移除失败: ${error.message}`, 'error'); } } function fetchLatestVideo(mid, container) { if (isFetchingVideo[mid]) return; isFetchingVideo[mid] = true; fetchLatestVideoPromise(mid).then(result => { if (result && result.video) { const video = result.video; const pubDate = new Date(video.ctime * 1000).toLocaleDateString(); container.innerHTML = `<div><a class="video-title" href="//www.bilibili.com/video/${video.bvid}" target="_blank" title="${video.title}">${video.title}</a><span style="color: var(--bili-text-lighter);">发布于: ${pubDate}</span></div>`; } else { container.textContent = result.message; } }).finally(() => { delete isFetchingVideo[mid]; }); } function fetchLatestVideoPromise(mid) { return new Promise((resolve) => { const params = new URLSearchParams({ vmid: mid, ps: 1, order: 'pubdate', build: '8430300', mobi_app: 'android', platform: 'android', qn: 80 }); GM_xmlhttpRequest({ method: "GET", url: `https://app.biliapi.com/x/v2/space/archive/cursor?${params.toString()}`, headers: { 'User-Agent': 'Mozilla/5.0' }, anonymous: true, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0 && data.data.item && data.data.item.length > 0) resolve({ video: data.data.item[0], message: '成功' }); else resolve({ video: null, message: '未找到或隐藏了动态' }); } catch (e) { resolve({ video: null, message: '数据解析失败' }); } }, onerror: function() { resolve({ video: null, message: '获取失败' }); } }); }); } // --- 渲染函数 --- function renderBlacklist(users) { const container = document.getElementById('blacklist-list-container'); container.innerHTML = ''; if (intersectionObserver) intersectionObserver.disconnect(); if (!users || users.length === 0) { container.innerHTML = '<div id="blacklist-message">黑名单中没有用户。</div>'; return; } intersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const target = entry.target; const mid = target.dataset.mid; fetchLatestVideo(mid, target); intersectionObserver.unobserve(target); } }); }, { root: container }); users.forEach(user => { const faceUrl = user.face.startsWith('//') ? 'https:' + user.face : user.face; const userSpaceUrl = `https://space.bilibili.com/${user.mid}`; const item = document.createElement('div'); item.className = 'blacklist-item'; const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`; const isCancelled = user.uname === '账号已注销'; let statusOrVideoHTML = isBanned ? `<div class="latest-video-info"><span class="user-status status-banned">状态: 已封禁</span></div>` : isCancelled ? `<div class="latest-video-info"><span class="user-status status-cancelled">状态: 已注销</span></div>` : `<div class="latest-video-info" data-mid="${user.mid}"><div class="spinner"></div></div>`; const addTime = formatTimestamp(user.mtime); const userInfoHTML = ` <div class="info"> <a href="${userSpaceUrl}" target="_blank" title="${user.uname}">${user.uname}</a> <span>MID: ${user.mid}</span> <span>拉黑于: ${addTime}</span> </div>`; item.innerHTML = `<a href="${userSpaceUrl}" target="_blank"><img src="${faceUrl.replace('http:', 'https:')}" alt="${user.uname}的头像"></a>${userInfoHTML}${statusOrVideoHTML}<button class="btn-remove">移除</button>`; container.appendChild(item); const removeBtn = item.querySelector('.btn-remove'); removeBtn.onclick = () => { removeFromBlacklist(user.mid, user.uname, () => { removeBtn.textContent = '已移除'; removeBtn.disabled = true; }); }; if (!isBanned && !isCancelled) intersectionObserver.observe(item.querySelector('.latest-video-info')); }); } function renderPagination() { const container = document.getElementById('blacklist-pagination'); if (totalUsers === 0) { container.innerHTML = ''; return; } container.innerHTML = `<button id="scanInactiveBtn">扫描不活跃用户</button><div><button id="prevPageBtn">上一页</button><span style="margin: 0 8px;">第</span><input type="number" id="pageJumpInput" value="${currentPage}" min="1" max="${totalPages}"><span> / ${totalPages} 页 (共 ${totalUsers} 人)</span><button id="jumpPageBtn" style="margin-left:8px;">跳转</button><button id="nextPageBtn" style="margin-left:8px;">下一页</button></div>`; document.getElementById('prevPageBtn').disabled = currentPage <= 1; document.getElementById('nextPageBtn').disabled = currentPage >= totalPages; document.getElementById('prevPageBtn').onclick = () => fetchBlacklist(currentPage - 1); document.getElementById('nextPageBtn').onclick = () => fetchBlacklist(currentPage + 1); document.getElementById('scanInactiveBtn').onclick = scanForInactiveUsers; const jumpInput = document.getElementById('pageJumpInput'); const jumpBtn = document.getElementById('jumpPageBtn'); const jumpToPage = () => { let pageNum = parseInt(jumpInput.value, 10); if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) { fetchBlacklist(pageNum); } else { showToast(`请输入 1 到 ${totalPages} 之间的有效页码!`, 'error'); jumpInput.value = currentPage; } }; jumpBtn.onclick = jumpToPage; jumpInput.onkeydown = (event) => { if (event.key === 'Enter') { event.preventDefault(); jumpToPage(); } }; } function showLoadingMessage(msg) { document.getElementById('blacklist-list-container').innerHTML = `<div id="blacklist-message">${msg}</div>`; document.getElementById('blacklist-pagination').innerHTML = ''; } // --- 不活跃用户扫描功能 --- async function scanForInactiveUsers() { if (isScanning) { showToast('扫描已在进行中...', 'error'); return; } isScanning = true; const scanModal = document.getElementById('inactive-scan-modal'); const scanContainer = document.getElementById('inactive-list-container'); const scanStatus = document.getElementById('scan-status'); const stopScanBtn = document.getElementById('stopScanBtn'); scanModal.style.display = 'flex'; scanContainer.innerHTML = ''; stopScanBtn.style.display = 'inline-block'; stopScanBtn.disabled = false; stopScanBtn.textContent = '中止扫描'; let inactiveUsersFoundCount = 0; let scanPage = 1; const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); while (scanPage <= totalPages && isScanning) { scanStatus.textContent = `正在扫描第 ${scanPage}/${totalPages} 页... 已找到 ${inactiveUsersFoundCount} 位不活跃用户...`; try { const response = await fetch(`https://api.bilibili.com/x/relation/blacks?pn=${scanPage}&ps=${pageSize}`, { credentials: 'include' }); const data = await response.json(); if (data.code !== 0) throw new Error(data.message); const usersToCheckVideo = []; for (const user of data.data.list) { if (!isScanning) break; const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`; const isCancelled = user.uname === '账号已注销'; if (isBanned || isCancelled) { inactiveUsersFoundCount++; renderInactiveUser(user, null); } else { usersToCheckVideo.push(user); } } if (isScanning && usersToCheckVideo.length > 0) { const videoPromises = usersToCheckVideo.map(user => fetchLatestVideoPromise(user.mid).then(result => ({ user, result }))); const results = await Promise.all(videoPromises); for (const { user, result } of results) { if (!isScanning) break; if (result.video) { const videoDate = new Date(result.video.ctime * 1000); if (videoDate < threeMonthsAgo) { inactiveUsersFoundCount++; renderInactiveUser(user, result.video); } } else { // 如果用户没有视频动态,也视为不活跃 inactiveUsersFoundCount++; renderInactiveUser(user, null, '未找到动态'); } } } scanPage++; } catch (error) { scanStatus.textContent = `扫描出错: ${error.message}。已停止。`; isScanning = false; } } if (!isScanning && scanPage <= totalPages) { scanStatus.textContent = `扫描已中止。共扫描 ${scanPage - 1} 页,找到 ${inactiveUsersFoundCount} 位不活跃用户。`; } else if (inactiveUsersFoundCount === 0) { scanStatus.textContent = `扫描完成。在 ${totalPages} 页中未找到不活跃用户。`; } else { scanStatus.textContent = `扫描完成!共找到 ${inactiveUsersFoundCount} 位不活跃用户。`; } isScanning = false; stopScanBtn.style.display = 'none'; } function renderInactiveUser(user, video, message = '') { const container = document.getElementById('inactive-list-container'); const item = document.createElement('div'); item.className = 'blacklist-item'; item.id = `inactive-user-${user.mid}`; const faceUrl = user.face.startsWith('//') ? 'https:' + user.face : user.face; const userSpaceUrl = `https://space.bilibili.com/${user.mid}`; let statusOrVideoHTML = ''; if (video) { const pubDate = new Date(video.ctime * 1000).toLocaleDateString(); statusOrVideoHTML = `<div class="latest-video-info"><div><a class="video-title" href="//www.bilibili.com/video/${video.bvid}" target="_blank" title="${video.title}">${video.title}</a><span style="color: var(--bili-text-lighter);">最后更新: ${pubDate} (超过3个月)</span></div></div>`; } else { const isBanned = user.uname.startsWith('bili_') && user.uname === `bili_${user.mid}`; if (isBanned) { statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-banned">状态: 已封禁</span></div>`; } else if(user.uname === '账号已注销') { statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-cancelled">状态: 已注销</span></div>`; } else { statusOrVideoHTML = `<div class="latest-video-info"><span class="user-status status-cancelled">状态: ${message || '无动态'}</span></div>`; } } const addTime = formatTimestamp(user.mtime); const userInfoHTML = ` <div class="info"> <a href="${userSpaceUrl}" target="_blank" title="${user.uname}">${user.uname}</a> <span>MID: ${user.mid}</span> <span>拉黑于: ${addTime}</span> </div>`; item.innerHTML = `<a href="${userSpaceUrl}" target="_blank"><img src="${faceUrl.replace('http:', 'https:')}" alt="${user.uname}的头像"></a>${userInfoHTML}${statusOrVideoHTML}<button class="btn-remove">移除</button>`; container.appendChild(item); const removeBtn = item.querySelector('.btn-remove'); removeBtn.onclick = () => { removeFromBlacklist(user.mid, user.uname, () => { removeBtn.textContent = '已移除'; removeBtn.disabled = true; }); }; } // --- 初始化 --- function init() { const modal = document.createElement('div'); modal.id = 'blacklist-modal'; modal.innerHTML = `<div id="blacklist-modal-content"><div id="blacklist-modal-header"><h2>黑名单管理</h2><span id="blacklist-modal-close">×</span></div><div id="blacklist-list-container"></div><div id="blacklist-pagination"></div></div>`; document.body.appendChild(modal); const scanModal = document.createElement('div'); scanModal.id = 'inactive-scan-modal'; scanModal.innerHTML = ` <div id="inactive-scan-modal-content"> <div id="inactive-scan-modal-header"> <h2>不活跃用户扫描结果</h2> <div> <button id="stopScanBtn" style="display:none;">中止扫描</button> <span id="inactive-scan-modal-close" style="margin-left: 15px;">×</span> </div> </div> <div id="scan-status">点击主面板的扫描按钮开始...</div> <div id="inactive-list-container"></div> </div>`; document.body.appendChild(scanModal); const floatBtn = document.createElement('button'); floatBtn.textContent = '管理黑名单'; Object.assign(floatBtn.style, { position: 'fixed', bottom: '120px', left: '30px', zIndex: '9998', padding: '10px 15px', backgroundColor: 'var(--bili-blue)', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', boxShadow: '0 2px 10px rgba(0,0,0,0.2)', fontSize: '14px', transition: 'background-color 0.2s' }); floatBtn.onmouseover = () => floatBtn.style.backgroundColor = 'var(--bili-blue-hover)'; floatBtn.onmouseout = () => floatBtn.style.backgroundColor = 'var(--bili-blue)'; document.body.appendChild(floatBtn); floatBtn.onclick = () => { csrfToken = getCsrfToken(); if (!csrfToken) { showToast('获取登录信息失败,请确保您已登录B站!', 'error'); return; } isFetchingVideo = {}; modal.style.display = 'flex'; fetchBlacklist(1); }; const closeModal = () => { modal.style.display = 'none'; }; document.getElementById('blacklist-modal-close').onclick = closeModal; modal.onclick = (event) => { if (event.target == modal) closeModal(); }; const stopScanBtn = document.getElementById('stopScanBtn'); const closeScanModal = () => { scanModal.style.display = 'none'; if(isScanning) { isScanning = false; showToast('扫描已中止', 'success'); } }; stopScanBtn.onclick = () => { if(isScanning) { isScanning = false; stopScanBtn.disabled = true; stopScanBtn.textContent = '正在中止...'; showToast('扫描将在当前页面完成后中止...', 'success'); } }; document.getElementById('inactive-scan-modal-close').onclick = closeScanModal; scanModal.onclick = (event) => { if (event.target == scanModal) closeScanModal(); }; } if (document.readyState === 'complete' || document.readyState === 'interactive') { init(); } else { window.addEventListener('DOMContentLoaded', init); } })();