Bilibili Feed Card Rollback

Save and restore Bilibili feed card information

  1. // ==UserScript==
  2. // @name Bilibili Feed Card Rollback
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Save and restore Bilibili feed card information
  6. // @author GloryIsMine
  7. // @license MIT
  8. // @match https://www.bilibili.com/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // 最大保存记录数
  16. const MAX_HISTORY = 10;
  17. const STORAGE_KEY = 'bilibili_feed_history';
  18.  
  19. // 获取所有视频卡片信息的函数
  20. function getFeedCardInfo() {
  21. const feedCards = document.querySelectorAll('.feed-card');
  22. return Array.from(feedCards).map(card => {
  23. // 获取封面图片
  24. const coverImg = card.querySelector('.bili-video-card__cover img');
  25. const coverUrl = coverImg ? coverImg.src : '';
  26.  
  27. // 获取视频标题
  28. const titleElement = card.querySelector('.bili-video-card__info--tit');
  29. const title = titleElement ? titleElement.textContent.trim() : '';
  30.  
  31. // 获取播放次数和评论数
  32. const statsTexts = card.querySelectorAll('.bili-video-card__stats--text');
  33. const viewCount = statsTexts[0] ? statsTexts[0].textContent.trim() : '0';
  34. const commentCount = statsTexts[1] ? statsTexts[1].textContent.trim() : '0';
  35.  
  36. // 获取视频时长
  37. const durationElement = card.querySelector('.bili-video-card__stats__duration');
  38. const duration = durationElement ? durationElement.textContent.trim() : '';
  39.  
  40. // 获取UP主/频道信息
  41. const authorElement = card.querySelector('.bili-video-card__info--author');
  42. const author = authorElement ? authorElement.textContent.trim() : '';
  43.  
  44. // 获取视频链接
  45. const linkElement = card.querySelector('.bili-video-card__info--tit a');
  46. const videoUrl = linkElement ? linkElement.href : '';
  47.  
  48. // 获取inline-video元素
  49. const inlineVideoElement = card.querySelector('video');
  50. const inlineVideoUrl = inlineVideoElement ? inlineVideoElement.src : '';
  51.  
  52. return {
  53. coverUrl,
  54. title,
  55. viewCount,
  56. commentCount,
  57. duration,
  58. author,
  59. videoUrl,
  60. inlineVideoUrl
  61. };
  62. });
  63. }
  64.  
  65. // 保存feed-card信息到sessionStorage
  66. function saveFeedCards() {
  67. const currentInfo = getFeedCardInfo();
  68. let history = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '[]');
  69. // 添加新记录到开头
  70. history.unshift({
  71. timestamp: new Date().getTime(),
  72. cards: currentInfo
  73. });
  74.  
  75. // 限制历史记录数量
  76. if (history.length > MAX_HISTORY) {
  77. history = history.slice(0, MAX_HISTORY);
  78. }
  79.  
  80. sessionStorage.setItem(STORAGE_KEY, JSON.stringify(history));
  81. }
  82.  
  83. // 恢复feed-card信息
  84. function restoreFeedCards() {
  85. const history = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '[]');
  86. if (history.length === 0) {
  87. alert('没有可恢复的历史记录');
  88. return;
  89. }
  90.  
  91. const lastRecord = history[0];
  92. const feedCards = document.querySelectorAll('.feed-card');
  93. // 确保有足够的卡片可以恢复
  94. if (feedCards.length !== lastRecord.cards.length) {
  95. alert('当前页面卡片数量与历史记录不匹配,无法恢复');
  96. return;
  97. }
  98.  
  99. // 更新每个卡片的内容
  100. feedCards.forEach((card, index) => {
  101. const cardInfo = lastRecord.cards[index];
  102. // 更新封面图片(包括所有图片源)
  103. const picture = card.querySelector('.bili-video-card__cover');
  104. if (picture) {
  105. // 更新所有source标签的srcset
  106. const sources = picture.querySelectorAll('source');
  107. sources.forEach(source => {
  108. const currentSrcset = source.srcset;
  109. // 从当前srcset中提取图片格式后缀(.avif或.webp)
  110. const formatMatch = currentSrcset.match(/\.(avif|webp)/);
  111. if (formatMatch) {
  112. const format = formatMatch[0];
  113. // 构建新的srcset,保持原有的尺寸和格式
  114. const newSrcset = cardInfo.coverUrl + format;
  115. source.srcset = newSrcset;
  116. }
  117. });
  118.  
  119. // 更新img标签
  120. const img = picture.querySelector('img');
  121. if (img) {
  122. img.src = cardInfo.coverUrl;
  123. img.alt = cardInfo.title;
  124. }
  125. }
  126.  
  127. // 更新视频标题和链接
  128. const titleElements = card.querySelectorAll('.bili-video-card__info--tit a');
  129. titleElements.forEach(element => {
  130. element.textContent = cardInfo.title;
  131. element.href = cardInfo.videoUrl;
  132. element.title = cardInfo.title;
  133. });
  134.  
  135. // 更新播放次数和评论数
  136. const statsTexts = card.querySelectorAll('.bili-video-card__stats--text');
  137. if (statsTexts[0]) statsTexts[0].textContent = cardInfo.viewCount;
  138. if (statsTexts[1]) statsTexts[1].textContent = cardInfo.commentCount;
  139.  
  140. // 更新视频时长
  141. const durationElement = card.querySelector('.bili-video-card__stats__duration');
  142. if (durationElement) durationElement.textContent = cardInfo.duration;
  143.  
  144. // 更新UP主/频道信息
  145. const authorElement = card.querySelector('.bili-video-card__info--author');
  146. if (authorElement) authorElement.textContent = cardInfo.author;
  147.  
  148. // 更新所有相关的链接
  149. const imageLink = card.querySelector('.bili-video-card__image--link');
  150. if (imageLink) imageLink.href = cardInfo.videoUrl;
  151.  
  152. const inlineVideoElement = card.querySelector('video');
  153. if (inlineVideoElement) inlineVideoElement.src = cardInfo.inlineVideoUrl;
  154. });
  155.  
  156. // 移除已使用的记录
  157. history.shift();
  158. sessionStorage.setItem(STORAGE_KEY, JSON.stringify(history));
  159. }
  160.  
  161. // 创建rollback按钮
  162. function createRollbackButton() {
  163. const feedRollBtn = document.querySelector('.feed-roll-btn');
  164. if (!feedRollBtn) return;
  165.  
  166. const button = document.createElement('button');
  167. button.textContent = '回滚';
  168. button.className = 'feed-rollback-btn';
  169. button.style.cssText = `
  170. position: fixed;
  171. padding: 8px;
  172. background-color: #00a1d6;
  173. color: white;
  174. border: none;
  175. border-radius: 4px;
  176. cursor: pointer;
  177. font-size: 12px;
  178. z-index: ${getComputedStyle(feedRollBtn).zIndex};
  179. width: 54px;
  180. `;
  181. button.addEventListener('click', restoreFeedCards);
  182. // 将按钮添加到body中
  183. document.body.appendChild(button);
  184.  
  185. // 更新按钮位置的函数
  186. function updateButtonPosition() {
  187. const feedRollBtnRect = feedRollBtn.getBoundingClientRect();
  188. button.style.left = `${feedRollBtnRect.left}px`;
  189. button.style.top = `${feedRollBtnRect.bottom + 8}px`;
  190. }
  191.  
  192. // 初始化位置
  193. updateButtonPosition();
  194.  
  195. // 监听可能影响位置的事件
  196. window.addEventListener('resize', updateButtonPosition);
  197. window.addEventListener('scroll', updateButtonPosition);
  198. // 监听页面缩放
  199. window.visualViewport?.addEventListener('resize', updateButtonPosition);
  200. window.visualViewport?.addEventListener('scroll', updateButtonPosition);
  201.  
  202. // 创建MutationObserver来监听DOM变化
  203. const observer = new MutationObserver(updateButtonPosition);
  204. observer.observe(document.body, {
  205. childList: true,
  206. subtree: true,
  207. attributes: true
  208. });
  209. }
  210.  
  211. // 监听roll-btn点击事件
  212. function setupRollButtonListener() {
  213. const observer = new MutationObserver((mutations) => {
  214. const rollBtn = document.querySelector('.roll-btn');
  215. // 确保roll按钮存在且未初始化
  216. if (rollBtn && !rollBtn.dataset.rollbackInitialized) {
  217. rollBtn.dataset.rollbackInitialized = 'true';
  218. rollBtn.addEventListener('click', saveFeedCards);
  219. // 创建rollback按钮
  220. createRollbackButton();
  221. }
  222. });
  223.  
  224. observer.observe(document.body, {
  225. childList: true,
  226. subtree: true
  227. });
  228. }
  229.  
  230. // 初始化
  231. function init() {
  232. setupRollButtonListener();
  233. }
  234.  
  235. // 等待页面加载完成后初始化
  236. if (document.readyState === 'loading') {
  237. document.addEventListener('DOMContentLoaded', init);
  238. } else {
  239. init();
  240. }
  241. })();

QingJ © 2025

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