Bilibili 动态筛选

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

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

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

QingJ © 2025

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