给这个脚本取个名字

自动计算B站视频列表中从当前集数到最后一集的剩余时长,并支持手动输入集数计算

目前為 2025-01-25 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name 给这个脚本取个名字
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.8
  5. // @description 自动计算B站视频列表中从当前集数到最后一集的剩余时长,并支持手动输入集数计算
  6. // @author Jian A
  7. // @match https://www.bilibili.com/video/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // 将时长从MM:SS或HH:MM:SS转换为秒
  16. function parseDuration(duration) {
  17. const cleanDuration = duration.trim().replace(/^0+/, '');
  18. const parts = cleanDuration.split(':').map(Number);
  19.  
  20. if (parts.length < 2 || parts.length > 3) {
  21. console.warn('异常时长格式:', duration);
  22. return 0;
  23. }
  24.  
  25. const isValid = parts.every((part, index) => {
  26. if (isNaN(part)) return false;
  27. if (index === 0 && parts.length === 3) return part < 24; // 小时
  28. return part < 60; // 分钟和秒钟
  29. });
  30.  
  31. if (!isValid) {
  32. console.warn('异常时长值:', duration);
  33. return 0;
  34. }
  35.  
  36. if (parts.length === 2) {
  37. return parts[0] * 60 + parts[1];
  38. } else {
  39. return parts[0] * 3600 + parts[1] * 60 + parts[2];
  40. }
  41. }
  42.  
  43. // 将总秒数转换为HH:MM:SS格式
  44. function formatDuration(totalSeconds) {
  45. const hours = Math.floor(totalSeconds / 3600);
  46. const minutes = Math.floor((totalSeconds % 3600) / 60);
  47. const seconds = totalSeconds % 60;
  48. return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  49. }
  50.  
  51. // 获取当前播放的集数
  52. function getCurrentEpisode() {
  53. const urlParams = new URLSearchParams(window.location.search);
  54. const p = urlParams.get('p');
  55. return p ? parseInt(p) : 1;
  56. }
  57.  
  58. // 主要计算逻辑
  59. function calculateTotalDuration(startPage) {
  60. const videoItems = document.querySelectorAll('.stat-item.duration');
  61. let totalSeconds = 0;
  62. let totalEpisodes = videoItems.length;
  63. let validDurations = 0;
  64.  
  65. if (totalEpisodes === 0) {
  66. console.warn('未找到视频时长元素');
  67. return {
  68. duration: '0:00:00',
  69. totalEpisodes: 0,
  70. remainingEpisodes: 0
  71. };
  72. }
  73.  
  74. startPage = Math.max(1, Math.min(startPage, totalEpisodes));
  75.  
  76. let remainingEpisodes = 0;
  77. videoItems.forEach((item, index) => {
  78. const pageNum = index + 1;
  79. if (pageNum > startPage) {
  80. const duration = item.textContent.trim();
  81. const seconds = parseDuration(duration);
  82. if (seconds > 0) {
  83. totalSeconds += seconds;
  84. remainingEpisodes++;
  85. validDurations++;
  86. } else {
  87. console.warn(`第${pageNum}集时长(无效): ${duration}`);
  88. }
  89. }
  90. });
  91.  
  92. return {
  93. duration: formatDuration(totalSeconds),
  94. totalEpisodes: totalEpisodes,
  95. remainingEpisodes: remainingEpisodes
  96. };
  97. }
  98.  
  99. // 创建控件
  100. function createControls() {
  101. const container = document.createElement('div');
  102. container.style.cssText = `
  103. display: flex;
  104. align-items: center;
  105. gap: 10px;
  106. margin-left: auto;
  107. `;
  108.  
  109. const button = document.createElement('button');
  110. button.textContent = '计算剩余时长';
  111. button.style.cssText = `
  112. padding: 5px 10px;
  113. background: #00A1D6;
  114. color: white;
  115. border: none;
  116. border-radius: 4px;
  117. cursor: pointer;
  118. font-size: 14px;
  119. transition: background 0.3s ease;
  120. `;
  121. button.addEventListener('mouseover', () => {
  122. button.style.background = '#0087B3';
  123. });
  124. button.addEventListener('mouseout', () => {
  125. button.style.background = '#00A1D6';
  126. });
  127.  
  128. const input = document.createElement('input');
  129. input.type = 'number';
  130. input.placeholder = '输入集数';
  131. input.style.cssText = `
  132. width: 80px;
  133. padding: 5px;
  134. border: 1px solid #ccc;
  135. border-radius: 4px;
  136. font-size: 14px;
  137. text-align: center;
  138. `;
  139.  
  140. const resultBox = document.createElement('div');
  141. resultBox.style.cssText = `
  142. display: none;
  143. padding: 5px 10px;
  144. background: rgba(0, 0, 0, 0.7);
  145. color: white;
  146. border-radius: 4px;
  147. font-size: 14px;
  148. line-height: 1.5;
  149. white-space: nowrap;
  150. `;
  151.  
  152. container.appendChild(input);
  153. container.appendChild(button);
  154. container.appendChild(resultBox);
  155.  
  156. const titleContainer = document.querySelector('.video-info-title');
  157. if (titleContainer) {
  158. titleContainer.style.display = 'flex';
  159. titleContainer.style.alignItems = 'center';
  160. titleContainer.appendChild(container);
  161. } else {
  162. console.warn('未找到标题容器');
  163. }
  164.  
  165. return { button, input, resultBox };
  166. }
  167.  
  168. // 更新显示
  169. function updateDisplay(resultBox, startPage) {
  170. const result = calculateTotalDuration(startPage);
  171.  
  172. if (result.totalEpisodes === 0) {
  173. resultBox.textContent = '未找到视频时长信息';
  174. } else if (startPage < 1 || startPage > result.totalEpisodes) {
  175. resultBox.textContent = '请输入有效的集数(1 到 ' + result.totalEpisodes + ')';
  176. } else if (startPage >= result.totalEpisodes) {
  177. resultBox.textContent = '🎉 恭喜你已经看完全部内容啦!';
  178. } else {
  179. resultBox.textContent = `距离撒花还有${result.remainingEpisodes}集,` +
  180. `总时长: ${result.duration} 🌸`;
  181. }
  182. resultBox.style.display = 'block';
  183. }
  184.  
  185. // 初始化
  186. function init() {
  187. const durationElements = document.querySelectorAll('.stat-item.duration');
  188. const titleContainer = document.querySelector('.video-info-title');
  189.  
  190. if (durationElements.length > 0 && titleContainer) {
  191. const { button, input, resultBox } = createControls();
  192.  
  193. // 默认从当前集数计算
  194. let currentEpisode = getCurrentEpisode();
  195. updateDisplay(resultBox, currentEpisode);
  196.  
  197. // 点击按钮时从输入框的集数计算
  198. button.addEventListener('click', () => {
  199. const startPage = parseInt(input.value) || currentEpisode;
  200. updateDisplay(resultBox, startPage);
  201. });
  202.  
  203. // 回车键触发计算
  204. input.addEventListener('keypress', (e) => {
  205. if (e.key === 'Enter') {
  206. const startPage = parseInt(input.value) || currentEpisode;
  207. updateDisplay(resultBox, startPage);
  208. }
  209. });
  210.  
  211. // URL变化时自动更新
  212. let lastUrl = location.href;
  213. new MutationObserver(() => {
  214. const url = location.href;
  215. if (url !== lastUrl) {
  216. lastUrl = url;
  217. setTimeout(() => {
  218. currentEpisode = getCurrentEpisode();
  219. input.value = ''; // 清空输入框
  220. updateDisplay(resultBox, currentEpisode);
  221. }, 1000);
  222. }
  223. }).observe(document, { subtree: true, childList: true });
  224. } else {
  225. console.warn('未找到视频时长元素或标题容器');
  226. }
  227. }
  228.  
  229. // 启动时添加延迟
  230. setTimeout(() => {
  231. if (document.readyState === 'complete') {
  232. init();
  233. } else {
  234. window.addEventListener('load', init);
  235. }
  236. }, 2000);
  237. })();

QingJ © 2025

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