Bilibili 动态筛选

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

  1. // ==UserScript==
  2. // @name Bilibili 动态筛选
  3. // @namespace Schwi
  4. // @version 2.5
  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. ADDITIONAL_TYPE_UPOWER_LOTTERY: { key: "ADDITIONAL_TYPE_UPOWER_LOTTERY", name: "动态充电互动抽奖" },
  122. };
  123.  
  124. const STYPE = {
  125. 1: { key: 1, name: "视频更新预告" },
  126. 2: { key: 2, name: "直播预告" },
  127. };
  128.  
  129. const BUSINESS_TYPE = {
  130. 1: { key: 1, name: "直播预约抽奖" },
  131. 10: { key: 10, name: "动态互动抽奖" },
  132. 12: { key: 12, name: "充电动态互动抽奖" }
  133. }
  134.  
  135. // 添加全局变量
  136. let dynamicList = [];
  137. let collectedCount = 0;
  138. let userData = null;
  139.  
  140. // 获取用户UID
  141. const getUserData = async () => {
  142. if (!userData) {
  143. userData = (await apiRequest('https://api.bilibili.com/x/space/v2/myinfo')).data
  144. }
  145. return userData
  146. };
  147.  
  148. // 筛选按钮数据结构
  149. const defaultFilters = {
  150. // 全部: {type: "checkbox", filter: (item, input) => true },
  151. 只看自己: { type: "checkbox", filter: (item, input) => item.modules.module_author.mid === userData.profile.mid },
  152. 排除自己: { type: "checkbox", filter: (item, input) => !defaultFilters['只看自己'].filter(item, input) },
  153. 只看转发: { type: "checkbox", filter: (item, input) => item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key },
  154. 排除转发: { type: "checkbox", filter: (item, input) => !defaultFilters['只看转发'].filter(item, input) },
  155. 视频更新预告: { type: "checkbox", filter: (item, input) => (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic.additional?.reserve?.stype === 1 },
  156. 直播预告: { type: "checkbox", filter: (item, input) => (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic.additional?.reserve?.stype === 2 },
  157. 有奖预约: { type: "checkbox", filter: (item, input) => defaultFilters['直播预告'].filter(item, input) && (item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig : item).modules.module_dynamic.additional?.reserve?.desc3?.text },
  158. 互动抽奖: {
  159. type: "checkbox", filter: (item, input) =>
  160. (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)
  161. ||
  162. (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)
  163. },
  164. 充电互动抽奖: { type: "checkbox", filter: (item, input) => (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item)?.modules?.module_dynamic?.additional?.type === ADDITIONAL_TYPE.ADDITIONAL_TYPE_UPOWER_LOTTERY.key },
  165. 已参与: {
  166. type: "checkbox", filter: (item, input) => {
  167. return (defaultFilters['有奖预约'].filter(item) && item.reserve?.isFollow === 1)
  168. ||
  169. (defaultFilters['互动抽奖'].filter(item) && (item.reserveInfo?.followed && (item.reserveInfo?.need_post > 0 ? item.reserveInfo?.reposted : false)))
  170. ||
  171. (defaultFilters['充电互动抽奖'].filter(item) && (item.reserveInfo?.has_charge_right && item.reserveInfo?.participated))
  172. }
  173. },
  174. 未参与: {
  175. type: "checkbox", filter: (item, input) => {
  176. return (defaultFilters['有奖预约'].filter(item) && item.reserve?.isFollow === 0)
  177. ||
  178. (defaultFilters['互动抽奖'].filter(item) && !(item.reserveInfo?.followed && (item.reserveInfo?.need_post > 0 ? item.reserveInfo?.reposted : false)))
  179. ||
  180. (defaultFilters['充电互动抽奖'].filter(item) && !(item.reserveInfo?.has_charge_right && item.reserveInfo?.participated))
  181. }
  182. },
  183. 已开奖: { type: "checkbox", filter: (item, input) => item.reserveInfo?.lottery_result },
  184. 未开奖: { type: "checkbox", filter: (item, input) => item.reserveInfo && !item.reserveInfo.lottery_result },
  185. 我中奖的: {
  186. type: "checkbox", filter: (item, input) => {
  187. const lottery_result = item.reserveInfo?.lottery_result
  188. if (!lottery_result) {
  189. return false;
  190. }
  191. const prizeCategories = Object.keys(lottery_result);
  192. for (const category of prizeCategories) {
  193. const prizeList = lottery_result[category];
  194. if (prizeList.some(prize => prize.uid === userData.profile.mid)) {
  195. return true;
  196. }
  197. }
  198. return false;
  199. }
  200. },
  201. 未中奖: { type: "checkbox", filter: (item, input) => defaultFilters['已开奖'].filter(item, input) && !defaultFilters['我中奖的'].filter(item, input) },
  202. 搜索: {
  203. type: "text",
  204. filter: (item, input) => {
  205. const searchText = input.toLocaleUpperCase();
  206. const authorName = item.modules.module_author.name.toLocaleUpperCase();
  207. const authorMid = item.modules.module_author.mid.toString();
  208. const titleText = (dynamic.modules.module_dynamic.major?.opus?.title || dynamic.modules.module_dynamic.major?.archive?.title || '').toLocaleUpperCase();
  209. const descText = (dynamic.modules.module_dynamic.major?.opus?.summary?.text || dynamic.modules.module_dynamic.desc?.text || dynamic.modules.module_dynamic.major?.archive?.desc || '').toLocaleUpperCase();
  210.  
  211. const forwardAuthorName = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig.modules.module_author.name.toLocaleUpperCase() : '';
  212. const forwardAuthorMid = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? item.orig.modules.module_author.mid.toString() : '';
  213. const forwardDescText = item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key ? (item.orig.modules.module_dynamic.major.opus.summary.text || '').toLocaleUpperCase() : '';
  214.  
  215. return authorName.includes(searchText) || authorMid.includes(searchText) || titleText.includes(searchText) || descText.includes(searchText) ||
  216. forwardAuthorName.includes(searchText) || forwardAuthorMid.includes(searchText) || forwardDescText.includes(searchText);
  217. }
  218. },
  219. };
  220.  
  221. const typeFilters = {};
  222. let customFilters;
  223.  
  224. // 工具函数:创建 dialog
  225. function createDialog(id, title, content) {
  226. let dialog = document.createElement('div');
  227. dialog.id = id;
  228. dialog.style.position = 'fixed';
  229. dialog.style.top = '5%';
  230. dialog.style.left = '5%';
  231. dialog.style.width = '90%';
  232. dialog.style.height = '90%';
  233. dialog.style.backgroundColor = '#fff';
  234. dialog.style.border = '1px solid #ccc';
  235. dialog.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
  236. dialog.style.zIndex = '9999';
  237. dialog.style.display = 'none';
  238. dialog.style.overflow = 'hidden'; // 添加 overflow: hidden
  239.  
  240. let header = document.createElement('div');
  241. header.style.display = 'flex';
  242. header.style.justifyContent = 'space-between';
  243. header.style.alignItems = 'center';
  244. header.style.padding = '10px';
  245. header.style.borderBottom = '1px solid #ccc';
  246. header.style.backgroundColor = '#f9f9f9';
  247.  
  248. let titleElement = document.createElement('span');
  249. titleElement.textContent = title;
  250. header.appendChild(titleElement);
  251.  
  252. let closeButton = document.createElement('button');
  253. closeButton.textContent = '关闭';
  254. closeButton.style.backgroundColor = '#ff4d4f'; // 修改背景颜色为红色
  255. closeButton.style.color = '#fff'; // 修改文字颜色为白色
  256. closeButton.style.border = 'none';
  257. closeButton.style.borderRadius = '5px';
  258. closeButton.style.cursor = 'pointer';
  259. closeButton.style.padding = '5px 10px';
  260. closeButton.style.transition = 'background-color 0.3s'; // 添加过渡效果
  261. closeButton.onmouseover = () => { closeButton.style.backgroundColor = '#d93637'; } // 添加悬停效果
  262. closeButton.onmouseout = () => { closeButton.style.backgroundColor = '#ff4d4f'; } // 恢复背景颜色
  263. closeButton.onclick = () => dialog.remove();
  264. header.appendChild(closeButton);
  265.  
  266. dialog.appendChild(header);
  267.  
  268. let contentArea = document.createElement('div');
  269. contentArea.innerHTML = content;
  270. contentArea.style.padding = '10px';
  271. dialog.appendChild(contentArea);
  272.  
  273. document.body.appendChild(dialog);
  274.  
  275. return {
  276. dialog: dialog,
  277. header: header,
  278. titleElement: titleElement,
  279. closeButton: closeButton,
  280. contentArea: contentArea
  281. };
  282. }
  283.  
  284. // 创建并显示时间选择器 dialog
  285. function showTimeSelector(callback, isSelf) {
  286. let yesterday = new Date();
  287. yesterday.setDate(yesterday.getDate() - 1);
  288. let today = new Date();
  289.  
  290. let dialogContent = `<div style='padding:20px; display: flex; flex-direction: column; align-items: center;'>
  291. <label for='startDate' style='font-size: 16px; margin-bottom: 10px;'>开始时间:</label>
  292. <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;'>
  293. <label for='endDate' style='font-size: 16px; margin-bottom: 10px;'>结束时间:</label>
  294. <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;'>
  295. <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>
  296. </div>`;
  297.  
  298. const { dialog, contentArea } = createDialog('timeSelectorDialog', '选择时间', dialogContent);
  299. dialog.style.display = 'block';
  300.  
  301. contentArea.querySelector('#startTask').onclick = () => {
  302. const startDate = new Date(contentArea.querySelector('#startDate').value + ' 00:00:00').getTime() / 1000;
  303. const endDate = new Date(contentArea.querySelector('#endDate').value + ' 00:00:00').getTime() / 1000;
  304. dialog.style.display = 'none';
  305. callback(startDate, endDate, isSelf);
  306. };
  307. }
  308.  
  309. // API 请求函数
  310. async function apiRequest(url, retry = 3) {
  311. for (let attempt = 1; attempt <= retry; attempt++) {
  312. try {
  313. const response = await GM.xmlHttpRequest({
  314. method: 'GET',
  315. url: url,
  316. });
  317. const data = JSON.parse(response.responseText);
  318. return data;
  319. } catch (e) {
  320. console.error(`API ${url} 请求失败,正在重试...`, e);
  321. if (attempt === retry) {
  322. throw e;
  323. }
  324. }
  325. }
  326. }
  327.  
  328. // 显示结果 dialog
  329. function showResultsDialog() {
  330. 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()}`, '');
  331.  
  332. let gridContainer = document.createElement('div');
  333. gridContainer.style.display = 'grid';
  334. gridContainer.style.gridTemplateColumns = 'repeat(auto-fill,minmax(200px,1fr))';
  335. gridContainer.style.gap = '10px';
  336. gridContainer.style.padding = '10px';
  337. gridContainer.style.height = 'calc(90% - 50px)'; // 设置高度以启用滚动
  338. gridContainer.style.overflowY = 'auto'; // 启用垂直滚动
  339. gridContainer.style.alignContent = 'flex-start';
  340.  
  341. // 遍历 DYNAMIC_TYPE 生成 filters
  342. Object.values(DYNAMIC_TYPE).forEach(type => {
  343. if (type.filter) { // 根据 filter 判断是否纳入过滤条件
  344. if (!typeFilters[type.name]) {
  345. typeFilters[type.name] = { type: "checkbox", filter: (item, input) => item.baseType === type.key };
  346. } else {
  347. const existingFilter = typeFilters[type.name].filter;
  348. typeFilters[type.name].filter = (item, input) => existingFilter(item, input) || item.baseType === type.key;
  349. }
  350. }
  351. });
  352. // 用户自定义筛选条件
  353. customFilters = deserializeFilters(GM_getValue('customFilters', null));
  354.  
  355. const deal = (dynamicList) => {
  356. let checkedFilters = [];
  357. for (let key in defaultFilters) {
  358. const f = defaultFilters[key];
  359. const filter = filterButtonsContainer.querySelector(`#${key}`);
  360. let checkedFilter;
  361. switch (f.type) {
  362. case 'checkbox':
  363. checkedFilter = { ...f, value: filter.checked };
  364. break;
  365. case 'text':
  366. checkedFilter = { ...f, value: filter.value };
  367. break;
  368. }
  369. checkedFilters.push(checkedFilter);
  370. }
  371. for (let key in typeFilters) {
  372. const f = typeFilters[key];
  373. const filter = filterButtonsContainer.querySelector(`#${key}`);
  374. let checkedFilter;
  375. switch (f.type) {
  376. case 'checkbox':
  377. checkedFilter = { ...f, value: filter.checked };
  378. break;
  379. case 'text':
  380. checkedFilter = { ...f, value: filter.value };
  381. break;
  382. }
  383. checkedFilters.push(checkedFilter);
  384. }
  385. // 添加自定义筛选条件
  386. if (customFilters && Object.keys(customFilters).length > 0) {
  387. for (let key in customFilters) {
  388. const f = customFilters[key];
  389. const filter = filterButtonsContainer.querySelector(`#${key}`);
  390. let checkedFilter;
  391. switch (f.type) {
  392. case 'checkbox':
  393. checkedFilter = { ...f, value: filter.checked };
  394. break;
  395. case 'text':
  396. checkedFilter = { ...f, value: filter.value };
  397. break;
  398. }
  399. checkedFilters.push(checkedFilter);
  400. }
  401. }
  402. dynamicList.forEach(item => {
  403. item.display = checkedFilters.every(f => f.value ? f.filter(item, f.value) : true);
  404. });
  405. console.log(checkedFilters, dynamicList.filter(item => item.display));
  406.  
  407. // 更新标题显示筛选后的条数和总条数
  408. 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()}`;
  409.  
  410. // 重新初始化 IntersectionObserver
  411. observer.disconnect();
  412. renderedCount = 0;
  413. gridContainer.innerHTML = ''; // 清空 gridContainer 的内容
  414. renderBatch();
  415. };
  416.  
  417. // 封装生成筛选按钮的函数
  418. const createFilterButtons = (filters, dynamicList) => {
  419. let mainContainer = document.createElement('div');
  420. mainContainer.style.display = 'flex';
  421. mainContainer.style.flexWrap = 'wrap'; // 修改为换行布局
  422. mainContainer.style.width = '100%';
  423.  
  424. for (let key in filters) {
  425. let filter = filters[key];
  426. let input = document.createElement('input');
  427. input.type = filter.type;
  428. input.id = key;
  429. input.style.marginRight = '5px';
  430. // 添加边框样式
  431. if (filter.type === 'text') {
  432. input.style.border = '1px solid #ccc';
  433. input.style.padding = '5px';
  434. input.style.borderRadius = '5px';
  435. }
  436.  
  437. let label = document.createElement('label');
  438. label.htmlFor = key;
  439. label.textContent = `${key}${filter.note ? `(${filter.note})` : ''}`;
  440. label.style.display = 'flex'; // 确保 label 和 input 在同一行
  441. label.style.alignItems = 'center'; // 垂直居中对齐
  442. label.style.marginRight = '5px';
  443.  
  444. let container = document.createElement('div');
  445. container.style.display = 'flex';
  446. container.style.alignItems = 'center';
  447. container.style.marginRight = '10px';
  448.  
  449. if (['checkbox', 'radio'].includes(filter.type)) {
  450. (function (dynamicList, filter, input) {
  451. input.addEventListener('change', () => deal(dynamicList));
  452. })(dynamicList, filter, input);
  453. container.appendChild(input);
  454. container.appendChild(label);
  455. } else {
  456. let timeout;
  457. (function (dynamicList, filter, input) {
  458. input.addEventListener('input', () => {
  459. clearTimeout(timeout);
  460. timeout = setTimeout(() => deal(dynamicList), 1000); // 增加延迟处理
  461. });
  462. })(dynamicList, filter, input);
  463. container.appendChild(label);
  464. container.appendChild(input);
  465. }
  466.  
  467. mainContainer.appendChild(container);
  468. }
  469.  
  470. return mainContainer;
  471. };
  472.  
  473. // 生成筛选按钮
  474. let filterButtonsContainer = document.createElement('div');
  475. filterButtonsContainer.style.marginBottom = '10px';
  476. filterButtonsContainer.style.display = 'flex'; // 添加 flex 布局
  477. filterButtonsContainer.style.flexWrap = 'wrap'; // 添加换行
  478. filterButtonsContainer.style.gap = '10px'; // 添加间距
  479. filterButtonsContainer.style.padding = '10px';
  480. filterButtonsContainer.style.alignItems = 'center'; // 添加垂直居中对齐
  481.  
  482. filterButtonsContainer.appendChild(createFilterButtons(defaultFilters, dynamicList));
  483. filterButtonsContainer.appendChild(createFilterButtons(typeFilters, dynamicList));
  484.  
  485. // 添加自定义筛选按钮
  486. if (customFilters && Object.keys(customFilters).length > 0) {
  487. filterButtonsContainer.appendChild(createFilterButtons(customFilters, dynamicList));
  488. }
  489.  
  490. const getDescText = (dynamic, isForward) => {
  491. let titleText = dynamic.modules.module_dynamic.major?.opus?.title || dynamic.modules.module_dynamic.major?.archive?.title || ''
  492. let descText = dynamic.modules.module_dynamic.major?.opus?.summary?.text || dynamic.modules.module_dynamic.desc?.text || dynamic.modules.module_dynamic.major?.archive?.desc || ''
  493.  
  494. if (isForward) {
  495. const subDescText = getDescText(dynamic.orig)
  496. descText += `<hr />${subDescText}`
  497. }
  498.  
  499. return `${titleText ? '<h3>' + titleText + '</h3><br />' : ''}${descText}`
  500. }
  501.  
  502. const createDynamicItem = (dynamic) => {
  503. const isForward = dynamic.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key;
  504. const baseDynamic = isForward ? dynamic.orig : dynamic;
  505. const type = baseDynamic.type;
  506. const authorName = dynamic.modules.module_author.name;
  507. const mid = dynamic.modules.module_author.mid;
  508. const dynamicUrl = `https://t.bilibili.com/${dynamic.id_str}`;
  509. const jumpUrl = (mid, dynamicType) => {
  510. if (dynamicType === DYNAMIC_TYPE.DYNAMIC_TYPE_UGC_SEASON.key) {
  511. return `https://www.bilibili.com/video/av${mid}/`
  512. }
  513. if (dynamicType === DYNAMIC_TYPE.DYNAMIC_TYPE_PGC_UNION.key) {
  514. return `https://bangumi.bilibili.com/anime/${mid}`
  515. }
  516. return `https://space.bilibili.com/${mid}`
  517. }
  518.  
  519. let backgroundImage = '';
  520. if (type === DYNAMIC_TYPE.DYNAMIC_TYPE_DRAW.key) {
  521. backgroundImage = baseDynamic.modules.module_dynamic.major.opus.pics[0].url;
  522. }
  523.  
  524. let dynamicItem = document.createElement('div');
  525. dynamicItem.style.position = "relative";
  526. dynamicItem.style.border = "1px solid #ddd";
  527. dynamicItem.style.borderRadius = "10px";
  528. dynamicItem.style.overflow = "hidden";
  529. dynamicItem.style.height = "300px";
  530. dynamicItem.style.display = "flex";
  531. dynamicItem.style.flexDirection = "column";
  532. dynamicItem.style.justifyContent = "flex-start"; // 修改为 flex-start 以使内容从顶部开始
  533. dynamicItem.style.padding = "10px";
  534. dynamicItem.style.color = "#fff";
  535. dynamicItem.style.transition = "transform 0.3s, background-color 0.3s"; // 添加过渡效果
  536.  
  537. dynamicItem.onmouseover = () => {
  538. dynamicItem.style.transform = "scale(1.05)"; // 略微放大
  539. cardTitle.style.background = "rgba(0, 0, 0, 0.3)";
  540. publishTime.style.background = "rgba(0, 0, 0, 0.3)";
  541. typeComment.style.background = "rgba(0, 0, 0, 0.3)";
  542. describe.style.background = "rgba(0, 0, 0, 0.3)";
  543. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
  544. };
  545.  
  546. dynamicItem.onmouseout = () => {
  547. dynamicItem.style.transform = "scale(1)"; // 恢复原始大小
  548. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  549. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  550. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  551. describe.style.background = "rgba(0, 0, 0, 0.5)";
  552. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  553. };
  554.  
  555. // 背景图片
  556. if (backgroundImage) {
  557. const img = document.createElement('img');
  558. img.src = backgroundImage;
  559. img.loading = "lazy";
  560. img.style.position = "absolute";
  561. img.style.top = "0";
  562. img.style.left = "0";
  563. img.style.width = "100%";
  564. img.style.height = "100%";
  565. img.style.objectFit = "cover";
  566. img.style.zIndex = "-1";
  567. dynamicItem.appendChild(img);
  568. }
  569.  
  570. // 标题
  571. const cardTitle = document.createElement("div");
  572. cardTitle.style.fontWeight = "bold";
  573. cardTitle.style.textShadow = "0 2px 4px rgba(0, 0, 0, 0.8)";
  574. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  575. cardTitle.style.backdropFilter = "blur(5px)";
  576. cardTitle.style.borderRadius = "5px";
  577. cardTitle.style.padding = "5px";
  578. cardTitle.style.marginBottom = "5px";
  579. cardTitle.style.textAlign = "center";
  580.  
  581. // 创建 authorName 和原作者的 a 标签
  582. const authorLink = document.createElement('a');
  583. authorLink.href = jumpUrl(mid, type);
  584. authorLink.target = "_blank";
  585. authorLink.textContent = authorName;
  586.  
  587. let originalAuthorLink
  588. if (isForward) {
  589. originalAuthorLink = document.createElement('a');
  590. const originalMid = dynamic.orig.modules.module_author.mid;
  591. const originalType = dynamic.orig.type;
  592. originalAuthorLink.href = jumpUrl(originalMid, originalType);
  593. originalAuthorLink.target = "_blank";
  594. originalAuthorLink.textContent = dynamic.orig.modules.module_author.name;
  595. }
  596.  
  597. // 设置 cardTitle 的内容
  598. cardTitle.innerHTML = isForward ? `${authorLink.outerHTML} 转发了 ${originalAuthorLink.outerHTML} 的动态` : `${authorLink.outerHTML} 发布了动态`;
  599.  
  600. // 显示发布时间
  601. const publishTime = document.createElement("div");
  602. publishTime.style.fontSize = "12px";
  603. publishTime.style.marginTop = "2px";
  604. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  605. publishTime.style.backdropFilter = "blur(5px)";
  606. publishTime.style.borderRadius = "5px";
  607. publishTime.style.padding = "5px";
  608. publishTime.style.marginBottom = "5px";
  609. publishTime.style.textAlign = "center";
  610. publishTime.textContent = `发布时间: ${new Date(dynamic.modules.module_author.pub_ts * 1000).toLocaleString()}`;
  611.  
  612. // 显示 DYNAMIC_TYPE 对应的注释
  613. const typeComment = document.createElement("div");
  614. typeComment.style.fontSize = "12px";
  615. typeComment.style.marginTop = "2px";
  616. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  617. typeComment.style.backdropFilter = "blur(5px)";
  618. typeComment.style.borderRadius = "5px";
  619. typeComment.style.padding = "5px";
  620. typeComment.style.marginBottom = "5px";
  621. typeComment.style.textAlign = "center";
  622. typeComment.textContent = `类型: ${DYNAMIC_TYPE[dynamic.type]?.name || dynamic.type} ${isForward ? `(${DYNAMIC_TYPE[dynamic.orig.type]?.name || dynamic.orig.type})` : ''} ${(defaultFilters['有奖预约'].filter(dynamic) || defaultFilters['互动抽奖'].filter(dynamic) || defaultFilters['充电互动抽奖'].filter(dynamic)) ? defaultFilters['充电互动抽奖'].filter(dynamic) ? '🎁🔋' : '🎁' : ''}`;
  623.  
  624. // 正文
  625. const describe = document.createElement("div");
  626. describe.style.fontSize = "14px";
  627. describe.style.marginTop = "2px";
  628. describe.style.background = "rgba(0, 0, 0, 0.5)";
  629. describe.style.backdropFilter = "blur(5px)";
  630. describe.style.borderRadius = "5px";
  631. describe.style.padding = "5px";
  632. describe.style.marginBottom = "5px";
  633. describe.style.textAlign = "center";
  634. describe.style.flexGrow = "1"; // 添加 flexGrow 以使描述占据剩余空间
  635. describe.style.overflowY = "auto";
  636. describe.style.textOverflow = "ellipsis";
  637. describe.innerHTML = getDescText(dynamic, isForward); // 修改为 innerHTML 以支持 HTML 标签
  638.  
  639. const viewDetailsButton = document.createElement("a");
  640. viewDetailsButton.href = dynamicUrl;
  641. viewDetailsButton.target = "_blank";
  642. viewDetailsButton.textContent = "查看详情";
  643. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  644. viewDetailsButton.style.color = "#fff";
  645. viewDetailsButton.style.padding = "5px 10px";
  646. viewDetailsButton.style.borderRadius = "5px";
  647. viewDetailsButton.style.textDecoration = "none";
  648. viewDetailsButton.style.textAlign = "center";
  649.  
  650. dynamicItem.appendChild(cardTitle);
  651. dynamicItem.appendChild(typeComment);
  652. dynamicItem.appendChild(describe);
  653. dynamicItem.appendChild(publishTime); // 添加发布时间
  654. dynamicItem.appendChild(viewDetailsButton);
  655.  
  656. return dynamicItem;
  657. };
  658.  
  659. // 分批渲染
  660. const batchSize = 50; // 每次渲染的动态数量
  661. let renderedCount = 0;
  662.  
  663. const renderBatch = () => {
  664. const renderList = dynamicList.filter(item => item.display);
  665. for (let i = 0; i < batchSize && renderedCount < renderList.length; i++, renderedCount++) {
  666. const dynamicItem = createDynamicItem(renderList[renderedCount]);
  667. dynamicItem.style.display = renderList[renderedCount].display ? 'flex' : 'none'; // 根据 display 属性显示或隐藏
  668. gridContainer.appendChild(dynamicItem);
  669. }
  670. // 检查是否还需要继续渲染
  671. if (renderedCount < renderList.length) {
  672. observer.observe(gridContainer.lastElementChild); // 观察最后一个 dynamicItem
  673. } else {
  674. observer.disconnect(); // 如果所有动态都已渲染,停止观察
  675. }
  676. };
  677.  
  678. const observer = new IntersectionObserver((entries) => {
  679. if (entries[0].isIntersecting) {
  680. observer.unobserve(entries[0].target); // 取消对当前目标的观察
  681. renderBatch();
  682. }
  683. });
  684.  
  685. renderBatch(); // 初始渲染一批
  686.  
  687. dialog.appendChild(filterButtonsContainer);
  688. dialog.appendChild(gridContainer);
  689. dialog.style.display = 'block';
  690. }
  691.  
  692. // 主任务函数
  693. async function collectDynamic(startTime, endTime, isSelf = false) {
  694. let offset = '';
  695. dynamicList = [];
  696. collectedCount = 0;
  697. let shouldContinue = true; // 引入标志位
  698.  
  699. let { dialog, contentArea } = createDialog('progressDialog', '任务进度', `<p>已收集动态数:<span id='collectedCount'>0</span>/<span id='totalCount'>0</span></p><p>已获取最早动态时间:<span id='earliestTime'>N/A</span></p>`);
  700. dialog.style.display = 'block';
  701.  
  702. // 添加样式优化
  703. dialog.querySelector('p').style.textAlign = 'center';
  704. dialog.querySelector('p').style.fontSize = '18px';
  705. dialog.querySelector('p').style.fontWeight = 'bold';
  706. dialog.querySelector('p').style.marginTop = '20px';
  707.  
  708. await getUserData()
  709.  
  710. let shouldInclude = false;
  711. while (shouldContinue) { // 使用标志位控制循环
  712. const api = isSelf ?
  713. `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?host_mid=${userData.profile.mid}&offset=${offset}&features=itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard,onlyfansAssetsV2,forwardListHidden,ugcDelete,onlyfansQaCard,commentsNewVersion` :
  714. `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all?type=all&offset=${offset}&features=itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard,onlyfansAssetsV2,forwardListHidden,ugcDelete,onlyfansQaCard,commentsNewVersion`;
  715.  
  716. try {
  717. const data = await apiRequest(api);
  718. const items = data?.data?.items;
  719.  
  720. // 如果出错等原因导致没有,直接跳过
  721. if (!items) {
  722. continue;
  723. }
  724.  
  725. if (!shouldInclude) {
  726. shouldInclude = items.some(item => item.modules.module_author.pub_ts > 0 && item.modules.module_author.pub_ts < (endTime + 24 * 60 * 60));
  727. }
  728. for (let item of items) {
  729. if (item.type !== DYNAMIC_TYPE.DYNAMIC_TYPE_LIVE_RCMD.key) {
  730. // 直播动态可能不按时间顺序出现,不能用来判断时间要求
  731. if (item.modules.module_author.pub_ts > 0 && item.modules.module_author.pub_ts < startTime) {
  732. shouldContinue = false; // 设置标志位为 false 以结束循环
  733. }
  734. }
  735. item.baseType = item.type;
  736. if (item.type === DYNAMIC_TYPE.DYNAMIC_TYPE_FORWARD.key) {
  737. item.baseType = item.orig.type;
  738. }
  739. item.display = true;
  740.  
  741. // 如果是直播预约动态,获取预约信息
  742. let reserve = null;
  743. let reserveInfo = null;
  744. if (defaultFilters['直播预告'].filter(item)) {
  745. const rid = (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).modules.module_dynamic?.additional?.reserve?.rid;
  746. if (rid) {
  747. reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${rid}&business_type=10`)).data;
  748. }
  749. if (reserveInfo?.business_id) {
  750. const business_id = reserveInfo.business_id;
  751. const reserveRelationInfo = (await apiRequest(`https://api.bilibili.com/x/activity/up/reserve/relation/info?ids=${business_id}`)).data;
  752. reserve = reserveRelationInfo?.list[business_id];
  753. }
  754. }
  755. // 如果是互动抽奖动态,获取预约信息
  756. if (defaultFilters['互动抽奖'].filter(item)) {
  757. const id_str = (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).id_str
  758. if (id_str) {
  759. reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${id_str}&business_type=1`)).data;
  760. }
  761. }
  762. // 如果是充电互动抽奖动态,获取预约信息
  763. if (defaultFilters['充电互动抽奖'].filter(item)) {
  764. const id_str = (item.type === 'DYNAMIC_TYPE_FORWARD' ? item.orig : item).id_str
  765. if (id_str) {
  766. reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${id_str}&business_type=12`)).data;
  767. }
  768. }
  769. item.reserve = reserve;
  770. item.reserveInfo = reserveInfo;
  771.  
  772. if (shouldInclude) {
  773. dynamicList.push(item);
  774. }
  775. collectedCount++;
  776. contentArea.querySelector('#collectedCount').textContent = dynamicList.length;
  777. contentArea.querySelector('#totalCount').textContent = collectedCount;
  778. contentArea.querySelector('#earliestTime').textContent = new Date(dynamicList[dynamicList.length - 1].modules.module_author.pub_ts * 1000).toLocaleString();
  779. }
  780. offset = items[items.length - 1].id_str;
  781.  
  782. if (shouldContinue) { // 检查标志位
  783. if (!data.data.has_more) shouldContinue = false; // 没有更多数据时结束循环
  784. }
  785. } catch (e) {
  786. console.error(`Error fetching data: ${e.message}`, e);
  787. continue; // 出错时继续
  788. }
  789. }
  790. console.log(`${dynamicList.length}/${collectedCount}`);
  791. 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()}`);
  792. console.log(dynamicList);
  793. console.log(new Set(dynamicList.map(item => item.type).filter(item => item)));
  794. console.log(new Set(dynamicList.map(item => item.orig?.type).filter(item => item)));
  795.  
  796. dialog.style.display = 'none';
  797. showResultsDialog();
  798. }
  799.  
  800. // 注册菜单项
  801. GM_registerMenuCommand("检查动态", () => {
  802. showTimeSelector(collectDynamic);
  803. });
  804. GM_registerMenuCommand("只看自己动态", async () => {
  805. showTimeSelector(collectDynamic, true);
  806. });
  807. })();