Greasy Fork is available in English.

B站弹幕查询器(查发布者)

通过B站视频查询弹幕并查找指定用户

  1. // ==UserScript==
  2. // @name B站弹幕查询器(查发布者)
  3. // @namespace PyHaoCoder
  4. // @version 1.0
  5. // @description 通过B站视频查询弹幕并查找指定用户
  6. // @author PyHaoCoder
  7. // @icon https://www.bilibili.com/favicon.ico
  8. // @match https://www.bilibili.com/video/*
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_addStyle
  11. // @connect api.bilibili.com
  12. // ==/UserScript==
  13.  
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. // 定时器间隔时间(单位:毫秒)
  19. const intervalTime = 1000; // 5秒
  20.  
  21. // ==================== 获取视频CID ====================
  22. function fetchCID() {
  23. const bvid = location.href.split('/')[4].split('?')[0];
  24. const url = `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}&jsonp=jsonp`
  25.  
  26. GM_xmlhttpRequest({
  27. method: 'GET',
  28. url: url,
  29. onload: function (response) {
  30. // 获取页面内容
  31. const pageContent = response.responseText;
  32. const data = JSON.parse(pageContent);
  33. if (data.data) {
  34. // 获取视频CID
  35. window._cid = data.data[0].cid; // 更新全局变量
  36. console.log('获取视频CID:', window._cid);
  37.  
  38. // 更新 _url
  39. if (!window._url || location.href !== _url) {
  40. window._url = location.href;
  41. }
  42. } else {
  43. console.error('获取视频CID失败');
  44. }
  45. },
  46. onerror: function (err) {
  47. console.log('获取视频CID失败:' + err.statusText);
  48. }
  49. });
  50. }
  51.  
  52. // 隐藏表格
  53. function hideTable() {
  54. const container = document.getElementById('resultContainer')
  55. container.style.display = "none"
  56.  
  57. }
  58.  
  59. // 初始化定时器
  60. function startTimer() {
  61. setInterval(() => {
  62. // 如果 _url 未定义或当前 URL 不等于 _url,则更新 CID
  63. if (window._url && location.href !== window._url) {
  64. fetchCID();
  65. hideTable();
  66. }
  67. }, intervalTime);
  68. }
  69.  
  70. // 首次初始化
  71. fetchCID();
  72.  
  73. // 启动定时器
  74. startTimer();
  75. })();
  76.  
  77. (function () {
  78. 'use strict';
  79.  
  80. // ==================== 哈希转换模块 ====================
  81. window.BiliBili_midcrc = function () {
  82. 'use strict';
  83. const CRCPOLYNOMIAL = 0xEDB88320;
  84. const startTime = new Date().getTime(),
  85. crctable = new Array(256),
  86. create_table = function () {
  87. let crcreg,
  88. i, j;
  89. for (i = 0; i < 256; ++i) {
  90. crcreg = i;
  91. for (j = 0; j < 8; ++j) {
  92. if ((crcreg & 1) != 0) {
  93. crcreg = CRCPOLYNOMIAL ^ (crcreg >>> 1);
  94. } else {
  95. crcreg >>>= 1;
  96. }
  97. }
  98. crctable[i] = crcreg;
  99. }
  100. },
  101. crc32 = function (input) {
  102. if (typeof (input) != 'string')
  103. input = input.toString();
  104. let crcstart = 0xFFFFFFFF, len = input.length, index;
  105. for (let i = 0; i < len; ++i) {
  106. index = (crcstart ^ input.charCodeAt(i)) & 0xff;
  107. crcstart = (crcstart >>> 8) ^ crctable[index];
  108. }
  109. return crcstart;
  110. },
  111. crc32lastindex = function (input) {
  112. if (typeof (input) != 'string')
  113. input = input.toString();
  114. let crcstart = 0xFFFFFFFF, len = input.length, index;
  115. for (let i = 0; i < len; ++i) {
  116. index = (crcstart ^ input.charCodeAt(i)) & 0xff;
  117. crcstart = (crcstart >>> 8) ^ crctable[index];
  118. }
  119. return index;
  120. },
  121. getcrcindex = function (t) {
  122. for (let i = 0; i < 256; i++) {
  123. if (crctable[i] >>> 24 == t)
  124. return i;
  125. }
  126. return -1;
  127. },
  128. deepCheck = function (i, index) {
  129. let tc = 0x00, str = '',
  130. hash = crc32(i);
  131. tc = hash & 0xff ^ index[2];
  132. if (!(tc <= 57 && tc >= 48))
  133. return [0];
  134. str += tc - 48;
  135. hash = crctable[index[2]] ^ (hash >>> 8);
  136. tc = hash & 0xff ^ index[1];
  137. if (!(tc <= 57 && tc >= 48))
  138. return [0];
  139. str += tc - 48;
  140. hash = crctable[index[1]] ^ (hash >>> 8);
  141. tc = hash & 0xff ^ index[0];
  142. if (!(tc <= 57 && tc >= 48))
  143. return [0];
  144. str += tc - 48;
  145. hash = crctable[index[0]] ^ (hash >>> 8);
  146. return [1, str];
  147. };
  148. create_table();
  149. const index = new Array(4);
  150.  
  151. // 单次转换函数
  152. const singleConvert = function (input) {
  153. let ht = parseInt('0x' + input) ^ 0xffffffff,
  154. snum, i, lastindex, deepCheckData;
  155. for (i = 3; i >= 0; i--) {
  156. index[3 - i] = getcrcindex(ht >>> (i * 8));
  157. snum = crctable[index[3 - i]];
  158. ht ^= snum >>> ((3 - i) * 8);
  159. }
  160. for (i = 0; i < 100000000; i++) {
  161. lastindex = crc32lastindex(i);
  162. if (lastindex == index[3]) {
  163. deepCheckData = deepCheck(i, index)
  164. if (deepCheckData[0])
  165. break;
  166. }
  167. }
  168.  
  169. if (i == 100000000)
  170. return -1;
  171. return i + '' + deepCheckData[1];
  172. };
  173.  
  174. // 批量转换函数
  175. const batchConvert = function (hashArray) {
  176. return hashArray.map(function (hash) {
  177. return singleConvert(hash);
  178. });
  179. };
  180.  
  181. return {
  182. singleConvert: singleConvert, // 单次转换
  183. batchConvert: batchConvert // 批量转换
  184. };
  185. };
  186. })();
  187.  
  188. (function () {
  189. 'use strict';
  190.  
  191. // ==================== 油猴脚本主逻辑 ====================
  192. // 创建UI界面
  193. function createUI() {
  194. const style = `
  195. <style>
  196. .bili-parser-container {
  197. position: fixed;
  198. top: 70px;
  199. right: 20px;
  200. z-index: 9999;
  201. background: white;
  202. padding: 20px;
  203. border-radius: 10px;
  204. box-shadow: 0 0 10px rgba(0,0,0,0.2);
  205. width: 300px;
  206. cursor: default;
  207. transition: transform 0.1s ease-out; /* 平滑复位效果 */
  208. will-change: transform; /* 提前声明变化属性 */
  209. }
  210. .bili-parser-header {
  211. cursor: move;
  212. padding: 10px 0;
  213. margin: -10px 0 10px;
  214. border-bottom: 1px solid #eee;
  215. }
  216. #resultContainer {
  217. max-height: 400px; /* 设置最大高度 */
  218. overflow-y: auto; /* 添加垂直滚动条 */
  219. margin-top: 0;
  220. display: none;
  221. }
  222. .bili-input {
  223. width: 100%;
  224. padding: 8px 0;
  225. margin: 0 0 8px;
  226. border: 1px solid #ddd;
  227. box-sizing: border-box; /* 确保宽度包括内边距和边框 */
  228. }
  229. #keywordInput.bili-input {
  230. padding-left: 8px;
  231. padding-right: 8px;
  232. }
  233. .bili-btn {
  234. background: #00a1d6;
  235. color: white;
  236. border: none;
  237. padding: 8px 15px;
  238. cursor: pointer;
  239. width: 100%;
  240. box-sizing: border-box; /* 确保宽度包括内边距和边框 */
  241. }
  242. .result-table {
  243. width: 100%;
  244. border-collapse: collapse;
  245. display: block;
  246. }
  247. .result-table td, .result-table th {
  248. border: 1px solid #ddd;
  249. padding: 8px;
  250. font-size: 12px;
  251. }
  252. </style>
  253. `;
  254.  
  255. const html = `
  256. <div class="bili-parser-container">
  257. <div class="bili-parser-header"><h3>B站弹幕查询器 <span style="float: right;">By: @PyHaoCoder</span></h3></div>
  258. <input type="text" class="bili-input" id="keywordInput" placeholder="输入要查找的关键字">
  259. <button class="bili-btn" id="startSearch">开始搜索</button>
  260. <div id="resultContainer"></div>
  261. </div>
  262. `;
  263.  
  264. document.body.insertAdjacentHTML('afterbegin', style + html);
  265.  
  266. // 添加悬浮窗移动功能
  267. addDragFunctionality();
  268. }
  269.  
  270. // ==================== 添加悬浮窗移动功能 ====================
  271. function addDragFunctionality() {
  272. const container = document.querySelector('.bili-parser-container');
  273. const header = document.querySelector('.bili-parser-header');
  274.  
  275. let isDragging = false;
  276. let startX, startY, initialX, initialY;
  277.  
  278. header.addEventListener('mousedown', (e) => {
  279. isDragging = true;
  280. startX = e.clientX;
  281. startY = e.clientY;
  282. initialX = container.offsetLeft;
  283. initialY = container.offsetTop;
  284.  
  285. // 防止文本选中
  286. document.body.style.userSelect = 'none';
  287. document.body.style.webkitUserSelect = 'none';
  288. });
  289.  
  290. document.addEventListener('mousemove', (e) => {
  291. if (!isDragging) return;
  292.  
  293. const deltaX = e.clientX - startX;
  294. const deltaY = e.clientY - startY;
  295.  
  296. // 计算新位置(限制在窗口范围内)
  297. const newX = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, initialX + deltaX));
  298. const newY = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, initialY + deltaY));
  299.  
  300. container.style.left = `${newX}px`;
  301. container.style.right = 'auto';
  302. container.style.top = `${newY}px`;
  303. });
  304.  
  305. document.addEventListener('mouseup', () => {
  306. isDragging = false;
  307. document.body.style.userSelect = '';
  308. document.body.style.webkitUserSelect = '';
  309. });
  310. }
  311.  
  312. // 获取视频CID
  313. function getVideoCID() {
  314. if (window._cid) {
  315. return window._cid
  316. }
  317. }
  318.  
  319. // 显示结果
  320. function showResults(comments) {
  321. const container = document.getElementById('resultContainer');
  322. let html = `
  323. <table class="result-table">
  324. <tr>
  325. <th>用户MID</th>
  326. <th>时间</th>
  327. <th>内容</th>
  328. </tr>
  329. `;
  330.  
  331. // 显示所有数据
  332. comments.forEach(comment => {
  333. // 如果弹幕长度超过 40,则截断并添加“...”
  334. const text = comment.text.length > 40 ? comment.text.substring(0, 40) + '...' : comment.text;
  335. html += `
  336. <tr>
  337. <td><a href="https://space.bilibili.com/${comment.mid}" target="_blank">${comment.mid}</a></td>
  338. <td>${comment.date}</td>
  339. <td>${text}</td>
  340. </tr>
  341. `;
  342. });
  343.  
  344. html += '</table>';
  345.  
  346. // 添加“共 n 条数据”提示
  347. html += `
  348. <div style="margin-top: 10px; color: #666; text-align: center;">
  349. ${comments.length} 条数据
  350. </div>
  351. `;
  352.  
  353. container.innerHTML = html;
  354. container.style.marginTop = "10px"
  355. container.style.display = "block"
  356.  
  357. // 动态调整表格高度和滚动条
  358. const table = container.querySelector('.result-table');
  359. if (table) {
  360. if (table.scrollHeight > 280) {
  361. table.style.maxHeight = '280px';
  362. table.style.overflowY = 'auto'; // 添加垂直滚动条
  363. } else {
  364. table.style.maxHeight = 'none';
  365. table.style.overflowY = 'visible';
  366. }
  367. }
  368. }
  369.  
  370. // 主逻辑
  371. async function main(keyword) {
  372. const cid = getVideoCID();
  373. if (!cid) {
  374. alert('获取视频信息失败,请刷新页面重试');
  375. return;
  376. } else {
  377. console.log(`开始解析:https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`)
  378. }
  379.  
  380. GM_xmlhttpRequest({
  381. method: 'GET',
  382. url: `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`,
  383. onload: function (response) {
  384. const parser = new DOMParser();
  385. const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
  386. const ds = xmlDoc.getElementsByTagName('d');
  387.  
  388. const comments = [];
  389. for (let d of ds) {
  390. const text = d.textContent;
  391. if (keyword && !text.includes(keyword)) continue;
  392.  
  393. const p = d.getAttribute('p').split(',');
  394. comments.push({
  395. hash: p[6],
  396. ts: parseInt(p[4]) * 1000,
  397. text: text
  398. });
  399. }
  400.  
  401. // 使用优化后的哈希转换模块
  402. const midcrc = new BiliBili_midcrc();
  403. const midBatch = midcrc.batchConvert(comments.map(comment => comment.hash))
  404.  
  405. const results = comments.map((comment, idx) => ({
  406. mid: midBatch[idx],
  407. date: new Date(comment.ts).toLocaleString(),
  408. text: comment.text
  409. })).filter(comment => comment.mid); // 过滤无效结果
  410.  
  411. console.log('解析结果:', results)
  412. showResults(results);
  413. },
  414. onerror: function (err) {
  415. alert('获取弹幕失败:' + err.statusText);
  416. }
  417. });
  418. }
  419.  
  420. // 初始化
  421. function init() {
  422. createUI();
  423.  
  424. document.getElementById('startSearch').addEventListener('click', () => {
  425. const keyword = document.getElementById('keywordInput').value.trim();
  426. main(keyword || undefined);
  427. });
  428. }
  429.  
  430. // 启动脚本
  431. init();
  432. })();