B站成分检测器

B站评论区自动标注成分,支持动态和关注识别,默认包括原神玩家和王者荣耀玩家

目前为 2022-09-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name B站成分检测器
  3. // @version 1.7
  4. // @author xulaupuz,trychen
  5. // @namespace trychen.com
  6. // @license GPLv3
  7. // @description B站评论区自动标注成分,支持动态和关注识别,默认包括原神玩家和王者荣耀玩家
  8. // @match https://www.bilibili.com/video/*
  9. // @match https://www.bilibili.com/read/*
  10. // @match https://t.bilibili.com/*
  11. // @icon https://static.hdslb.com/images/favicon.ico
  12. // @connect bilibili.com
  13. // @grant GM_xmlhttpRequest
  14. // @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js
  15. // ==/UserScript==
  16.  
  17. $(function () {
  18. // 在这里配置要检查的成分
  19. const checkers = [
  20. {
  21. displayName: "原神",
  22. displayIcon: "https://i2.hdslb.com/bfs/face/d2a95376140fb1e5efbcbed70ef62891a3e5284f.jpg@240w_240h_1c_1s.jpg",
  23. keywords: ["互动抽奖 #原神", "米哈游", "#米哈游#", "#miHoYo#"],
  24. followings: [401742377] // 原神官方号的 UID
  25. },
  26. {
  27. displayName: "王者荣耀",
  28. displayIcon: "https://i2.hdslb.com/bfs/face/effbafff589a27f02148d15bca7e97031a31d772.jpg@240w_240h_1c_1s.jpg",
  29. keywords: ["互动抽奖 #王者荣耀"]
  30. }
  31. ]
  32.  
  33. // 空间动态api
  34. const spaceApiUrl = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid='
  35. const followingApiUrl = 'https://api.bilibili.com/x/relation/followings?vmid='
  36.  
  37. const checked = {}
  38. const checking = {}
  39. var printed = false
  40.  
  41. // 监听用户ID元素出现
  42. waitForKeyElements(".user-name", installCheckButton);
  43. waitForKeyElements(".sub-user-name", installCheckButton);
  44. waitForKeyElements(".user .name", installCheckButton);
  45.  
  46. console.log("开启B站用户成分检查器...")
  47.  
  48. // 添加检查按钮
  49. function installCheckButton(element) {
  50. let node = $(`<div style="display: inline;" class="composition-checkable"><div class="composition-badge">
  51. <a class="composition-name">检查成分</a>
  52. </div></div>`)
  53.  
  54. node.on('click', function () {
  55. node.find(".composition-name").text("检查中...")
  56. checkComposition(element, node.find(".composition-name"))
  57. })
  58.  
  59. element.after(node)
  60. }
  61.  
  62. // 添加标签
  63. function installComposition(id, element, setting) {
  64. let node = $(`<div style="display: inline;"><div class="composition-badge">
  65. <a class="composition-name">${setting.displayName}</a>
  66. <img src="${setting.displayIcon}" class="composition-icon">
  67. </div></div>`)
  68.  
  69. element.after(node)
  70. }
  71.  
  72. // 检查标签
  73. function checkComposition(element, loadingElement) {
  74. // 用户ID
  75. let userID = element.attr("data-user-id") || element.attr("data-usercard-mid")
  76. // 用户名
  77. let name = element.text().charAt(0) == "@" ? element.text().substring(1) : element.text()
  78.  
  79. if (checked[userID]) {
  80. // 已经缓存过了
  81. for(let setting of checked[userID]) {
  82. installComposition(userID, element, setting)
  83. }
  84. } else if (checking[userID] != undefined) {
  85. // 检查中
  86. if (checking[userID].indexOf(element) < 0)
  87. checking[userID].push(element)
  88. } else {
  89. checking[userID] = [element]
  90.  
  91. // 获取最近动态
  92. GM_xmlhttpRequest({
  93. method: "get",
  94. url: spaceApiUrl + userID,
  95. data: '',
  96. headers: {
  97. 'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
  98. },
  99. onload: res => {
  100. if(res.status === 200) {
  101. // 获取关注列表
  102. GM_xmlhttpRequest({
  103. method: "get",
  104. url: followingApiUrl + userID,
  105. data: '',
  106. headers: {
  107. 'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
  108. },
  109. onload: followingRes => {
  110. if(followingRes.status === 200) {
  111. // 解析关注列表
  112. let followingData = JSON.parse(followingRes.response)
  113. // 可能无权限
  114. let following = followingData.code == 0 ? followingData.data.list.map(it => it.mid) : []
  115.  
  116. // 解析并拼接动态数据
  117. let st = JSON.stringify(JSON.parse(res.response).data.items)
  118.  
  119. // 找到的匹配内容
  120. let found = []
  121. for(let setting of checkers) {
  122. // 检查动态内容
  123. if (setting.keywords)
  124. if (setting.keywords.find(keyword => st.includes(keyword))) {
  125. if (found.indexOf(setting) < 0)
  126. found.push(setting)
  127. continue;
  128. }
  129.  
  130. // 检查关注列表
  131. if (setting.followings)
  132. for(let mid of setting.followings) {
  133. if (following.indexOf(mid) >= 0) {
  134. if (found.indexOf(setting) < 0)
  135. found.push(setting)
  136. continue;
  137. }
  138. }
  139. }
  140.  
  141. // 添加标签
  142. if (found.length > 0) {
  143. if (!printed) {
  144. console.log(JSON.parse(res.response).data)
  145. printed = true
  146. }
  147.  
  148.  
  149. // 输出日志
  150. console.log(`检测到 ${name} ${userID} 的成分为 `, found.map(it => it.displayName))
  151. checked[userID] = found
  152.  
  153. // 给所有用到的地方添加标签
  154. for (let element of checking[userID]) {
  155. for(let setting of found) {
  156. installComposition(userID, element, setting)
  157. }
  158. }
  159. }
  160.  
  161. loadingElement.text('无')
  162. } else {
  163. console.log(`检测 ${name} ${userID} 的关注列表失败`, followingRes)
  164.  
  165. loadingElement.text('失败')
  166. }
  167.  
  168. delete checking[userID]
  169. },
  170. onerror: err => {
  171. console.log(`检测 ${name} ${userID} 的成分最近动态失败`, err)
  172.  
  173. loadingElement.text('失败')
  174. delete checking[userID]
  175. },
  176. })
  177.  
  178.  
  179. } else {
  180. console.log(`检测 ${name} ${userID} 的成分失败`, res)
  181. loadingElement.text('失败')
  182.  
  183. delete checking[userID]
  184. }
  185. },
  186. onerror: err => {
  187. console.log(`检测 ${name} ${userID} 的成分失败`, err)
  188. loadingElement.text('失败')
  189. delete checking[userID]
  190. },
  191. });
  192. }
  193. }
  194.  
  195. // 添加标签样式
  196. addGlobalStyle(`
  197. .composition-badge {
  198. display: inline-flex;
  199. justify-content: center;
  200. align-items: center;
  201. width: fit-content;
  202. background: #00AEEC26;
  203. border-radius: 10px;
  204. margin: 0 5px;
  205. font-family: PingFang SC, HarmonyOS_Regular, Helvetica Neue, Microsoft YaHei, sans-serif;
  206. }
  207. .composition-name {
  208. line-height: 13px;
  209. font-size: 13px;
  210. color: #00AEEC;
  211. padding: 2px 8px;
  212. }
  213. .composition-icon {
  214. width: 25px;
  215. height: 25px;
  216. border-radius: 50%;
  217. border: 2px solid white;
  218. margin: -6px;
  219. margin-right: 5px;
  220. }
  221. `)
  222.  
  223. function addGlobalStyle(css) {
  224. var head, style;
  225. head = document.getElementsByTagName('head')[0];
  226. if (!head) { return; }
  227. style = document.createElement('style');
  228. style.type = 'text/css';
  229. style.innerHTML = css;
  230. head.appendChild(style);
  231. }
  232.  
  233. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  234. that detects and handles AJAXed content.
  235. Usage example:
  236. waitForKeyElements (
  237. "div.comments"
  238. , commentCallbackFunction
  239. );
  240. //--- Page-specific function to do what we want when the node is found.
  241. function commentCallbackFunction (jNode) {
  242. jNode.text ("This comment changed by waitForKeyElements().");
  243. }
  244. IMPORTANT: This function requires your script to have loaded jQuery.
  245. */
  246. function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) {
  247. var targetNodes, btargetsFound;
  248.  
  249. if (typeof iframeSelector == "undefined")
  250. targetNodes = $(selectorTxt);
  251. else
  252. targetNodes = $(iframeSelector).contents ()
  253. .find (selectorTxt);
  254.  
  255. if (targetNodes && targetNodes.length > 0) {
  256. btargetsFound = true;
  257. targetNodes.each ( function () {
  258. var jThis = $(this);
  259. var alreadyFound = jThis.data ('alreadyFound') || false;
  260.  
  261. if (!alreadyFound) {
  262. //--- Call the payload function.
  263. var cancelFound = actionFunction (jThis);
  264. if (cancelFound) btargetsFound = false;
  265. else jThis.data ('alreadyFound', true);
  266. }
  267. } );
  268. } else {
  269. btargetsFound = false;
  270. }
  271.  
  272. //--- Get the timer-control variable for this selector.
  273. var controlObj = waitForKeyElements.controlObj || {};
  274. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  275. var timeControl = controlObj [controlKey];
  276.  
  277. //--- Now set or clear the timer as appropriate.
  278. if (btargetsFound && bWaitOnce && timeControl) {
  279. //--- The only condition where we need to clear the timer.
  280. clearInterval (timeControl);
  281. delete controlObj [controlKey]
  282. } else {
  283. //--- Set a timer, if needed.
  284. if ( ! timeControl) {
  285. timeControl = setInterval ( function () {
  286. waitForKeyElements(selectorTxt,actionFunction,bWaitOnce,iframeSelector);
  287. }, 300);
  288. controlObj [controlKey] = timeControl;
  289. }
  290. }
  291. waitForKeyElements.controlObj = controlObj;
  292. }
  293. })

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址