// ==UserScript== // @name 哔哩哔哩动态按关注分组过滤 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.1.0 // @description 动态信息按照分组组别过滤,选择后下拉刷新即可 // @author 口吃者 // @match https://t.bilibili.com/* // @require https://scriptcat.org/lib/2691/1.0.0/sweetalert2.all.min-11.15.10.js // @run-at document-body // @grant unsafeWindow // @license MIT // ==/UserScript== (function () { 'use strict'; class RequestMonitor { constructor() { // 预编译正则表达式提升匹配性能 this._patterns = new Map(); // Map // 使用WeakMap防止内存泄漏 this.pendingRequests = new WeakMap(); // 缓存已验证的URL this._urlCache = { monitored: new Set(), ignored: new Set() }; // 性能统计 this.stats = { total: 0, processed: 0, errors: 0 }; this._initHooks(); } /* pattern:正则表达式(只要满足其一就会进行监听处理),processor:处理器函数(对满足的每一个pattern进行processor处理,一对多) */ registerHandler(pattern, processor) { const regex = new RegExp(pattern); this._patterns.set(regex, processor); return this; // 支持链式调用 } _initHooks() { // 保存原生方法引用 const nativeFetch = unsafeWindow.fetch; const nativeXHROpen = XMLHttpRequest.prototype.open; const nativeXHRSend = XMLHttpRequest.prototype.send; const self = this; // Hook XHR open方法捕获URL XMLHttpRequest.prototype.open = function (method, url) { this._requestURL = url; return nativeXHROpen.apply(this, arguments); }; // Hook XHR send XMLHttpRequest.prototype.send = function (body) { const xhr = this; const url = xhr._requestURL || ''; // 前置检查 if (!self._shouldMonitor(url)) { return nativeXHRSend.apply(this, arguments); } const requestKey = {}; self.pendingRequests.set(xhr, requestKey); self.stats.total++; // 添加事件监听 xhr.addEventListener('load', () => self._handleXHRResponse(xhr, requestKey)); xhr.addEventListener('error', () => self._cleanup(xhr)); nativeXHRSend.apply(this, arguments); }; // Hook Fetch unsafeWindow.fetch = async (input, init) => { const url = typeof input === 'string' ? input : input.url; if (!self._shouldMonitor(url)) { return nativeFetch(input, init); } const requestKey = {}; self.pendingRequests.set(requestKey, true); self.stats.total++; try { const response = await nativeFetch(input, init); /* Response对象有一个重要特性:​​其body流(stream)只能被读取一次​。 ​避免直接消费response,导致原有网页代码报错,消费response.clone() */ self._handleFetchResponse(url, response.clone(), requestKey); return response; } catch (error) { self.stats.errors++; self._cleanup(requestKey); throw error; } }; } _shouldMonitor(url) { if (this._urlCache.monitored.has(url)) return true; if (this._urlCache.ignored.has(url)) return false; for (const [regex] of this._patterns) { if (regex.test(url)) { this._urlCache.monitored.add(url); return true; } } this._urlCache.ignored.add(url); return false; } async _handleXHRResponse(xhr, requestKey) { try { const data = await this._parseResponse(xhr.response); await this._processData(xhr._requestURL, data); this.stats.processed++; } catch (error) { this.stats.errors++; console.error('XHR处理失败:', error); } finally { this._cleanup(xhr); } } async _handleFetchResponse(url, response, requestKey) { try { const data = await this._parseResponse(await response.text()); await this._processData(url, data); this.stats.processed++; } catch (error) { this.stats.errors++; console.error('Fetch处理失败:', error); } finally { this._cleanup(requestKey); } } async _processData(url, rawData) { for (const [regex, processor] of this._patterns) { if (regex.test(url)) { await processor(rawData); break; // 仅匹配第一个处理器 } } } async _parseResponse(data) { try { return JSON.parse(data); } catch { return data; } } _cleanup(target) { this.pendingRequests.delete(target); // 定期清理缓存防止内存增长 if (this._urlCache.monitored.size > 1000) { this._urlCache.monitored.clear(); } } // 调试方法 printStats() { console.table({ '总请求数': this.stats.total, '已处理请求': this.stats.processed, '当前挂起请求': this.pendingRequests.size, '错误数量': this.stats.errors, '缓存命中率': `${(this.stats.processed / this.stats.total * 100).toFixed(1)}%` }); } } class FollerGroup { constructor() { this.groups = {}; // 使用对象存储分组,键是分组tagid,值是包含uid的数组 this.groupTagIdMap = {} } // 添加新分组 addGroup(groupName) { if (typeof groupName !== 'string' || groupName.trim() === '') { throw new Error('分组名称不能为空或无效'); } if (!this.groups[groupName]) { this.groups[groupName] = []; } } // 向指定分组添加博主 addUserToGroup(groupName, uid) { if (!this.groups[groupName]) { throw new Error(`分组 ${groupName} 不存在`); } if (!this.groups[groupName].includes(uid)) { this.groups[groupName].push(uid); } } addGroupTagIdMap(groupTagId, groupName) { this.groupTagIdMap[groupTagId] = groupName; } // 从指定分组移除博主 removeUserFromGroup(groupName, uid) { const group = this.groups[groupName]; if (group) { const index = group.indexOf(uid); if (index !== -1) { group.splice(index, 1); } } } // 移除整个分组 removeGroup(groupName) { delete this.groups[groupName]; } // 获取所有分组名称 getGroups() { return Object.keys(this.groups); } // 获取指定分组的所有博主uid getUsersInGroup(groupName) { return this.groups[groupName] || []; } // 获取所有分组的详细信息(分组名及其中的uid列表) getGroupsDetails() { return this.groups; } // 获取所有分组的详细信息(分组名及其中的uid列表) getGroupTagIdMap() { return this.groupTagIdMap; } extractUniqueUserIds() { const dictionary = this.groups; const uniqueUserIds = new Set(); //只处理对象本身拥有的属性,而不是从其原型链继承的属性 for (const key in dictionary) { if (dictionary.hasOwnProperty(key)) { const userIds = dictionary[key]; userIds.forEach(userId => uniqueUserIds.add(userId)); } } return Array.from(uniqueUserIds); } // 新增方法:将 groups 中的每个键对应的数组重置为空列表 resetAllGroups() { for (const key in this.groups) { if (this.groups.hasOwnProperty(key)) { this.groups[key] = []; } } } } const follerGroup = new FollerGroup(); const monitorTest = new RequestMonitor(); monitorTest.registerHandler('web-dynamic/v1/feed/all', async data => { const dynamicHtmlArray = document.querySelectorAll("div.bili-dyn-list > div.bili-dyn-list__items > div"); const saveItems = follerGroup.extractUniqueUserIds(); if(saveItems.length <= 0){ return; } if(dynamicHtmlArray){ dynamicHtmlArray.forEach(item=>{ if(item.style.display == 'none'){ return; } //防止懒加载报错 if(!item.querySelector("div.bili-dyn-title > span")){ return; } if(!saveItems.includes(item.querySelector("div.bili-dyn-title > span").innerText)){ item.style.display = "none"; } }) } }); window.addEventListener('load', addPanel); window.addEventListener('load', async () => { var result01 = await fetchUserFollowerGroup(); for (var result01ItemIndex in result01['data']) { var result01Item = result01['data'][result01ItemIndex]; if (result01Item["tagid"] == 0) { continue; } follerGroup.addGroupTagIdMap(result01Item['tagid'].toString(), result01Item['name']); follerGroup.addGroup(result01Item['tagid'].toString()); } console.log(follerGroup) }) async function fetchUserFollowerGroup(comment_params) { const response = await fetch("https://api.bilibili.com/x/relation/tags", { headers: { 'origin': 'https://www.bilibili.com' }, credentials: 'include' // 明确指定携带cookies }); return await response.json(); } async function fetchGroupDetail(comment_params) { const response = await fetch(`https://api.bilibili.com/x/relation/tag?tagid=293381761`, { headers: { 'origin': 'https://www.bilibili.com' }, credentials: 'include' // 明确指定携带cookies }); return await response.json(); } // Your code here... function addPanel() { function genButton(text, foo, id, fooParams = {}) { let b = document.createElement('button'); b.textContent = text; b.style.verticalAlign = 'inherit'; // 使用箭头函数创建闭包来保存 fooParams 并传递给 foo b.addEventListener('click', () => { foo.call(b, ...Object.values(fooParams)); // 使用 call 方法确保 this 指向按钮对象 }); if (id) { b.id = id }; return b; } function generateGroupCheckboxes(groupTagIdMap) { let html = ''; for (const groupId in groupTagIdMap) { const groupName = groupTagIdMap[groupId]; html += ` `; } return html.trim(); } function getSelectedValues() { const checkboxes = document.querySelectorAll('#swal2-html-container .swal2-checkbox input:checked'); if (!checkboxes) { return []; } const selectedValues = Array.from(checkboxes).map(cb => cb.value); return selectedValues; } async function openPanelFunc() { var swalRangeValue = 0; var initHtml = `|ω・)请等待网页加载完成~`; const { value: formValues } = await Swal.fire({ position: "center-end", draggable: true, backdrop: false, title: "Filter Dynamic", showCancelButton: true, cancelButtonText: 'Cancel', confirmButtonText: 'Confirm', //class="swal2-range" swalalert框架可能会对其有特殊处理,导致其内标签的id声明失效 html: initHtml, focusConfirm: false, didOpen: () => { document.getElementById('swal2-html-container').innerHTML = generateGroupCheckboxes(follerGroup.getGroupTagIdMap()); }, willClose: () => { }, preConfirm: () => { var selectedValues = getSelectedValues(); return [ selectedValues ]; } }); if (formValues) { if (formValues[0].length <= 0) { follerGroup.resetAllGroups(); const Toast = Swal.mixin({ toast: true, position: "top-end", showConfirmButton: false, timer: 3000, timerProgressBar: true, didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer; toast.onmouseleave = Swal.resumeTimer; } }); Toast.fire({ icon: "success", title: "重置成功~" }); return; } const fetchPromises = formValues[0].map(groupId => { // 发起请求 return fetch(`https://api.bilibili.com/x/relation/tag?tagid=${groupId}`, { headers: { 'origin': 'https://www.bilibili.com' }, credentials: 'include' // 明确指定携带 cookies }) .then(response => { if (!response.ok) { throw new Error("HTTP error " + response.status); } return response.json(); }) .then(data => { // 为成功的结果添加 groupId 作为标识符 return { groupId, data }; }) .catch(error => { // 为失败的结果添加 groupId 作为标识符 return { groupId, error }; }); }); Promise.all(fetchPromises).then( fetchResultArray => { fetchResultArray.forEach(result => { if (result.error) { console.error(`Request for groupId ${result.groupId} failed:`, result.error); } else { // console.log(`Result for groupId ${result.groupId}:`, result.data); const resultArray = result.data.data; follerGroup.resetAllGroups(); // 如果 data 存在且长度大于 0,执行 addUserToGroup if (resultArray && resultArray.length > 0) { resultArray.forEach(item => follerGroup.addUserToGroup(result.groupId.toString(), item["uname"])); } } }) const dynamicHtmlArray = document.querySelectorAll("div.bili-dyn-list > div.bili-dyn-list__items > div"); const saveItems = follerGroup.extractUniqueUserIds(); if(dynamicHtmlArray){ dynamicHtmlArray.forEach(item=>{ if(item.style.display == 'none'){ return; } //防止懒加载报错 if(!item.querySelector("div.bili-dyn-title > span")){ return; } if(!saveItems.includes(item.querySelector("div.bili-dyn-title > span").innerText)){ item.style.display = "none"; } }) } const Toast = Swal.mixin({ toast: true, position: "top-end", showConfirmButton: false, timer: 3000, timerProgressBar: true, didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer; toast.onmouseleave = Swal.resumeTimer; } }); Toast.fire({ icon: "success", title: "过滤成功~" }); } ) } } let myButton = genButton('Filter', openPanelFunc, 'Filter'); document.body.appendChild(myButton); var css_text = ` #Filter { position: fixed; color: #FF6B35; /* 橙色活力主题主色 */ top: 70%; right: -20px; /* 初始右侧隐藏 */ transform: translateY(-50%); z-index: 1000; padding: 10px 24px; border-radius: 5px; cursor: pointer; border: 0; background-color: white; box-shadow: rgba(0 0 0 / 5%) 0 0 8px; letter-spacing: 1.5px; text-transform: uppercase; font-size: 9px; transition: all 0.5s ease; } #Filter:hover { right: 0%; /* 鼠标悬停时完整显示 */ letter-spacing: 3px; background-image: linear-gradient(to top, #FFD700 0%, #FFA080 100%); /* 橙色渐变 */ box-shadow: rgba(255, 107, 0, 0.7) 0px 7px 29px 0px; /* 橙色阴影 */ } #Filter:active { letter-spacing: 3px; background-image: linear-gradient(to top, #FFD700 0%, #FFA080 100%); box-shadow: rgba(255, 107, 0, 0.5) 0px 0px 0px 0px; transition: 100ms; } /* 固定复选框容器高度并添加滚动条 */ #swal2-html-container { max-height: 200px; /* 可根据需求调整高度 */ overflow-y: auto; /* 垂直滚动条 */ overflow-x: hidden; /* 隐藏水平滚动条 */ padding-right: 15px; /* 防止滚动条遮挡内容 */ border: 1px solid #ddd; /* 可选:添加边框 */ } /* 可选:优化滚动条样式(仅现代浏览器支持) */ #swal2-html-container::-webkit-scrollbar { width: 8px; /* 滚动条宽度 */ } #swal2-html-container::-webkit-scrollbar-track { background: #f1f1f1; } #swal2-html-container::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } #swal2-html-container::-webkit-scrollbar-thumb:hover { background: #555; } /* 基础样式 */ #swal2-html-container .swal2-checkbox { padding: 8px 12px; /* 增加内边距以便悬停效果更明显 */ transition: background-color 0.2s ease; /* 平滑过渡效果 */ } /* 鼠标悬停时的背景色 */ #swal2-html-container .swal2-checkbox:hover { background-color: #e0e0e0; /* 浅灰色 */ } /* 可选:已选中时的样式(可选) */ #swal2-html-container .swal2-checkbox input:checked ~ .swal2-label { color: #2E8B57; /* 选中后的文字颜色(可自定义) */ } `; GMaddStyle(css_text); } function GMaddStyle(css) { var myStyle = document.createElement('style'); myStyle.textContent = css; var doc = document.head || document.documentElement; doc.appendChild(myStyle); } })();