Bilibili 动态筛选

Bilibili 动态筛选,快速找出感兴趣的动态

目前为 2025-02-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Bilibili 动态筛选
  3. // @namespace Schwi
  4. // @version 1.6
  5. // @description Bilibili 动态筛选,快速找出感兴趣的动态
  6. // @author Schwi
  7. // @match *://*.bilibili.com/*
  8. // @connect api.bilibili.com
  9. // @connect api.vc.bilibili.com
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @noframes
  15. // @supportURL https://github.com/cyb233/script
  16. // @icon https://www.bilibili.com/favicon.ico
  17. // @license GPL-3.0
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22.  
  23. // 将字符串转换回函数
  24. const serializeFilters = (filters) => {
  25. if (!filters) return null;
  26. for (const key in filters) {
  27. filters[key].filter = filters[key].filter.toString();
  28. }
  29. return filters;
  30. }
  31. // 将字符串转换回函数
  32. const deserializeFilters = (filters) => {
  33. if (!filters) return null;
  34. for (const key in filters) {
  35. filters[key].filter = new Function('return ' + filters[key].filter)();
  36. }
  37. return filters;
  38. }
  39.  
  40. // 初始化 自定义筛选规则,示例值:{全部: {type: "checkbox", filter: "(item, input) => true" }, ...}
  41. GM_setValue('customFilters', serializeFilters(deserializeFilters(GM_getValue('customFilters', null))));
  42.  
  43. // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/dynamic/dynamic_enum.md
  44. const DYNAMIC_TYPE = {
  45. DYNAMIC_TYPE_NONE: { key: "DYNAMIC_TYPE_NONE", name: "动态失效", filter: false },
  46. DYNAMIC_TYPE_FORWARD: { key: "DYNAMIC_TYPE_FORWARD", name: "转发", filter: false },
  47. DYNAMIC_TYPE_AV: { key: "DYNAMIC_TYPE_AV", name: "视频", filter: true },
  48. DYNAMIC_TYPE_PGC: { key: "DYNAMIC_TYPE_PGC", name: "剧集", filter: true },
  49. DYNAMIC_TYPE_COURSES: { key: "DYNAMIC_TYPE_COURSES", name: "课程", filter: true },
  50. DYNAMIC_TYPE_WORD: { key: "DYNAMIC_TYPE_WORD", name: "文本", filter: true },
  51. DYNAMIC_TYPE_DRAW: { key: "DYNAMIC_TYPE_DRAW", name: "图文", filter: true },
  52. DYNAMIC_TYPE_ARTICLE: { key: "DYNAMIC_TYPE_ARTICLE", name: "专栏", filter: true },
  53. DYNAMIC_TYPE_MUSIC: { key: "DYNAMIC_TYPE_MUSIC", name: "音乐", filter: true },
  54. DYNAMIC_TYPE_COMMON_SQUARE: { key: "DYNAMIC_TYPE_COMMON_SQUARE", name: "卡片", filter: true }, // 充电专属问答,收藏集等
  55. DYNAMIC_TYPE_COMMON_VERTICAL: { key: "DYNAMIC_TYPE_COMMON_VERTICAL", name: "竖屏", filter: true },
  56. DYNAMIC_TYPE_LIVE: { key: "DYNAMIC_TYPE_LIVE", name: "直播", filter: true },
  57. DYNAMIC_TYPE_MEDIALIST: { key: "DYNAMIC_TYPE_MEDIALIST", name: "收藏夹", filter: true },
  58. DYNAMIC_TYPE_COURSES_SEASON: { key: "DYNAMIC_TYPE_COURSES_SEASON", name: "课程合集", filter: true },
  59. DYNAMIC_TYPE_COURSES_BATCH: { key: "DYNAMIC_TYPE_COURSES_BATCH", name: "课程批次", filter: true },
  60. DYNAMIC_TYPE_AD: { key: "DYNAMIC_TYPE_AD", name: "广告", filter: true },
  61. DYNAMIC_TYPE_APPLET: { key: "DYNAMIC_TYPE_APPLET", name: "小程序", filter: true },
  62. DYNAMIC_TYPE_SUBSCRIPTION: { key: "DYNAMIC_TYPE_SUBSCRIPTION", name: "订阅", filter: true },
  63. DYNAMIC_TYPE_LIVE_RCMD: { key: "DYNAMIC_TYPE_LIVE_RCMD", name: "直播", filter: true }, // 被转发
  64. DYNAMIC_TYPE_BANNER: { key: "DYNAMIC_TYPE_BANNER", name: "横幅", filter: true },
  65. DYNAMIC_TYPE_UGC_SEASON: { key: "DYNAMIC_TYPE_UGC_SEASON", name: "合集", filter: true },
  66. DYNAMIC_TYPE_PGC_UNION: { key: "DYNAMIC_TYPE_PGC_UNION", name: "番剧影视", filter: true },
  67. DYNAMIC_TYPE_SUBSCRIPTION_NEW: { key: "DYNAMIC_TYPE_SUBSCRIPTION_NEW", name: "新订阅", filter: true },
  68. };
  69.  
  70. const MAJOR_TYPE = {
  71. MAJOR_TYPE_NONE: { key: "MAJOR_TYPE_NONE", name: "动态失效" },
  72. MAJOR_TYPE_OPUS: { key: "MAJOR_TYPE_OPUS", name: "动态" },
  73. MAJOR_TYPE_ARCHIVE: { key: "MAJOR_TYPE_ARCHIVE", name: "视频" },
  74. MAJOR_TYPE_PGC: { key: "MAJOR_TYPE_PGC", name: "番剧影视" },
  75. MAJOR_TYPE_COURSES: { key: "MAJOR_TYPE_COURSES", name: "课程" },
  76. MAJOR_TYPE_DRAW: { key: "MAJOR_TYPE_DRAW", name: "图文" },
  77. MAJOR_TYPE_ARTICLE: { key: "MAJOR_TYPE_ARTICLE", name: "专栏" },
  78. MAJOR_TYPE_MUSIC: { key: "MAJOR_TYPE_MUSIC", name: "音乐" },
  79. MAJOR_TYPE_COMMON: { key: "MAJOR_TYPE_COMMON", name: "卡片" },
  80. MAJOR_TYPE_LIVE: { key: "MAJOR_TYPE_LIVE", name: "直播" },
  81. MAJOR_TYPE_MEDIALIST: { key: "MAJOR_TYPE_MEDIALIST", name: "收藏夹" },
  82. MAJOR_TYPE_APPLET: { key: "MAJOR_TYPE_APPLET", name: "小程序" },
  83. MAJOR_TYPE_SUBSCRIPTION: { key: "MAJOR_TYPE_SUBSCRIPTION", name: "订阅" },
  84. MAJOR_TYPE_LIVE_RCMD: { key: "MAJOR_TYPE_LIVE_RCMD", name: "直播推荐" },
  85. MAJOR_TYPE_UGC_SEASON: { key: "MAJOR_TYPE_UGC_SEASON", name: "合集" },
  86. MAJOR_TYPE_SUBSCRIPTION_NEW: { key: "MAJOR_TYPE_SUBSCRIPTION_NEW", name: "新订阅" },
  87. };
  88.  
  89. const RICH_TEXT_NODE_TYPE = {
  90. RICH_TEXT_NODE_TYPE_NONE: { key: "RICH_TEXT_NODE_TYPE_NONE", name: "无效节点" },
  91. RICH_TEXT_NODE_TYPE_TEXT: { key: "RICH_TEXT_NODE_TYPE_TEXT", name: "文本" },
  92. RICH_TEXT_NODE_TYPE_AT: { key: "RICH_TEXT_NODE_TYPE_AT", name: "@用户" },
  93. RICH_TEXT_NODE_TYPE_LOTTERY: { key: "RICH_TEXT_NODE_TYPE_LOTTERY", name: "互动抽奖" },
  94. RICH_TEXT_NODE_TYPE_VOTE: { key: "RICH_TEXT_NODE_TYPE_VOTE", name: "投票" },
  95. RICH_TEXT_NODE_TYPE_TOPIC: { key: "RICH_TEXT_NODE_TYPE_TOPIC", name: "话题" },
  96. RICH_TEXT_NODE_TYPE_GOODS: { key: "RICH_TEXT_NODE_TYPE_GOODS", name: "商品链接" },
  97. RICH_TEXT_NODE_TYPE_BV: { key: "RICH_TEXT_NODE_TYPE_BV", name: "视频链接" },
  98. RICH_TEXT_NODE_TYPE_AV: { key: "RICH_TEXT_NODE_TYPE_AV", name: "视频" },
  99. RICH_TEXT_NODE_TYPE_EMOJI: { key: "RICH_TEXT_NODE_TYPE_EMOJI", name: "表情" },
  100. RICH_TEXT_NODE_TYPE_USER: { key: "RICH_TEXT_NODE_TYPE_USER", name: "用户" },
  101. RICH_TEXT_NODE_TYPE_CV: { key: "RICH_TEXT_NODE_TYPE_CV", name: "专栏" },
  102. RICH_TEXT_NODE_TYPE_VC: { key: "RICH_TEXT_NODE_TYPE_VC", name: "音频" },
  103. RICH_TEXT_NODE_TYPE_WEB: { key: "RICH_TEXT_NODE_TYPE_WEB", name: "网页链接" },
  104. RICH_TEXT_NODE_TYPE_TAOBAO: { key: "RICH_TEXT_NODE_TYPE_TAOBAO", name: "淘宝链接" },
  105. RICH_TEXT_NODE_TYPE_MAIL: { key: "RICH_TEXT_NODE_TYPE_MAIL", name: "邮箱地址" },
  106. RICH_TEXT_NODE_TYPE_OGV_SEASON: { key: "RICH_TEXT_NODE_TYPE_OGV_SEASON", name: "剧集信息" },
  107. RICH_TEXT_NODE_TYPE_OGV_EP: { key: "RICH_TEXT_NODE_TYPE_OGV_EP", name: "剧集" },
  108. RICH_TEXT_NODE_TYPE_SEARCH_WORD: { key: "RICH_TEXT_NODE_TYPE_SEARCH_WORD", name: "搜索词" },
  109. };
  110.  
  111. const ADDITIONAL_TYPE = {
  112. ADDITIONAL_TYPE_NONE: { key: "ADDITIONAL_TYPE_NONE", name: "无附加类型" },
  113. ADDITIONAL_TYPE_PGC: { key: "ADDITIONAL_TYPE_PGC", name: "番剧影视" },
  114. ADDITIONAL_TYPE_GOODS: { key: "ADDITIONAL_TYPE_GOODS", name: "商品信息" },
  115. ADDITIONAL_TYPE_VOTE: { key: "ADDITIONAL_TYPE_VOTE", name: "投票" },
  116. ADDITIONAL_TYPE_COMMON: { key: "ADDITIONAL_TYPE_COMMON", name: "一般类型" },
  117. ADDITIONAL_TYPE_MATCH: { key: "ADDITIONAL_TYPE_MATCH", name: "比赛" },
  118. ADDITIONAL_TYPE_UP_RCMD: { key: "ADDITIONAL_TYPE_UP_RCMD", name: "UP主推荐" },
  119. ADDITIONAL_TYPE_UGC: { key: "ADDITIONAL_TYPE_UGC", name: "视频跳转" },
  120. ADDITIONAL_TYPE_RESERVE: { key: "ADDITIONAL_TYPE_RESERVE", name: "直播预约" },
  121. };
  122.  
  123. const STYPE = {
  124. 1: { key: 1, name: "视频更新预告" },
  125. 2: { key: 2, name: "直播预告" },
  126. };
  127.  
  128. // 添加全局变量
  129. let dynamicList = [];
  130. let collectedCount = 0;
  131.  
  132. // 筛选按钮数据结构
  133. const filters1 = {
  134. // 全部: {type: "checkbox", filter: (item, input) => true },
  135. 只看自己: { type: "checkbox", filter: (item, input) => item.modules.module_author.following === null },
  136. 排除自己: { type: "checkbox", filter: (item, input) => !filters1['只看自己'].filter(item, input) },
  137. 只看转发: { type: "checkbox", filter: (item, input) => item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key },
  138. 排除转发: { type: "checkbox", filter: (item, input) => !filters1['只看转发'].filter(item, input) },
  139. 视频更新预告: { type: "checkbox", filter: (item, input) => (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic.additional?.reserve?.stype === 1 },
  140. 直播预告: { type: "checkbox", filter: (item, input) => (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic.additional?.reserve?.stype === 2 },
  141. 有奖预约: { type: "checkbox", filter: (item, input) => filters1['直播预告'].filter(item, input) && (item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig : item).modules.module_dynamic.additional?.reserve?.desc3?.text },
  142. 互动抽奖: {
  143. type: "checkbox", filter: (item, input) =>
  144. (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item)?.modules?.module_dynamic?.major?.opus?.summary?.rich_text_nodes?.some(n => n?.type === RICH_TEXT_NODE_TYPE.RICH_TEXT_NODE_TYPE_LOTTERY.key)
  145. ||
  146. (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item)?.modules?.module_dynamic?.desc?.rich_text_nodes?.some(n => n?.type === RICH_TEXT_NODE_TYPE.RICH_TEXT_NODE_TYPE_LOTTERY.key)
  147. },
  148. 只看已开奖: {
  149. type: "checkbox", filter: (item, input) => item.reserveInfo?.lottery_result
  150. },
  151. 排除已开奖: { type: "checkbox", filter: (item, input) => item.reserveInfo && !item.reserveInfo.lottery_result },
  152. 搜索: {
  153. type: "text",
  154. filter: (item, input) => {
  155. const searchText = input.toLocaleUpperCase();
  156. const authorName = item.modules.module_author.name.toLocaleUpperCase();
  157. const authorMid = item.modules.module_author.mid.toString();
  158. const descText = (item.modules.module_dynamic.desc?.text || '').toLocaleUpperCase();
  159. const forwardAuthorName = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig.modules.module_author.name.toLocaleUpperCase() : '';
  160. const forwardAuthorMid = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig.modules.module_author.mid.toString() : '';
  161. const forwardDescText = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? (item.orig.modules.module_dynamic.desc?.text || '').toLocaleUpperCase() : '';
  162.  
  163. return authorName.includes(searchText) || authorMid.includes(searchText) || descText.includes(searchText) ||
  164. forwardAuthorName.includes(searchText) || forwardAuthorMid.includes(searchText) || forwardDescText.includes(searchText);
  165. }
  166. },
  167. };
  168.  
  169. const filters2 = {};
  170. let customFilters;
  171.  
  172. // 工具函数:创建 dialog
  173. function createDialog(id, title, content) {
  174. let dialog = document.createElement('div');
  175. dialog.id = id;
  176. dialog.style.position = 'fixed';
  177. dialog.style.top = '5%';
  178. dialog.style.left = '5%';
  179. dialog.style.width = '90%';
  180. dialog.style.height = '90%';
  181. dialog.style.backgroundColor = '#fff';
  182. dialog.style.border = '1px solid #ccc';
  183. dialog.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
  184. dialog.style.zIndex = '9999';
  185. dialog.style.display = 'none';
  186. dialog.style.overflow = 'hidden'; // 添加 overflow: hidden
  187.  
  188. let header = document.createElement('div');
  189. header.style.display = 'flex';
  190. header.style.justifyContent = 'space-between';
  191. header.style.alignItems = 'center';
  192. header.style.padding = '10px';
  193. header.style.borderBottom = '1px solid #ccc';
  194. header.style.backgroundColor = '#f9f9f9';
  195.  
  196. let titleElement = document.createElement('span');
  197. titleElement.textContent = title;
  198. header.appendChild(titleElement);
  199.  
  200. let closeButton = document.createElement('button');
  201. closeButton.textContent = '关闭';
  202. closeButton.style.backgroundColor = '#ff4d4f'; // 修改背景颜色为红色
  203. closeButton.style.color = '#fff'; // 修改文字颜色为白色
  204. closeButton.style.border = 'none';
  205. closeButton.style.borderRadius = '5px';
  206. closeButton.style.cursor = 'pointer';
  207. closeButton.style.padding = '5px 10px';
  208. closeButton.style.transition = 'background-color 0.3s'; // 添加过渡效果
  209. closeButton.onmouseover = () => { closeButton.style.backgroundColor = '#d93637'; } // 添加悬停效果
  210. closeButton.onmouseout = () => { closeButton.style.backgroundColor = '#ff4d4f'; } // 恢复背景颜色
  211. closeButton.onclick = () => dialog.remove();
  212. header.appendChild(closeButton);
  213.  
  214. dialog.appendChild(header);
  215.  
  216. let contentArea = document.createElement('div');
  217. contentArea.innerHTML = content;
  218. contentArea.style.padding = '10px';
  219. dialog.appendChild(contentArea);
  220.  
  221. document.body.appendChild(dialog);
  222.  
  223. return {
  224. dialog: dialog,
  225. header: header,
  226. titleElement: titleElement,
  227. closeButton: closeButton,
  228. contentArea: contentArea
  229. };
  230. }
  231.  
  232. // 创建并显示时间选择器 dialog
  233. function showTimeSelector(callback) {
  234. let yesterday = new Date();
  235. yesterday.setDate(yesterday.getDate() - 1);
  236. let today = new Date();
  237.  
  238. let dialogContent = `<div style='padding:20px; display: flex; flex-direction: column; align-items: center;'>
  239. <label for='startDate' style='font-size: 16px; margin-bottom: 10px;'>开始时间:</label>
  240. <input type='date' id='startDate' value='${yesterday.getFullYear()}-${(yesterday.getMonth() + 1) < 10 ? '0' + (yesterday.getMonth() + 1) : (yesterday.getMonth() + 1)}-${yesterday.getDate() < 10 ? '0' + yesterday.getDate() : yesterday.getDate()}' style='margin-bottom: 20px; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px;'>
  241. <label for='endDate' style='font-size: 16px; margin-bottom: 10px;'>结束时间:</label>
  242. <input type='date' id='endDate' value='${today.getFullYear()}-${(today.getMonth() + 1) < 10 ? '0' + (today.getMonth() + 1) : (today.getMonth() + 1)}-${today.getDate() < 10 ? '0' + today.getDate() : today.getDate()}' style='margin-bottom: 20px; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 5px;'>
  243. <button id='startTask' style='padding: 10px 20px; font-size: 16px; background-color: #00a1d6; color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s;'>开始</button>
  244. </div>`;
  245.  
  246. const { dialog, contentArea } = createDialog('timeSelectorDialog', '选择时间', dialogContent);
  247. dialog.style.display = 'block';
  248.  
  249. contentArea.querySelector('#startTask').onclick = () => {
  250. const startDate = new Date(contentArea.querySelector('#startDate').value + ' 00:00:00').getTime() / 1000;
  251. const endDate = new Date(contentArea.querySelector('#endDate').value + ' 00:00:00').getTime() / 1000;
  252. dialog.style.display = 'none';
  253. callback(startDate, endDate);
  254. };
  255. }
  256.  
  257. // API 请求函数
  258. function apiRequest(url) {
  259. return new Promise((resolve, reject) => {
  260. GM_xmlhttpRequest({
  261. method: 'GET',
  262. url: url,
  263. onload: response => {
  264. try {
  265. const data = JSON.parse(response.responseText);
  266. resolve(data);
  267. } catch (e) {
  268. reject(e);
  269. }
  270. },
  271. onerror: reject
  272. });
  273. });
  274. }
  275.  
  276. // 显示结果 dialog
  277. function showResultsDialog() {
  278. const { dialog, titleElement } = createDialog('resultsDialog', `动态结果(${dynamicList.length}/${dynamicList.length}) ${new Date(dynamicList[dynamicList.length - 1].modules.module_author.pub_ts * 1000).toLocaleString()} ~ ${new Date(dynamicList[0].modules.module_author.pub_ts * 1000).toLocaleString()}`, '');
  279.  
  280. let gridContainer = document.createElement('div');
  281. gridContainer.style.display = 'grid';
  282. gridContainer.style.gridTemplateColumns = 'repeat(auto-fill,minmax(200px,1fr))';
  283. gridContainer.style.gap = '10px';
  284. gridContainer.style.padding = '10px';
  285. gridContainer.style.height = 'calc(90% - 50px)'; // 设置高度以启用滚动
  286. gridContainer.style.overflowY = 'auto'; // 启用垂直滚动
  287.  
  288. // 遍历 DYNAMIC_TYPE 生成 filters
  289. Object.values(DYNAMIC_TYPE).forEach(type => {
  290. if (type.filter) { // 根据 filter 判断是否纳入过滤条件
  291. if (!filters2[type.name]) {
  292. filters2[type.name] = { type: "checkbox", filter: (item, input) => item.baseType === type.key };
  293. } else {
  294. const existingFilter = filters2[type.name].filter;
  295. filters2[type.name].filter = (item, input) => existingFilter(item, input) || item.baseType === type.key;
  296. }
  297. }
  298. });
  299. // 用户自定义筛选条件
  300. customFilters = deserializeFilters(GM_getValue('customFilters', null));
  301.  
  302. const deal = (dynamicList) => {
  303. let checkedFilters = [];
  304. for (let key in filters1) {
  305. const f = filters1[key];
  306. const filter = filterButtonsContainer.querySelector(`#${key}`);
  307. let checkedFilter;
  308. switch (f.type) {
  309. case 'checkbox':
  310. checkedFilter = { ...f, value: filter.checked };
  311. break;
  312. case 'text':
  313. checkedFilter = { ...f, value: filter.value };
  314. break;
  315. }
  316. checkedFilters.push(checkedFilter);
  317. }
  318. for (let key in filters2) {
  319. const f = filters2[key];
  320. const filter = filterButtonsContainer.querySelector(`#${key}`);
  321. let checkedFilter;
  322. switch (f.type) {
  323. case 'checkbox':
  324. checkedFilter = { ...f, value: filter.checked };
  325. break;
  326. case 'text':
  327. checkedFilter = { ...f, value: filter.value };
  328. break;
  329. }
  330. checkedFilters.push(checkedFilter);
  331. }
  332. // 添加自定义筛选条件
  333. if (customFilters && Object.keys(customFilters).length > 0) {
  334. for (let key in customFilters) {
  335. const f = customFilters[key];
  336. const filter = filterButtonsContainer.querySelector(`#${key}`);
  337. let checkedFilter;
  338. switch (f.type) {
  339. case 'checkbox':
  340. checkedFilter = { ...f, value: filter.checked };
  341. break;
  342. case 'text':
  343. checkedFilter = { ...f, value: filter.value };
  344. break;
  345. }
  346. checkedFilters.push(checkedFilter);
  347. }
  348. }
  349. dynamicList.forEach(item => {
  350. item.display = checkedFilters.every(f => f.value ? f.filter(item, f.value) : true);
  351. });
  352. console.log(checkedFilters, dynamicList.filter(item => item.display));
  353.  
  354. // 更新标题显示筛选后的条数和总条数
  355. titleElement.textContent = `动态结果(${dynamicList.filter(item => item.display).length}/${dynamicList.length}) ${new Date(dynamicList[dynamicList.length - 1].modules.module_author.pub_ts * 1000).toLocaleString()} ~ ${new Date(dynamicList[0].modules.module_author.pub_ts * 1000).toLocaleString()}`;
  356.  
  357. // 重新初始化 IntersectionObserver
  358. observer.disconnect();
  359. renderedCount = 0;
  360. gridContainer.innerHTML = ''; // 清空 gridContainer 的内容
  361. renderBatch();
  362. };
  363.  
  364. // 封装生成筛选按钮的函数
  365. const createFilterButtons = (filters, dynamicList) => {
  366. let mainContainer = document.createElement('div');
  367. mainContainer.style.display = 'flex';
  368. mainContainer.style.flexWrap = 'wrap'; // 修改为换行布局
  369. mainContainer.style.width = '100%';
  370.  
  371. for (let key in filters) {
  372. let filter = filters[key];
  373. let input = document.createElement('input');
  374. input.type = filter.type;
  375. input.id = key;
  376. input.style.marginRight = '5px';
  377. // 添加边框样式
  378. if (filter.type === 'text') {
  379. input.style.border = '1px solid #ccc';
  380. input.style.padding = '5px';
  381. input.style.borderRadius = '5px';
  382. }
  383.  
  384. let label = document.createElement('label');
  385. label.htmlFor = key;
  386. label.textContent = key;
  387. label.style.display = 'flex'; // 确保 label 和 input 在同一行
  388. label.style.alignItems = 'center'; // 垂直居中对齐
  389. label.style.marginRight = '5px';
  390.  
  391. let container = document.createElement('div');
  392. container.style.display = 'flex';
  393. container.style.alignItems = 'center';
  394. container.style.marginRight = '10px';
  395.  
  396. if (['checkbox', 'radio'].includes(filter.type)) {
  397. (function (dynamicList, filter, input) {
  398. input.addEventListener('change', () => deal(dynamicList));
  399. })(dynamicList, filter, input);
  400. container.appendChild(input);
  401. container.appendChild(label);
  402. } else {
  403. let timeout;
  404. (function (dynamicList, filter, input) {
  405. input.addEventListener('input', () => {
  406. clearTimeout(timeout);
  407. timeout = setTimeout(() => deal(dynamicList), 1000); // 增加延迟处理
  408. });
  409. })(dynamicList, filter, input);
  410. container.appendChild(label);
  411. container.appendChild(input);
  412. }
  413.  
  414. mainContainer.appendChild(container);
  415. }
  416.  
  417. return mainContainer;
  418. };
  419.  
  420. // 生成筛选按钮
  421. let filterButtonsContainer = document.createElement('div');
  422. filterButtonsContainer.style.marginBottom = '10px';
  423. filterButtonsContainer.style.display = 'flex'; // 添加 flex 布局
  424. filterButtonsContainer.style.flexWrap = 'wrap'; // 添加换行
  425. filterButtonsContainer.style.gap = '10px'; // 添加间距
  426. filterButtonsContainer.style.padding = '10px';
  427. filterButtonsContainer.style.alignItems = 'center'; // 添加垂直居中对齐
  428.  
  429. filterButtonsContainer.appendChild(createFilterButtons(filters1, dynamicList));
  430. filterButtonsContainer.appendChild(createFilterButtons(filters2, dynamicList));
  431.  
  432. // 添加自定义筛选按钮
  433. if (customFilters && Object.keys(customFilters).length > 0) {
  434. filterButtonsContainer.appendChild(createFilterButtons(customFilters, dynamicList));
  435. }
  436.  
  437. const getDescText = (dynamic, isForward) => {
  438. let descText = dynamic.modules.module_dynamic.desc?.text || ''
  439. if (isForward) {
  440. const subDescText = getDescText(dynamic.orig)
  441. descText += `<hr />${subDescText}`
  442. }
  443.  
  444. return descText
  445. }
  446.  
  447. const createDynamicItem = (dynamic) => {
  448. const isForward = dynamic.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key;
  449. const baseDynamic = isForward ? dynamic.orig : dynamic;
  450. const type = baseDynamic.type;
  451. const authorName = dynamic.modules.module_author.name;
  452. const mid = dynamic.modules.module_author.mid;
  453. const dynamicUrl = `https://t.bilibili.com/${dynamic.id_str}`;
  454. const jumpUrl = (mid, dynamicType) => {
  455. if (dynamicType === DYNAMIC_TYPE.DYNAMIC_TYPE_UGC_SEASON.key) {
  456. return `https://www.bilibili.com/video/av${mid}/`
  457. }
  458. if (dynamicType === DYNAMIC_TYPE.DYNAMIC_TYPE_PGC_UNION.key) {
  459. return `https://bangumi.bilibili.com/anime/${mid}`
  460. }
  461. return `https://space.bilibili.com/${mid}`
  462. }
  463.  
  464. let backgroundImage = '';
  465. if (type === DYNAMIC_TYPE.DYNAMIC_TYPE_DRAW.key) {
  466. backgroundImage = baseDynamic.modules.module_dynamic.major.draw.items[0].src;
  467. }
  468.  
  469. let dynamicItem = document.createElement('div');
  470. dynamicItem.style.position = "relative";
  471. dynamicItem.style.border = "1px solid #ddd";
  472. dynamicItem.style.borderRadius = "10px";
  473. dynamicItem.style.overflow = "hidden";
  474. dynamicItem.style.height = "300px";
  475. dynamicItem.style.display = "flex";
  476. dynamicItem.style.flexDirection = "column";
  477. dynamicItem.style.justifyContent = "flex-start"; // 修改为 flex-start 以使内容从顶部开始
  478. dynamicItem.style.padding = "10px";
  479. dynamicItem.style.color = "#fff";
  480. dynamicItem.style.transition = "transform 0.3s, background-color 0.3s"; // 添加过渡效果
  481.  
  482. dynamicItem.onmouseover = () => {
  483. dynamicItem.style.transform = "scale(1.05)"; // 略微放大
  484. cardTitle.style.background = "rgba(0, 0, 0, 0.3)";
  485. publishTime.style.background = "rgba(0, 0, 0, 0.3)";
  486. typeComment.style.background = "rgba(0, 0, 0, 0.3)";
  487. describe.style.background = "rgba(0, 0, 0, 0.3)";
  488. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
  489. };
  490.  
  491. dynamicItem.onmouseout = () => {
  492. dynamicItem.style.transform = "scale(1)"; // 恢复原始大小
  493. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  494. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  495. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  496. describe.style.background = "rgba(0, 0, 0, 0.5)";
  497. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  498. };
  499.  
  500. // 背景图片
  501. if (backgroundImage) {
  502. const img = document.createElement('img');
  503. img.src = backgroundImage;
  504. img.loading = "lazy";
  505. img.style.position = "absolute";
  506. img.style.top = "0";
  507. img.style.left = "0";
  508. img.style.width = "100%";
  509. img.style.height = "100%";
  510. img.style.objectFit = "cover";
  511. img.style.zIndex = "-1";
  512. dynamicItem.appendChild(img);
  513. }
  514.  
  515. // 标题
  516. const cardTitle = document.createElement("div");
  517. cardTitle.style.fontWeight = "bold";
  518. cardTitle.style.textShadow = "0 2px 4px rgba(0, 0, 0, 0.8)";
  519. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  520. cardTitle.style.backdropFilter = "blur(5px)";
  521. cardTitle.style.borderRadius = "5px";
  522. cardTitle.style.padding = "5px";
  523. cardTitle.style.marginBottom = "5px";
  524. cardTitle.style.textAlign = "center";
  525.  
  526. // 创建 authorName 和原作者的 a 标签
  527. const authorLink = document.createElement('a');
  528. authorLink.href = jumpUrl(mid, type);
  529. authorLink.target = "_blank";
  530. authorLink.textContent = authorName;
  531.  
  532. let originalAuthorLink
  533. if (isForward) {
  534. originalAuthorLink = document.createElement('a');
  535. const originalMid = dynamic.orig.modules.module_author.mid;
  536. const originalType = dynamic.orig.type;
  537. originalAuthorLink.href = jumpUrl(originalMid, originalType);
  538. originalAuthorLink.target = "_blank";
  539. originalAuthorLink.textContent = dynamic.orig.modules.module_author.name;
  540. }
  541.  
  542. // 设置 cardTitle 的内容
  543. cardTitle.innerHTML = isForward ? `${authorLink.outerHTML} 转发了 ${originalAuthorLink.outerHTML} 的动态` : `${authorLink.outerHTML} 发布了动态`;
  544.  
  545. // 显示发布时间
  546. const publishTime = document.createElement("div");
  547. publishTime.style.fontSize = "12px";
  548. publishTime.style.marginTop = "2px";
  549. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  550. publishTime.style.backdropFilter = "blur(5px)";
  551. publishTime.style.borderRadius = "5px";
  552. publishTime.style.padding = "5px";
  553. publishTime.style.marginBottom = "5px";
  554. publishTime.style.textAlign = "center";
  555. publishTime.textContent = `发布时间: ${new Date(dynamic.modules.module_author.pub_ts * 1000).toLocaleString()}`;
  556.  
  557. // 显示 DYNAMIC_TYPE 对应的注释
  558. const typeComment = document.createElement("div");
  559. typeComment.style.fontSize = "12px";
  560. typeComment.style.marginTop = "2px";
  561. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  562. typeComment.style.backdropFilter = "blur(5px)";
  563. typeComment.style.borderRadius = "5px";
  564. typeComment.style.padding = "5px";
  565. typeComment.style.marginBottom = "5px";
  566. typeComment.style.textAlign = "center";
  567. typeComment.textContent = `类型: ${DYNAMIC_TYPE[dynamic.type]?.name || dynamic.type} ${isForward ? `(${DYNAMIC_TYPE[dynamic.orig.type]?.name || dynamic.orig.type})` : ''} ${(filters1['有奖预约'].filter(dynamic) || filters1['互动抽奖'].filter(dynamic)) ? '🎁' : ''}`;
  568.  
  569. // 正文
  570. const describe = document.createElement("div");
  571. describe.style.fontSize = "14px";
  572. describe.style.marginTop = "2px";
  573. describe.style.background = "rgba(0, 0, 0, 0.5)";
  574. describe.style.backdropFilter = "blur(5px)";
  575. describe.style.borderRadius = "5px";
  576. describe.style.padding = "5px";
  577. describe.style.marginBottom = "5px";
  578. describe.style.textAlign = "center";
  579. describe.style.flexGrow = "1"; // 添加 flexGrow 以使描述占据剩余空间
  580. describe.style.overflowY = "auto";
  581. describe.style.textOverflow = "ellipsis";
  582. describe.innerHTML = getDescText(dynamic, isForward); // 修改为 innerHTML 以支持 HTML 标签
  583.  
  584. const viewDetailsButton = document.createElement("a");
  585. viewDetailsButton.href = dynamicUrl;
  586. viewDetailsButton.target = "_blank";
  587. viewDetailsButton.textContent = "查看详情";
  588. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  589. viewDetailsButton.style.color = "#fff";
  590. viewDetailsButton.style.padding = "5px 10px";
  591. viewDetailsButton.style.borderRadius = "5px";
  592. viewDetailsButton.style.textDecoration = "none";
  593. viewDetailsButton.style.textAlign = "center";
  594.  
  595. dynamicItem.appendChild(cardTitle);
  596. dynamicItem.appendChild(typeComment);
  597. dynamicItem.appendChild(describe);
  598. dynamicItem.appendChild(publishTime); // 添加发布时间
  599. dynamicItem.appendChild(viewDetailsButton);
  600.  
  601. return dynamicItem;
  602. };
  603.  
  604. // 分批渲染
  605. const batchSize = 50; // 每次渲染的动态数量
  606. let renderedCount = 0;
  607.  
  608. const renderBatch = () => {
  609. const renderList = dynamicList.filter(item => item.display);
  610. for (let i = 0; i < batchSize && renderedCount < renderList.length; i++, renderedCount++) {
  611. const dynamicItem = createDynamicItem(renderList[renderedCount]);
  612. dynamicItem.style.display = renderList[renderedCount].display ? 'flex' : 'none'; // 根据 display 属性显示或隐藏
  613. gridContainer.appendChild(dynamicItem);
  614. }
  615. // 检查是否还需要继续渲染
  616. if (renderedCount < renderList.length) {
  617. observer.observe(gridContainer.lastElementChild); // 观察最后一个 dynamicItem
  618. } else {
  619. observer.disconnect(); // 如果所有动态都已渲染,停止观察
  620. }
  621. };
  622.  
  623. const observer = new IntersectionObserver((entries) => {
  624. if (entries[0].isIntersecting) {
  625. observer.unobserve(entries[0].target); // 取消对当前目标的观察
  626. renderBatch();
  627. }
  628. });
  629.  
  630. renderBatch(); // 初始渲染一批
  631.  
  632. dialog.appendChild(filterButtonsContainer);
  633. dialog.appendChild(gridContainer);
  634. dialog.style.display = 'block';
  635. }
  636.  
  637. // 主任务函数
  638. async function collectDynamic(startTime, endTime) {
  639. let offset = '';
  640. dynamicList = [];
  641. collectedCount = 0;
  642. let shouldContinue = true; // 引入标志位
  643.  
  644. let { dialog, contentArea } = createDialog('progressDialog', '任务进度', `<p>已收集动态数:<span id='collectedCount'>0</span>/<span id='totalCount'>0</span></p>`);
  645. dialog.style.display = 'block';
  646.  
  647. // 添加样式优化
  648. dialog.querySelector('p').style.textAlign = 'center';
  649. dialog.querySelector('p').style.fontSize = '18px';
  650. dialog.querySelector('p').style.fontWeight = 'bold';
  651. dialog.querySelector('p').style.marginTop = '20px';
  652.  
  653. let shouldInclude = false;
  654. while (shouldContinue) { // 使用标志位控制循环
  655. const api = `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all?type=all&offset=${offset}`;
  656. const data = await apiRequest(api);
  657. const items = data.data.items;
  658.  
  659. if (!shouldInclude) {
  660. shouldInclude = items.some(item => item.modules.module_author.pub_ts > 0 && item.modules.module_author.pub_ts < (endTime + 24 * 60 * 60));
  661. }
  662. for (let item of items) {
  663. if (item.type !== DYNAMIC_TYPE.DYNAMIC_TYPE_LIVE_RCMD.key) {
  664. // 直播动态可能不按时间顺序出现,不能用来判断时间要求
  665. if (item.modules.module_author.pub_ts > 0 && item.modules.module_author.pub_ts < startTime) {
  666. shouldContinue = false; // 设置标志位为 false 以结束循环
  667. }
  668. }
  669. item.baseType = item.type;
  670. if (item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key) {
  671. item.baseType = item.orig.type;
  672. }
  673. item.display = true;
  674.  
  675. // 如果是直播预约动态,获取预约信息
  676. let reserveInfo = null;
  677. if (filters1['直播预告'].filter(item)) {
  678. const rid = (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic?.additional?.reserve?.rid;
  679. if (rid) {
  680. reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${rid}&business_type=10`)).data;
  681. }
  682. }
  683. // 如果是互动抽奖动态,获取预约信息
  684. if (filters1['互动抽奖'].filter(item)) {
  685. const rid = (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).id_str
  686. if (rid) {
  687. reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${rid}&business_type=1`)).data;
  688. }
  689. }
  690. item.reserveInfo = reserveInfo;
  691.  
  692. if (shouldInclude) {
  693. dynamicList.push(item);
  694. }
  695. collectedCount++;
  696. contentArea.querySelector('#collectedCount').textContent = dynamicList.length;
  697. contentArea.querySelector('#totalCount').textContent = collectedCount;
  698. }
  699. offset = items[items.length - 1].id_str;
  700.  
  701. if (shouldContinue) { // 检查标志位
  702. if (!data.data.has_more) shouldContinue = false; // 没有更多数据时结束循环
  703. }
  704. }
  705. console.log(`${dynamicList.length}/${collectedCount}`);
  706. console.log(`${new Date(dynamicList[dynamicList.length - 1].modules.module_author.pub_ts * 1000).toLocaleString()} ~ ${new Date(dynamicList[0].modules.module_author.pub_ts * 1000).toLocaleString()}`);
  707. console.log(dynamicList);
  708. console.log(new Set(dynamicList.map(item => item.type).filter(item => item)));
  709. console.log(new Set(dynamicList.map(item => item.orig?.type).filter(item => item)));
  710.  
  711. dialog.style.display = 'none';
  712. showResultsDialog();
  713. }
  714.  
  715. // 注册(不可用)菜单项
  716. GM_registerMenuCommand("检查动态", () => {
  717. showTimeSelector(collectDynamic);
  718. });
  719. })();

QingJ © 2025

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