B站评论区自动标注玩家,依据是动态里是否有游戏的相关内容。灵感来自于原神玩家指示器。
// ==UserScript== // @name B站玩家指示器 // @namespace http://853lab.com/ // @version 1.1 // @description B站评论区自动标注玩家,依据是动态里是否有游戏的相关内容。灵感来自于原神玩家指示器。 // @author Sonic853 // @match https://www.bilibili.com/video/* // @icon https://static.hdslb.com/images/favicon.ico // @run-at document-end // @license MIT License // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @original-author xulaupuz // @original-license MIT // @original-script https://greasyfork.org/zh-CN/scripts/450720-%E5%8E%9F%E7%A5%9E%E7%8E%A9%E5%AE%B6%E6%8C%87%E7%A4%BA%E5%99%A8 // ==/UserScript== // 凭实力走在对立面, // 这就是我的觉悟。 // https://h.bilibili.com/67313825 // So #### YOU, miHoYo! // 如何超越米哈游公司? // ChatGPT:米哈游公司是一家拥有极高知名度和成功游戏产品的公司,要超越它需要做出一些艰苦的工作和采取正确的策略。以下是几个可能有用的建议: // 注重产品质量:米哈游公司之所以能够取得成功,是因为他们推出了高质量、受玩家欢迎的游戏。要超越米哈游公司,您需要注重产品质量,确保游戏具有高水准的游戏体验和优秀的画面效果。 // 创新:在游戏行业,创新是非常重要的。米哈游公司之所以能够在市场上脱颖而出,是因为他们不断尝试新的玩法和元素。如果您想要超越米哈游公司,需要不断探索新的玩法和游戏元素,推出具有创新性的产品。 // 建立强大的社区:米哈游公司拥有一个庞大的社区,这个社区不仅可以帮助他们推广游戏,还可以为游戏提供反馈和建议。要超越米哈游公司,您需要建立一个强大的社区,与玩家建立良好的互动关系,并且倾听他们的反馈和建议。 // 投入足够的资源:游戏开发需要大量的资源,包括人力、物力、资金等。如果您想要超越米哈游公司,需要投入足够的资源,包括聘请优秀的开发团队、研发创新技术、开展广告宣传等。 // 扩大市场:米哈游公司的成功,与其在全球市场上的表现密不可分。如果您想要超越米哈游公司,需要扩大市场,将游戏推向更广泛的受众,特别是在海外市场上建立自己的品牌和口碑。 // 请注意,要超越米哈游公司是一个艰难的任务,需要付出大量的努力和资源。同时,市场竞争也是不断变化的,需要时刻保持敏锐的洞察力和灵活性,随时调整自己的策略。 (async function () { 'use strict' const DEV_Log = Boolean(localStorage.getItem("Dev-853")) const localItem = "miHoYoCheck" const NAME = "miHoYoCheck" const D = () => { return new Date().toLocaleTimeString() } const Console_log = function (...text) { let d = new Date().toLocaleTimeString() console.log(`[${NAME}][${d}]: `, ...text) } const Console_Devlog = function (...text) { let d = new Date().toLocaleTimeString() DEV_Log && (console.log(`[${NAME}][${d}]: `, ...text)) } const Console_error = function (...text) { let d = new Date().toLocaleTimeString() console.error(`[${NAME}][${d}]: `, ...text) } const snooze = ms => new Promise(resolve => setTimeout(resolve, ms)) const RList = new class { time = 200 #list = -1 async Push() { this.#list++ await snooze(this.#list * this.time) Promise.resolve().finally(() => { setTimeout(() => { this.#list-- }, (this.#list + 1) * this.time) }) } } if (typeof GM_xmlhttpRequest === 'undefined' && typeof GM_registerMenuCommand === 'undefined' && typeof GM_setValue === 'undefined' && typeof GM_getValue === 'undefined') { console.error(`[${NAME}][${D()}]: `, "GM is no Ready.") } else { console.log(`[${NAME}][${D()}]: `, "GM is Ready.") } /** * * @param {string} url * @param {string} method * @param {Object.<string, any>} headers * @param {string} responseType * @param {*} successHandler * @param {*} errorHandler * @returns */ let HTTPsend = function (url, method, headers, responseType, successHandler, errorHandler) { Console_Devlog(url) if (typeof GM_xmlhttpRequest != 'undefined') { return new Promise((rl, rj) => { try { GM_xmlhttpRequest({ method, url, headers, responseType, onerror: function (response) { Console_Devlog(response.status) errorHandler && errorHandler(response.status) rj(response.status) }, onload: function (response) { let status if (response.readyState == 4) { // `DONE` status = response.status if (status == 200) { Console_Devlog(response.response) successHandler && successHandler(response.response) rl(response.response) } else { Console_Devlog(status) errorHandler && errorHandler(status) rj(status) } } }, }) } catch (error) { rj(error) } }) } else { return new Promise((rl, rj) => { try { let xhr = new XMLHttpRequest() xhr.open(method, url, true) xhr.withCredentials = true xhr.responseType = responseType xhr.onreadystatechange = function () { let status if (xhr.readyState == 4) { // `DONE` status = xhr.status if (status == 200) { Console_log(xhr.response) successHandler && successHandler(xhr.response) rl(xhr.response) } else { Console_log(status) errorHandler && errorHandler(status) rj(status) } } } xhr.send() } catch (error) { rj(error) } }) } } let BLab8A = class { /** * @type {Object.<string, { * name: string, * uid: string * }[]>} data */ data constructor() { this.data = this.load() } load() { console.log(`[${NAME}][${D()}]: `, "正在加载数据") const defaultData = "{\"unknown\":[],\"miHoYo\":[],\"none_Player\":[]}" if (typeof GM_getValue !== 'undefined') { let gdata = GM_getValue(localItem, JSON.parse(defaultData)) return gdata } else { let ldata = JSON.parse(localStorage.getItem(localItem) === null ? defaultData : localStorage.getItem(localItem)) return ldata } } save(d) { console.log(`[${NAME}][${D()}]: `, "正在保存数据") d === undefined ? (d = this.data) : (this.data = d) typeof GM_getValue != 'undefined' ? GM_setValue(localItem, d) : localStorage.setItem(localItem, JSON.stringify(d)) return this } } let bLab8A = new BLab8A() GM_registerMenuCommand("清空插件所有数据", () => { if (!confirm("确定要清空数据吗?")) return bLab8A.data = JSON.parse("{\"unknown\":[],\"miHoYo\":[],\"none_Player\":[]}") bLab8A.save() console.log(`[${NAME}][${D()}]: `, "数据已清空") }) GM_registerMenuCommand("清空插件里的玩家数据", () => { if (!confirm("确定要清空数据吗?")) return // 除了 none_Player ,其他都清空 for (let key in bLab8A.data) { if (key != "none_Player") { bLab8A.data[key] = [] } } bLab8A.save() console.log(`[${NAME}][${D()}]: `, "数据已清空") }) GM_registerMenuCommand("清空插件里的非玩家数据", () => { if (!confirm("确定要清空数据吗?")) return bLab8A.data.none_Player = [] bLab8A.save() console.log(`[${NAME}][${D()}]: `, "数据已清空") }) class Checker { running = false /** * @type {{ * name: string, * uid: string, * dom: HTMLElement * }[]} */ list = [] /** * @type {{ * keywords: string[], * soeWithKeywords: string[], * uids: string[], * games: string[], * tag: string, * type: string * }[]} */ KeywordChecklist = [ { keywords: [ "玩原神", "原神玩家", "#原神#", "【原神", "《原神", "[原神", "米哈游", "崩坏三", "崩坏3", "崩坏学园", "崩坏学院", "miHoYo", "崩坏星穹铁道", "崩坏:星穹铁道", "崩坏:星穹铁道", "未定事件簿", "绝区零", "米游社", // "鹿鸣", ], soeWithKeywords: [ "原神", ], uids: [ // 崩坏3 "256667467", // 米哈游miHoYo "318432901", // 崩坏学园2-灵依娘d### "133934", // 崩坏3第一偶像#酱 "27534330", // 崩坏3情报姬 "358367842", // 崩坏星穹铁道 "1340190821", // 原神 "401742377", // 米哈游崩坏3客服娘 "33307860", "52957002", // 米游姬 "510189715", // yoyo鹿鸣_Lumi "488836173", // 绝区零 "1636034895", // 未定事件簿 "436175352", ], games: [ "原神", "崩坏3", "崩坏学园2", "崩坏:星穹铁道", "绝区零", ], tag: "[米哈游玩家]", icon: "https://i1.hdslb.com/bfs/face/fda1b06144d41092a9ffb9a687f99bad078e7395.jpg", color: "#00abf1", type: "miHoYo", }, { keywords: [ "玩原神", "原神玩家", "#原神#", "【原神", "《原神", "[原神", ], soeWithKeywords: [ "原神", ], uids: [ // 原神 "401742377", ], games: [ "原神", ], tag: "[原神玩家]", icon: "https://i2.hdslb.com/bfs/face/d2a95376140fb1e5efbcbed70ef62891a3e5284f.jpg", color: "#ff0000", type: "Genshin", }, // { // keywords: [ // "王者荣耀", // ], // soeWithKeywords: [], // uids: [ // "57863910", // "392836434", // ], // games: [ // "王者荣耀", // ], // tag: "[王者荣耀玩家]", // icon: "https://i2.hdslb.com/bfs/face/effbafff589a27f02148d15bca7e97031a31d772.jpg", // color: "#79c7e7", // type: "HOK", // }, ] dynamicURL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space" lastplaygameURL = "https://api.bilibili.com/x/space/lastplaygame" get is_new() { return document.querySelectorAll(".reply-list").length != 0 } get getDom() { if (this.is_new) { return document.querySelectorAll(".reply-list") // sub-reply-list } else { return document.querySelectorAll(".comment-list") // reply-box } } /** * @param {HTMLElement} node * @returns {{ * name: string, * uid: string, * dom: HTMLElement * }} */ getUser(node) { if (this.is_new) { // console.log(node) return { name: node.innerText, uid: node.getAttribute("data-user-id"), dom: node } } else { /** @type {HTMLAnchorElement} */ let child = node.children[0] if (child.href == undefined) { for (let _child of node.children) { if (_child.href != undefined) { child = _child break } } } if (child === undefined && child.href == undefined) return // console.log(child) let uid = child.getAttribute("data-usercard-mid") === null ? child.href.replace(/[^\d]/g, "") : child.getAttribute("data-usercard-mid") return { name: child.innerText, uid, dom: child } } } getUserList() { /** * @type {{ * name: string, * uid: string, * dom: HTMLElement * }[]} */ let list = [] if (this.is_new) { let users = document.querySelectorAll(".user-name") let subuser = document.querySelectorAll(".sub-user-name") for (let user of users) { let u = this.getUser(user) if (u != undefined && !list.some(e => e.uid == u.uid)) { { list.push(u) } } } for (let user of subuser) { let u = this.getUser(user) if (u != undefined && !list.some(e => e.uid == u.uid)) { { list.push(u) } } } } else { let users = document.querySelectorAll(".user") for (let user of users) { let u = this.getUser(user) if (u != undefined && !list.some(e => e.uid == u.uid)) { { list.push(u) } } } } return list } /** * @param {string} uid * @param {string} offset */ async getUserDynamic(uid, offset = "") { // ?offset=&host_mid= if (uid == undefined) return /** * @type {{ * code: number, * message: string, * ttl: number, * data: { * has_more: boolean, * items: { * id_str: string, * modules: { * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * orig?: { * modules: { * module_author: { * mid: string, * name: string * }, * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * type: string * }, * type: string * }[], * offset: string, * update_baseline: string, * update_num: number * } * }} */ let data = JSON.parse(await HTTPsend(`${this.dynamicURL}?offset=${offset}&host_mid=${uid}`, "GET", { "Referer": `https://space.bilibili.com/${uid}/dynamic`, } )) if (data.code != 0) { console.error(`[${NAME}][${D()}]: `, data) return } let items = data.data.items return { items, offset: data.data.offset, } } async getUserLastPlayGame(uid) { /** * @type {{ * code: number, * message: string, * ttl: number, * data: { * website: string, * name: string, * image: string * }[] * }} */ let data = JSON.parse(await HTTPsend(`${this.lastplaygameURL}?mid=${uid}`, "GET", { "Referer": `https://space.bilibili.com/${uid}`, } )) if (data.code != 0) { console.error(`[${NAME}][${D()}]: `, data) return [] } let items = data.data return items } /** * @param {{ * id_str: string, * modules: { * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * orig?: { * modules: { * module_author: { * mid: string, * name: string * }, * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * type: string * }, * type: string * }[]} items */ checkDynamicsIncludeKeyword(items) { let types = [] for (let item of items) { let _types = this.checkDynamicIncludeKeyword(item) for (let type of _types) { if (!types.includes(type)) { types.push(type) } } } return [...new Set(types)] // return types } /** * @param {{ * id_str: string, * modules: { * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * orig?: { * modules: { * module_author: { * mid: string, * name: string * }, * module_dynamic: { * desc?: { * rich_text_nodes: { * orig_text: string, * text: string, * type: string * }[], * text: string * }, * major?: { * archive: { * aid: string, * title: string, * desc: string, * } * } * } * }, * type: string * }, * type: string * }} item */ checkDynamicIncludeKeyword(item) { if (item == undefined) return [] /** @type {string[]} */ let types = [] if (item.modules.module_dynamic.desc != null) { types = this.checkKeyword(item.modules.module_dynamic.desc.text) } switch (item.type) { case "DYNAMIC_TYPE_FORWARD": { if (item.orig != undefined) { for (let _item of this.KeywordChecklist) { for (let uid of _item.uids) { if (item.orig.modules.module_author.mid == uid) { if (!types.includes(_item.type)) { types.push(_item.type) } } } } switch (item.orig.type) { case "DYNAMIC_TYPE_DRAW": case "DYNAMIC_TYPE_COMMON_SQUARE": case "DYNAMIC_TYPE_WORD": { if (item.orig.modules.module_dynamic.desc != null) { let _types = this.checkKeyword(item.orig.modules.module_dynamic.desc.text) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } } } break case "DYNAMIC_TYPE_AV": { if (item.orig.modules.module_dynamic.desc != null) { let _types = this.checkKeyword(item.orig.modules.module_dynamic.desc.text) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } } if (item.orig.modules.module_dynamic.major != null) { Console_Devlog(item.orig.modules.module_dynamic.major) let _types = this.checkKeyword(item.orig.modules.module_dynamic.major?.archive?.title) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } _types = this.checkKeyword(item.orig.modules.module_dynamic.major?.archive?.desc) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } } } break case "DYNAMIC_TYPE_NONE": { } break } } } break case "DYNAMIC_TYPE_AV": { if (item.modules.module_dynamic.major != null) { let _types = this.checkKeyword(item.modules.module_dynamic.major?.archive?.title) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } _types = this.checkKeyword(item.modules.module_dynamic.major?.archive?.desc) // 加入 types 中并去重 for (let type of _types) { if (!types.includes(type)) { types.push(type) } } } } break case "DYNAMIC_TYPE_DRAW": case "DYNAMIC_TYPE_WORD": { } break default: break } return [...new Set(types)] // return types } /** * * @param {string} text * @returns {string[]} */ checkKeyword(text) { let types = [] // 循环 this.KeywordChecklist if (text != null) { for (let item of this.KeywordChecklist) { for (let keyword of item.keywords) { if (text.includes(keyword) && !types.includes(item.type)) { types.push(item.type) } } for (let keyword of item.soeWithKeywords) { if ((text.startsWith(keyword) || text.endsWith(keyword)) && !types.includes(item.type)) { types.push(item.type) } } } } return types } async checkUser() { if (this.running) return if (this.list.length == 0) return this.running = true // 复制一份 this.list let list = this.list.slice() for (let user of list) { /** * @type {{ * website: string; * name: string; * image: string; * }[]|undefined} */ let games = undefined let needCheck = false for (let item of this.KeywordChecklist) { if (!bLab8A.data[item.type]) { bLab8A.data[item.type] = [] } if (bLab8A.data[item.type].some(e => e.uid == user.uid)) { console.log(`[${NAME}][${D()}]: `, `已知的${item.tag}`, user) if (user.dom != undefined) { this.insertSpan(user.dom, item) } continue } else if (!bLab8A.data.none_Player.some(e => e.uid == user.uid)) { needCheck = true } } if (!needCheck) { continue } console.log(`[${NAME}][${D()}]: `, `检查用户 ${user.name}`) /** @type {string[]} */ let types = [] if (games === undefined) { games = await this.getUserLastPlayGame(user.uid) if (games.length != 0) { for (let game of games) { // 判断 game.name 是否和 this.KeywordChecklist 里的 games 一致 for (let item of this.KeywordChecklist) { if (item.games.includes(game.name) && !types.includes(item.type)) { types.push(item.type) } } } } } // await RList.Push() let dynamic = await this.getUserDynamic(user.uid) if (dynamic == undefined) continue if (dynamic.items.length == 0) continue types = [...new Set([...types, ...this.checkDynamicsIncludeKeyword(dynamic.items)])] // await RList.Push() dynamic = await this.getUserDynamic(user.uid, dynamic.offset) if (dynamic != undefined && dynamic.items.length !== 0) { // 合并 types 并去重 types = [...new Set([...types, ...this.checkDynamicsIncludeKeyword(dynamic.items)])] } if (types.length !== 0) { console.log(`[${NAME}][${D()}]: `, `游戏玩家${types.join(",")}`, user) for (let type of types) { if (!bLab8A.data[type]) { bLab8A.data[type] = [] } bLab8A.data[type].push({ uid: user.uid, name: user.name, }) bLab8A.save() // 从 this.KeywordChecklist 找到 type 一致的项 let _item = this.KeywordChecklist.find(e => e.type == type) if (_item != undefined) { this.insertSpan(user.dom, _item) } } } else { console.log(`[${NAME}][${D()}]: `, "非游戏玩家", user) if (!bLab8A.data.none_Player) { bLab8A.data.none_Player = [] } bLab8A.data.none_Player.push({ uid: user.uid, name: user.name, }) bLab8A.save() } } this.running = false // 删除已经检查过的用户 for (let user of list) { this.list.splice(this.list.indexOf(user), 1) } if (this.list.length != 0) { this.checkUser() } } /** * @param {HTMLElement} dom * @param {{ * tag: string, * type: string, * color: string, * }} item */ insertSpan(dom, item) { if (dom != undefined) { if (dom.querySelector(`span.check_tag_${item.type}`) != null) { return } let span = document.createElement("span") span.classList.add(`check_tag_${item.type}`) span.style.color = item.color span.title = `非准确结果,是否为${item.tag}请自行判断` span.innerText = item.tag dom.appendChild(span) } } } let checker = new Checker() /** * @param {MutationRecord[]} mutationList * @param {MutationObserver} observer */ let callback = (mutationList, observer) => { Console_Devlog(`[${NAME}][${D()}]: `, "callback", mutationList) let list = checker.getUserList() for (let item of list) { if (!checker.list.some(e => e.dom == item.dom)) { checker.list.push(item) } } checker.checkUser() // mutationList.forEach(mutation => { // if (mutation.type == "childList") { // // [0].addedNodes[0].children[1].children[0].children[0].href // // mutation.addedNodes.forEach(node => { // // if (checker.is_new) { // // try { // // } catch (error) { // // } // // } // // else { // // try { // // console.log(node.childNodes[1].childNodes[0].childNodes[0].href) // // } catch (error) { // // } // // } // // }) // } // }) } /** @type {MutationObserverInit} */ let observerOption = { childList: true, subtree: true, } let observer = new MutationObserver(callback) // while 检测 checker.getDom while (checker.getDom.length == 0) { console.log(`[${NAME}][${D()}]: `, "寻找中") await RList.Push() } for (let _dom of checker.getDom) { observer.observe(_dom, observerOption) } // observer.observe(checker.getDom[0], observerOption) Console_Devlog(`[${NAME}][${D()}]: `, "开始监听") // 先获取一次列表 let list = checker.getUserList() for (let item of list) { if (!checker.list.some(e => e.dom == item.dom)) { checker.list.push(item) } } checker.checkUser() })()