Bilibili 庆会广场

Bilibili 庆会广场查询

بۇ قوليازمىنى قاچىلاش؟
ئاپتورنىڭ تەۋسىيەلىگەن قوليازمىسى

سىز بەلكىم Bilibili 动态筛选 نى ياقتۇرۇشىڭىز مۇمكىن.

بۇ قوليازمىنى قاچىلاش
  1. // ==UserScript==
  2. // @name Bilibili 庆会广场
  3. // @namespace Schwi
  4. // @version 0.2
  5. // @description Bilibili 庆会广场查询
  6. // @author Schwi
  7. // @match *://*.bilibili.com/*
  8. // @connect api.live.bilibili.com
  9. // @connect api.vc.bilibili.com
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM_registerMenuCommand
  12. // @noframes
  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. let partyList = [];
  23. let collectedCount = 0;
  24.  
  25. // 筛选按钮数据结构
  26. const defaultFilters = {
  27. // 全部: {type: "checkbox", filter: (item, input) => true },
  28. 有奖预约: { type: "checkbox", filter: (item, input) => Object.keys(item.reserveInfo).length > 0 },
  29. 普通预约: { type: "checkbox", filter: (item, input) => Object.keys(item.reserveInfo).length === 0 },
  30. 已开奖: { type: "checkbox", filter: (item, input) => item.reserveInfo?.lottery_result },
  31. 未开奖: { type: "checkbox", filter: (item, input) => item.reserveInfo && !item.reserveInfo.lottery_result },
  32. 已预约: { type: "checkbox", filter: (item, input) => item.is_subscribed === 1 },
  33. 未预约: { type: "checkbox", filter: (item, input) => item.is_subscribed === 0 },
  34. 直播中: { type: "checkbox", filter: (item, input) => item.room_info.live_status === 1 },
  35. 未开播: { type: "checkbox", filter: (item, input) => item.room_info.live_status === 0 },
  36. 搜索: {
  37. type: "text",
  38. filter: (item, input) => {
  39. const searchText = input.toLocaleUpperCase();
  40. const authorName = item.room_info.name.toLocaleUpperCase();
  41. const authorMid = item.room_info.uid.toString().toLocaleUpperCase();
  42. const titleText = item.party_title.toLocaleUpperCase();
  43. const descText = (item.party_text || '').toLocaleUpperCase();
  44.  
  45. return authorName.includes(searchText) || authorMid.includes(searchText) || titleText.includes(searchText) || descText.includes(searchText);
  46. }
  47. },
  48. };
  49.  
  50. // 工具函数:创建 dialog
  51. function createDialog(id, title, content) {
  52. let dialog = document.createElement('div');
  53. dialog.id = id;
  54. dialog.style.position = 'fixed';
  55. dialog.style.top = '5%';
  56. dialog.style.left = '5%';
  57. dialog.style.width = '90%';
  58. dialog.style.height = '90%';
  59. dialog.style.backgroundColor = '#fff';
  60. dialog.style.border = '1px solid #ccc';
  61. dialog.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
  62. dialog.style.zIndex = '9999';
  63. dialog.style.display = 'none';
  64. dialog.style.overflow = 'hidden'; // 添加 overflow: hidden
  65.  
  66. let header = document.createElement('div');
  67. header.style.display = 'flex';
  68. header.style.justifyContent = 'space-between';
  69. header.style.alignItems = 'center';
  70. header.style.padding = '10px';
  71. header.style.borderBottom = '1px solid #ccc';
  72. header.style.backgroundColor = '#f9f9f9';
  73.  
  74. let titleElement = document.createElement('span');
  75. titleElement.textContent = title;
  76. header.appendChild(titleElement);
  77.  
  78. let closeButton = document.createElement('button');
  79. closeButton.textContent = '关闭';
  80. closeButton.style.backgroundColor = '#ff4d4f'; // 修改背景颜色为红色
  81. closeButton.style.color = '#fff'; // 修改文字颜色为白色
  82. closeButton.style.border = 'none';
  83. closeButton.style.borderRadius = '5px';
  84. closeButton.style.cursor = 'pointer';
  85. closeButton.style.padding = '5px 10px';
  86. closeButton.style.transition = 'background-color 0.3s'; // 添加过渡效果
  87. closeButton.onmouseover = () => { closeButton.style.backgroundColor = '#d93637'; } // 添加悬停效果
  88. closeButton.onmouseout = () => { closeButton.style.backgroundColor = '#ff4d4f'; } // 恢复背景颜色
  89. closeButton.onclick = () => dialog.remove();
  90. header.appendChild(closeButton);
  91.  
  92. dialog.appendChild(header);
  93.  
  94. let contentArea = document.createElement('div');
  95. contentArea.innerHTML = content;
  96. contentArea.style.padding = '10px';
  97. dialog.appendChild(contentArea);
  98.  
  99. document.body.appendChild(dialog);
  100.  
  101. return {
  102. dialog: dialog,
  103. header: header,
  104. titleElement: titleElement,
  105. closeButton: closeButton,
  106. contentArea: contentArea
  107. };
  108. }
  109.  
  110. // API 请求函数
  111. async function apiRequest(url, retry = 3) {
  112. for (let attempt = 1; attempt <= retry; attempt++) {
  113. try {
  114. const response = await GM.xmlHttpRequest({
  115. method: 'GET',
  116. url: url,
  117. });
  118. const data = JSON.parse(response.responseText);
  119. return data;
  120. } catch (e) {
  121. if (attempt === retry) {
  122. throw e;
  123. }
  124. }
  125. }
  126. }
  127.  
  128. // 显示结果 dialog
  129. function showResultsDialog() {
  130. const { dialog, titleElement } = createDialog('resultsDialog', `庆会结果(${partyList.length}/${partyList.length})`, '');
  131.  
  132. let gridContainer = document.createElement('div');
  133. gridContainer.style.display = 'grid';
  134. gridContainer.style.gridTemplateColumns = 'repeat(auto-fill,minmax(200px,1fr))';
  135. gridContainer.style.gap = '10px';
  136. gridContainer.style.padding = '10px';
  137. gridContainer.style.height = 'calc(90% - 50px)'; // 设置高度以启用滚动
  138. gridContainer.style.overflowY = 'auto'; // 启用垂直滚动
  139. gridContainer.style.alignContent = 'flex-start';
  140.  
  141. const deal = (partyList) => {
  142. let checkedFilters = [];
  143. for (let key in defaultFilters) {
  144. const f = defaultFilters[key];
  145. const filter = filterButtonsContainer.querySelector(`#${key}`);
  146. let checkedFilter;
  147. switch (f.type) {
  148. case 'checkbox':
  149. checkedFilter = { ...f, value: filter.checked };
  150. break;
  151. case 'text':
  152. checkedFilter = { ...f, value: filter.value };
  153. break;
  154. }
  155. checkedFilters.push(checkedFilter);
  156. }
  157. partyList.forEach(item => {
  158. item.display = checkedFilters.every(f => f.value ? f.filter(item, f.value) : true);
  159. });
  160. console.log(checkedFilters, partyList.filter(item => item.display));
  161.  
  162. // 更新标题显示筛选后的条数和总条数
  163. titleElement.textContent = `庆会结果(${partyList.filter(item => item.display).length}/${partyList.length})`;
  164.  
  165. // 重新初始化 IntersectionObserver
  166. observer.disconnect();
  167. renderedCount = 0;
  168. gridContainer.innerHTML = ''; // 清空 gridContainer 的内容
  169. renderBatch();
  170. };
  171.  
  172. // 封装生成筛选按钮的函数
  173. const createFilterButtons = (filters, partyList) => {
  174. let mainContainer = document.createElement('div');
  175. mainContainer.style.display = 'flex';
  176. mainContainer.style.flexWrap = 'wrap'; // 修改为换行布局
  177. mainContainer.style.width = '100%';
  178.  
  179. for (let key in filters) {
  180. let filter = filters[key];
  181. let input = document.createElement('input');
  182. input.type = filter.type;
  183. input.id = key;
  184. input.style.marginRight = '5px';
  185. // 添加边框样式
  186. if (filter.type === 'text') {
  187. input.style.border = '1px solid #ccc';
  188. input.style.padding = '5px';
  189. input.style.borderRadius = '5px';
  190. }
  191.  
  192. let label = document.createElement('label');
  193. label.htmlFor = key;
  194. label.textContent = key;
  195. label.style.display = 'flex'; // 确保 label 和 input 在同一行
  196. label.style.alignItems = 'center'; // 垂直居中对齐
  197. label.style.marginRight = '5px';
  198.  
  199. let container = document.createElement('div');
  200. container.style.display = 'flex';
  201. container.style.alignItems = 'center';
  202. container.style.marginRight = '10px';
  203.  
  204. if (['checkbox', 'radio'].includes(filter.type)) {
  205. (function (partyList, filter, input) {
  206. input.addEventListener('change', () => deal(partyList));
  207. })(partyList, filter, input);
  208. container.appendChild(input);
  209. container.appendChild(label);
  210. } else {
  211. let timeout;
  212. (function (partyList, filter, input) {
  213. input.addEventListener('input', () => {
  214. clearTimeout(timeout);
  215. timeout = setTimeout(() => deal(partyList), 1000); // 增加延迟处理
  216. });
  217. })(partyList, filter, input);
  218. container.appendChild(label);
  219. container.appendChild(input);
  220. }
  221.  
  222. mainContainer.appendChild(container);
  223. }
  224.  
  225. return mainContainer;
  226. };
  227.  
  228. // 生成筛选按钮
  229. let filterButtonsContainer = document.createElement('div');
  230. filterButtonsContainer.style.marginBottom = '10px';
  231. filterButtonsContainer.style.display = 'flex'; // 添加 flex 布局
  232. filterButtonsContainer.style.flexWrap = 'wrap'; // 添加换行
  233. filterButtonsContainer.style.gap = '10px'; // 添加间距
  234. filterButtonsContainer.style.padding = '10px';
  235. filterButtonsContainer.style.alignItems = 'center'; // 添加垂直居中对齐
  236.  
  237. filterButtonsContainer.appendChild(createFilterButtons(defaultFilters, partyList));
  238.  
  239. const createPartyItem = (party) => {
  240. const authorName = party.room_info.name;
  241. const mid = party.room_info.uid;
  242. const roomId = party.room_info.room_id;
  243. const liveUrl = `https://live.bilibili.com/${roomId}`;
  244. const spaceUrl = `https://space.bilibili.com/${mid}`;
  245. const lotteryUrl = party.reserveInfo.lottery_detail_url;
  246. const isLive = party.room_info.live_status === 1;
  247.  
  248. const hasLottery = defaultFilters['有奖预约'].filter(party);
  249.  
  250. const backgroundImage = party.party_poster;
  251.  
  252. let partyItem = document.createElement('div');
  253. partyItem.style.position = "relative";
  254. partyItem.style.border = "1px solid #ddd";
  255. partyItem.style.borderRadius = "10px";
  256. partyItem.style.overflow = "hidden";
  257. partyItem.style.height = "300px";
  258. partyItem.style.display = "flex";
  259. partyItem.style.flexDirection = "column";
  260. partyItem.style.justifyContent = "flex-start"; // 修改为 flex-start 以使内容从顶部开始
  261. partyItem.style.padding = "10px";
  262. partyItem.style.color = "#fff";
  263. partyItem.style.transition = "transform 0.3s, background-color 0.3s"; // 添加过渡效果
  264.  
  265. partyItem.onmouseover = () => {
  266. partyItem.style.transform = "scale(1.05)"; // 略微放大
  267. cardTitle.style.background = "rgba(0, 0, 0, 0.3)";
  268. publishTime.style.background = "rgba(0, 0, 0, 0.3)";
  269. typeComment.style.background = "rgba(0, 0, 0, 0.3)";
  270. describe.style.background = "rgba(0, 0, 0, 0.3)";
  271. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
  272. };
  273.  
  274. partyItem.onmouseout = () => {
  275. partyItem.style.transform = "scale(1)"; // 恢复原始大小
  276. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  277. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  278. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  279. describe.style.background = "rgba(0, 0, 0, 0.5)";
  280. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  281. };
  282.  
  283. // 背景图片
  284. if (backgroundImage) {
  285. const img = document.createElement('img');
  286. img.src = backgroundImage;
  287. img.loading = "lazy";
  288. img.style.position = "absolute";
  289. img.style.top = "0";
  290. img.style.left = "0";
  291. img.style.width = "100%";
  292. img.style.height = "100%";
  293. img.style.objectFit = "cover";
  294. img.style.zIndex = "-1";
  295. partyItem.appendChild(img);
  296. }
  297.  
  298. // 标题
  299. const cardTitle = document.createElement("div");
  300. cardTitle.style.fontWeight = "bold";
  301. cardTitle.style.textShadow = "0 2px 4px rgba(0, 0, 0, 0.8)";
  302. cardTitle.style.background = "rgba(0, 0, 0, 0.5)";
  303. cardTitle.style.backdropFilter = "blur(5px)";
  304. cardTitle.style.borderRadius = "5px";
  305. cardTitle.style.padding = "5px";
  306. cardTitle.style.marginBottom = "5px";
  307. cardTitle.style.textAlign = "center";
  308.  
  309. // 设置 cardTitle 的内容
  310. cardTitle.innerHTML = party.party_title;
  311.  
  312. // 创建 authorName 的 a 标签
  313. const authorLink = document.createElement('a');
  314. authorLink.href = spaceUrl;
  315. authorLink.target = "_blank";
  316. authorLink.textContent = authorName;
  317.  
  318. const typeComment = document.createElement("div");
  319. typeComment.style.fontSize = "12px";
  320. typeComment.style.marginTop = "2px";
  321. typeComment.style.background = "rgba(0, 0, 0, 0.5)";
  322. typeComment.style.backdropFilter = "blur(5px)";
  323. typeComment.style.borderRadius = "5px";
  324. typeComment.style.padding = "5px";
  325. typeComment.style.marginBottom = "5px";
  326. typeComment.style.textAlign = "center";
  327. typeComment.innerHTML = `${authorLink.outerHTML} ${party.party_name}${hasLottery ? ' 🎁' : ''}${isLive ? ' 🎥':''}`;
  328.  
  329. // 显示预约时间
  330. const publishTime = document.createElement("div");
  331. publishTime.style.fontSize = "12px";
  332. publishTime.style.marginTop = "2px";
  333. publishTime.style.background = "rgba(0, 0, 0, 0.5)";
  334. publishTime.style.backdropFilter = "blur(5px)";
  335. publishTime.style.borderRadius = "5px";
  336. publishTime.style.padding = "5px";
  337. publishTime.style.marginBottom = "5px";
  338. publishTime.style.textAlign = "center";
  339. publishTime.textContent = `预约时间: ${new Date(party.party_date * 1000).toLocaleString()}`;
  340.  
  341. // 正文
  342. const describe = document.createElement("div");
  343. describe.style.fontSize = "14px";
  344. describe.style.marginTop = "2px";
  345. describe.style.background = "rgba(0, 0, 0, 0.5)";
  346. describe.style.backdropFilter = "blur(5px)";
  347. describe.style.borderRadius = "5px";
  348. describe.style.padding = "5px";
  349. describe.style.marginBottom = "5px";
  350. describe.style.textAlign = "center";
  351. describe.style.flexGrow = "1"; // 添加 flexGrow 以使描述占据剩余空间
  352. describe.style.overflowY = "auto";
  353. describe.style.textOverflow = "ellipsis";
  354. describe.textContent = party.party_text;
  355.  
  356. const lotteryDetailsButton = document.createElement("a");
  357. lotteryDetailsButton.href = lotteryUrl;
  358. lotteryDetailsButton.target = "_blank";
  359. lotteryDetailsButton.textContent = "预约";
  360. lotteryDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  361. lotteryDetailsButton.style.color = "#fff";
  362. lotteryDetailsButton.style.padding = "5px 10px";
  363. lotteryDetailsButton.style.marginTop = "2px";
  364. lotteryDetailsButton.style.marginBottom = "5px";
  365. lotteryDetailsButton.style.borderRadius = "5px";
  366. lotteryDetailsButton.style.textDecoration = "none";
  367. lotteryDetailsButton.style.textAlign = "center";
  368.  
  369. const viewDetailsButton = document.createElement("a");
  370. viewDetailsButton.href = liveUrl;
  371. viewDetailsButton.target = "_blank";
  372. viewDetailsButton.textContent = "直播间";
  373. viewDetailsButton.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
  374. viewDetailsButton.style.color = "#fff";
  375. viewDetailsButton.style.padding = "5px 10px";
  376. viewDetailsButton.style.marginTop = "2px";
  377. viewDetailsButton.style.marginBottom = "5px";
  378. viewDetailsButton.style.borderRadius = "5px";
  379. viewDetailsButton.style.textDecoration = "none";
  380. viewDetailsButton.style.textAlign = "center";
  381.  
  382. partyItem.appendChild(cardTitle);
  383. partyItem.appendChild(typeComment);
  384. partyItem.appendChild(describe);
  385. partyItem.appendChild(publishTime); // 添加发布时间
  386. if (hasLottery) {
  387. partyItem.appendChild(lotteryDetailsButton);
  388. }
  389. partyItem.appendChild(viewDetailsButton);
  390.  
  391. return partyItem;
  392. };
  393.  
  394. // 分批渲染
  395. const batchSize = 50; // 每次渲染的庆会数量
  396. let renderedCount = 0;
  397.  
  398. const renderBatch = () => {
  399. const renderList = partyList.filter(item => item.display);
  400. for (let i = 0; i < batchSize && renderedCount < renderList.length; i++, renderedCount++) {
  401. const partyItem = createPartyItem(renderList[renderedCount]);
  402. partyItem.style.display = renderList[renderedCount].display ? 'flex' : 'none'; // 根据 display 属性显示或隐藏
  403. gridContainer.appendChild(partyItem);
  404. }
  405. // 检查是否还需要继续渲染
  406. if (renderedCount < renderList.length) {
  407. observer.observe(gridContainer.lastElementChild); // 观察最后一个 partyItem
  408. } else {
  409. observer.disconnect(); // 如果所有庆会都已渲染,停止观察
  410. }
  411. };
  412.  
  413. const observer = new IntersectionObserver((entries) => {
  414. if (entries[0].isIntersecting) {
  415. observer.unobserve(entries[0].target); // 取消对当前目标的观察
  416. renderBatch();
  417. }
  418. });
  419.  
  420. renderBatch(); // 初始渲染一批
  421.  
  422. dialog.appendChild(filterButtonsContainer);
  423. dialog.appendChild(gridContainer);
  424. dialog.style.display = 'block';
  425. }
  426.  
  427. // 主任务函数
  428. async function collectparty() {
  429. partyList = [];
  430. collectedCount = 0;
  431. let shouldContinue = true; // 引入标志位
  432.  
  433. 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>`);
  434. dialog.style.display = 'block';
  435.  
  436. // 添加样式优化
  437. dialog.querySelector('p').style.textAlign = 'center';
  438. dialog.querySelector('p').style.fontSize = '18px';
  439. dialog.querySelector('p').style.fontWeight = 'bold';
  440. dialog.querySelector('p').style.marginTop = '20px';
  441.  
  442. let page = 1;
  443. while (shouldContinue) { // 使用标志位控制循环
  444. const api = `https://api.live.bilibili.com/xlive/general-interface/v2/party/square?page=${page++}&page_size=100`;
  445.  
  446. try {
  447. const data = await apiRequest(api);
  448. const items = data?.data?.list;
  449.  
  450. // 如果出错等原因导致没有,直接跳过
  451. if (!items) {
  452. continue;
  453. }
  454.  
  455. for (let item of items) {
  456. item.display = true;
  457.  
  458. // 获取预约信息
  459. item.reserveInfo = (await apiRequest(`https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_id=${item.sid}&business_type=10`)).data;
  460.  
  461. partyList.push(item);
  462. collectedCount++;
  463. contentArea.querySelector('#collectedCount').textContent = partyList.length;
  464. contentArea.querySelector('#totalCount').textContent = data.data.total;
  465. contentArea.querySelector('#earliestTime').textContent = new Date(partyList[partyList.length - 1].party_date * 1000).toLocaleString();
  466. }
  467.  
  468. if (shouldContinue) { // 检查标志位
  469. if (partyList.length >= data.data.total) shouldContinue = false; // 没有更多数据时结束循环
  470. }
  471. } catch (e) {
  472. console.error(`Error fetching data: ${e.message}`);
  473. continue; // 出错时继续
  474. }
  475. }
  476. console.log(`${partyList.length}/${collectedCount}`);
  477. console.log(partyList);
  478.  
  479. dialog.style.display = 'none';
  480. showResultsDialog();
  481. }
  482.  
  483. // 注册菜单项
  484. GM_registerMenuCommand("检查庆会广场", collectparty);
  485. })();