B站/哔哩哔哩/bilibili多视频总时长统计

观看的哔哩哔哩视频存在多个选集时,可以方便统计多个视频的总时长。按下【Ctrl + Alt + N】开启/关闭统计面板,查看说明,访问https://github.com/Ningest/Bilibili-Video-Length-Counter

  1. // ==UserScript==
  2. // @name B站/哔哩哔哩/bilibili多视频总时长统计
  3. // @namespace https://github.com/Ningest/Bilibili-Video-Length-Counter
  4. // @version 1.0.6
  5. // @description 观看的哔哩哔哩视频存在多个选集时,可以方便统计多个视频的总时长。按下【Ctrl + Alt + N】开启/关闭统计面板,查看说明,访问https://github.com/Ningest/Bilibili-Video-Length-Counter
  6. // @author ningest
  7. // @match https://www.bilibili.com/video/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. var itemList = [];//存入对象->index索引,title课程标题,duration课程时长,scroller是否为当前播放状态
  17. var inject = false;//注入html片段状态
  18. var displayPopupState = false;//弹出层显示状态;
  19. const htmlString = `
  20. <div id="ningest_jessie1314" style="display: none; z-index: 2147483647; width: 100%;height: 100%;position: fixed;top: 0px;left: 0px; background-color: rgba(0, 0, 0, 0.2)">
  21. <div style="background-color: #fff; width: 600px; height: 100%; margin: 0 auto;padding: 5px; box-sizing: border-box;">
  22. <div style="text-align: right; width: 100%;">
  23. <button class="popup-close-btn">×</button>
  24. </div>
  25. <div style="margin-bottom: 20px;width: 100%; text-align: center;">
  26. <span id="mode_str" style="margin-right: 10px;color: #666;">未计算</span>
  27. <span style="margin-right: 10px;color: #666;">总时长</span>
  28. <span style="color: #666;">=</span>
  29. <span id="hms_str" style="margin-right: 10px;margin-left: 10px;color: #007BFF;">格式1</span>
  30. <span style="color: #666;">=</span>
  31. <span id="ms_str" style="margin-right: 10px;margin-left: 10px;color: #28A745;">格式2</span>
  32. <span style="color: #666;">=</span>
  33. <span id="s_str" style="margin-left: 10px;color: #FFA500;">格式3</span>
  34. </div>
  35. <div style="display: flex; width: 100%;">
  36. <button id="c_all_btn" style="flex-grow: 1; flex-basis: 0;" onclick="count_all()">计算全部</button>
  37. <button id="c_before_btn" style="flex-grow: 1; flex-basis: 0;" onclick="count_before()">计算之前</button>
  38. <button id="c_later_btn" style="flex-grow: 1; flex-basis: 0;" onclick="count_later()">计算之后</button>
  39. <button id="c_select_btn" style="flex-grow: 1; flex-basis: 0;" onclick="count_select()">计算选中</button>
  40. </div>
  41. <div style="display: flex; width: 100%; margin-top: 10px;">
  42. <button style="flex: 1; padding: 0 5px;" onclick="select_all()">选中全部</button>
  43. <button style="flex: 1; padding: 0 5px;" onclick="empty_select()">清除选中</button>
  44. <button style="flex: 1; padding: 0 5px;" onclick="set_scope()">选中设置范围</button>
  45. <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 0 5px;">
  46. <input id="min_num" type="number" style="width: 50%;text-align: center;" value="0"/>
  47. -
  48. <input id="max_num" type="number" style="width: 50%;text-align: center;" value="0"/>
  49. </div>
  50. </div>
  51. <div style="width: 100%;height: calc(100% - 130px); margin-top: 20px;overflow-y: auto;">
  52. <table style="width: 100%; text-align: center; border-collapse: collapse; border: 1px solid #000;">
  53. <thead>
  54. <tr>
  55. <th style="border: 1px solid #000; width: 40px;">选中</th>
  56. <th style="border: 1px solid #000; width: 40px;">序号</th>
  57. <th style="border: 1px solid #000;">标题</th>
  58. <th style="border: 1px solid #000; width: 100px;">时长</th>
  59. </tr>
  60. </thead>
  61. <tbody id="table_list">
  62. </tbody>
  63. </table>
  64. </div>
  65. </div>
  66. </div>`;
  67. var checkInterval;
  68. const cssTextString = `
  69. .popup-close-btn {
  70. width: 20px;
  71. height: 20px;
  72. border-radius: 6px;
  73. font-size: 14px;
  74. text-align: center;
  75. border-style: none;
  76. background-color: #f1f2f3;
  77. transition: background-color 0.3s ease;
  78. }
  79. .popup-close-btn:hover {
  80. background-color: #ffa6a6;
  81. }
  82. .header-info {
  83. display: flex;
  84. margin-top: 10px;
  85. margin-bottom: 0px;
  86. align-items: center;
  87. justify-content: space-between;
  88. }
  89. .video-info-duration {
  90. display: flex;
  91. font-size: 15px;
  92. color: #9499a0;
  93. margin-left: 40px;
  94. }
  95. .popup-open-btn {
  96. width: 72px;
  97. height: 24px;
  98. font-size: 13px;
  99. color: #61666D;
  100. border-style: none;
  101. border-color: #00aeec;
  102. border-width: 1px;
  103. background-color: #e3e5e7;
  104. border-radius: 12px;
  105. transition: background-color 0.3s ease, color 0.3s ease;
  106. }
  107. .popup-open-btn:hover {
  108. background-color: #e8e9ea;
  109. color: #00aeec;
  110. }
  111. `;
  112. // justify-content: flex-start / flex-end / center / space-between / space-around / space-evenly;
  113. // 显示文字“总时长”左侧的空白暂定为40,可在.video-info-duration {margin-left: 40px;}中修改
  114. window.onload=function(){
  115. init();
  116. checkInterval = setInterval(DispInit, 100);
  117. }
  118.  
  119. //初始化
  120. function init(){
  121. document.addEventListener('keydown', function(event) {
  122. // 感谢@JerryYang-30同学在此处发现的错误并帮助改正
  123. // 检查 Ctrl, Alt 和 N 键是否都被按下
  124. if (event.ctrlKey && event.altKey && (event.key === 'n' || event.code === 'KeyN')) {
  125. event.preventDefault();
  126. // displayPopupState的状态记录已转到displayPopup()、TableOpen()和TableClose()中实现
  127. displayPopup();
  128. }
  129. });
  130. console.log(
  131. '%cBilibili-Video-Length-Counter:按下【Ctrl + Alt + N】开启/关闭统计面板',
  132. 'font-size: 16px; color: #80a492;' // 设置字体大小和颜色
  133. );
  134. console.log(
  135. '%cBilibili-Video-Length-Counter:查看说明,访问https://github.com/Ningest/Bilibili-Video-Length-Counter',
  136. 'font-size: 16px; color: #80a492;' // 设置字体大小和颜色
  137. );
  138. }
  139.  
  140. // 创建显示区域元素
  141. function DispInit(){
  142. if(document.querySelector('.bili-avatar').querySelector('.bili-avatar-img.bili-avatar-face.bili-avatar-img-radius')){
  143. // 添加css样式
  144. addCssClass(cssTextString);
  145. // 找到视频栏的信息栏
  146. const header = document.querySelector('div.video-pod__header');
  147. if(header){
  148. // 创建显示区域div并添加到信息栏
  149. const headerBottom = header.querySelector('div.header-bottom');
  150. var dispDiv = document.createElement('div');
  151. dispDiv.className = 'header-info';
  152. if (headerBottom) header.insertBefore(dispDiv, headerBottom);
  153. else header.appendChild(dispDiv);
  154. // 总时长
  155. // 创建总时长div并添加到显示区域
  156. var durationDiv = document.createElement('div');
  157. durationDiv.className = 'video-info-duration';
  158. dispDiv.appendChild(durationDiv);
  159. // 读取全部时长信息
  160. const items = document.querySelectorAll('.video-pod__item');
  161. let durations = [];
  162. items.forEach((item, index) => {
  163. durations.push(item.querySelector('.stat-item.duration').textContent.trim())
  164. });
  165. // 显示总时长,这里对时长计算函数进行了改动
  166. durationDiv.innerHTML = '总时长:' + calculateTotalDuration(durations);
  167. // 按钮
  168. // 创建按钮的div
  169. var openDiv = document.createElement('div');
  170. openDiv.className = 'video-info-right';
  171. dispDiv.appendChild(openDiv);
  172. // 创建按钮
  173. var openBtn = document.createElement('button');
  174. openBtn.textContent = '详细统计';
  175. openBtn.className = 'popup-open-btn';
  176. openBtn.title = '快捷键:Ctrl+Alt+N';
  177. openDiv.appendChild(openBtn);
  178. // 添加点击事件监听器
  179. openBtn.addEventListener('click', function() {TableOpen();});
  180. // 取消计时器
  181. clearInterval(checkInterval);
  182. }
  183. }
  184. }
  185.  
  186. //用于添加css样式的函数
  187. function addCssClass(cssRules) {
  188. // 创建一个 <style> 元素
  189. let style = document.createElement('style');
  190. style.type = 'text/css';
  191. // 修改其内部的css
  192. if (style.styleSheet) style.styleSheet.cssText = cssRules;
  193. else {
  194. style.appendChild(document.createDocumentFragment());
  195. style.innerHTML = cssRules;
  196. }
  197. // 将style元素添加到head中
  198. document.head.appendChild(style);
  199. }
  200.  
  201. //是否显示弹出层
  202. function displayPopup(){
  203. // 为实现通过按钮控制,已将统计面板的打开和关闭改为独立函数
  204. if(displayPopupState) TableClose();
  205. else TableOpen();
  206. }
  207. // 打开统计面板(显示弹出层)
  208. function TableOpen(){
  209. if(!inject){
  210. // 插入统计面板html
  211. document.body.insertAdjacentHTML('beforeend', htmlString);
  212. // 添加关闭按钮的时间监听器
  213. var closeButton = document.querySelector('button.popup-close-btn');
  214. closeButton.addEventListener('click', function() {TableClose();});
  215. // 记录是否已创建
  216. inject = true;
  217. }
  218. const ningest_jessie1314 = document.getElementById("ningest_jessie1314");
  219. ningest_jessie1314.style.display = 'block'
  220. parseVideoPodItems();
  221. displayPopupState = true;
  222. }
  223. // 关闭统计面板(隐藏弹出层)
  224. function TableClose(){
  225. ningest_jessie1314.style.display = 'none'
  226. homing();
  227. displayPopupState = false;
  228. }
  229.  
  230. // 创建并插入新行到tbody中
  231. function addTableRow(checkboxValueAndSecondTdText, thirdTdText, fourthTdText ,scrolled) {
  232. // 创建新的表格行
  233. var newRow = document.createElement('tr');
  234. newRow.className = 'table_item_tr';
  235. // 创建包含复选框的第一个单元格
  236. var checkboxCell = document.createElement('td');
  237. checkboxCell.style.border = '1px solid #000';
  238. var checkbox = document.createElement('input');
  239. checkbox.type = 'checkbox';
  240. checkbox.className = 'table_item_cbox';
  241. checkbox.value = checkboxValueAndSecondTdText; // 设置复选框的值
  242. checkboxCell.appendChild(checkbox);
  243. // 创建第二个单元格,使用第一个参数作为文本内容
  244. var secondCell = document.createElement('td');
  245. secondCell.style.border = '1px solid #000';
  246. secondCell.textContent = checkboxValueAndSecondTdText;
  247. // 创建第三个和第四个单元格,分别使用第二、三个参数作为文本内容
  248. var thirdCell = document.createElement('td');
  249. thirdCell.style.border = '1px solid #000';
  250. thirdCell.textContent = thirdTdText;
  251. var fourthCell = document.createElement('td');
  252. fourthCell.style.border = '1px solid #000';
  253. fourthCell.textContent = fourthTdText;
  254. // 将所有单元格添加到新行中
  255. newRow.appendChild(checkboxCell);
  256. newRow.appendChild(secondCell);
  257. newRow.appendChild(thirdCell);
  258. newRow.appendChild(fourthCell);
  259. // 获取目标tbody元素并将新行添加进去
  260. var tableBody = document.getElementById('table_list');
  261. tableBody.appendChild(newRow);
  262. }
  263.  
  264.  
  265. //计算当前播放之前
  266. function count_before(){
  267. homing();
  268. if(itemList.length<=0){
  269. alert("该页面没解析到课程列表!")
  270. return;
  271. }
  272. let durations = [];
  273. for(let i=0; i<itemList.length; i++){
  274. let item = itemList[i];
  275. if(item.scroller){
  276. break;
  277. }
  278. durations.push(item.duration);
  279. }
  280. calculateTotalDuration(durations,"计算之前模式");
  281. }
  282.  
  283. //计算当前播放之后
  284. function count_later(){
  285. homing();
  286. if(itemList.length<=0){
  287. alert("该页面没解析到课程列表!")
  288. return;
  289. }
  290. let durations = [];
  291. let state = false;
  292. for(let i=0; i<itemList.length; i++){
  293. let item = itemList[i];
  294. if(state){
  295. durations.push(item.duration);
  296. }
  297. if(item.scroller){
  298. state = true;
  299. }
  300. }
  301. calculateTotalDuration(durations,"计算之后模式");
  302. }
  303.  
  304. //计算全部
  305. function count_all(){
  306. homing();
  307. if(itemList.length<=0){
  308. alert("该页面没解析到课程列表!")
  309. return;
  310. }
  311. let durations = [];
  312. itemList.forEach(function(item){
  313. durations.push(item.duration);
  314. })
  315. calculateTotalDuration(durations,"计算全部模式");
  316. }
  317.  
  318. //计算选中item
  319. function count_select(){
  320. homing();
  321. let durations = [];
  322. let indexs = getSelect();
  323. if(indexs.length <= 0){
  324. alert("未选择任何数据!")
  325. return;
  326. }
  327. for(let a=0; a<itemList.length; a++){
  328. for(let i=0; i<indexs.length; i++){
  329. if(itemList[a].index == indexs[i]){
  330. durations.push(itemList[a].duration);
  331. indexs.splice(i,1);
  332. break
  333. }
  334. }
  335. if(indexs.length == 0){
  336. break;
  337. }
  338. }
  339. calculateTotalDuration(durations,"计算选中模式");
  340. }
  341.  
  342. //获取选中索引数组【索引号】
  343. function getSelect(){
  344. // 创建一个空数组来保存选中的复选框的值
  345. let selectedValues = [];
  346. // 获取所有具有特定类名和类型的输入元素
  347. let checkboxes = document.querySelectorAll('input.table_item_cbox[type="checkbox"]');
  348. // 遍历这些复选框
  349. checkboxes.forEach(function(checkbox) {
  350. if (checkbox.checked) { // 检查是否被选中
  351. selectedValues.push(checkbox.value); // 将选中的值添加到数组中
  352. }
  353. });
  354. return selectedValues;
  355. }
  356.  
  357. //根据数组【索引号】设置选中item
  358. function setSelect(array){
  359. // 获取所有具有特定类名和类型的输入元素
  360. let checkboxes = document.querySelectorAll('input.table_item_cbox[type="checkbox"]');
  361. // 遍历这些复选框
  362. for(let a=0; a<checkboxes.length; a++){
  363. let item = checkboxes[a];
  364. for(let i = 0; i < array.length; i++){
  365. if(item.value == array[i]){
  366. item.checked = true;
  367. array.splice(i,1);
  368. break;
  369. }
  370. }
  371. if(array.length == 0){
  372. break;
  373. }
  374. }
  375. }
  376.  
  377. //解析课程列表信息并存入itemList数组中
  378. function parseVideoPodItems() {
  379. var tableBody = document.getElementById('table_list');
  380. tableBody.innerHTML = "";
  381. itemList = [];
  382. // 获取所有.video-pod__item元素
  383. const items = document.querySelectorAll('.video-pod__item');
  384. // 遍历每一个.video-pod__item元素
  385. items.forEach((item, index) => {
  386. // 从当前元素中提取所需的信息
  387. let title = item.querySelector('.title').getAttribute('title'); // 或者 .querySelector('.title-txt').textContent;
  388. let duration = item.querySelector('.stat-item.duration').textContent.trim();
  389. let scroller = item.getAttribute('data-scrolled') || false; // 如果没有设置data-scrolled属性,默认值为false
  390. // 将信息构造成对象并加入数组
  391. itemList.push({
  392. index: index,
  393. title: title,
  394. duration: duration,
  395. scroller: scroller === 'true'
  396. });
  397. });
  398. //解析到table中
  399. itemList.forEach(function(item){
  400. addTableRow(item.index,item.title,item.duration,item.scroller);
  401. })
  402. }
  403. //计算数组【时长】中时长的总和,并设置mode为显示的模式文本=计算全部模式,计算之前模式,计算之后模式,计算选中模式,
  404. // 添加了格式为HH:MM:SS的返回值,用于在视频信息栏中显示总时长
  405. function calculateTotalDuration(durations,mode) {
  406. // 初始化总秒数
  407. let totalSeconds = 0;
  408. // 遍历数组中的每个时长并转换为秒数后相加
  409. durations.forEach(duration => {
  410. const [minutes, seconds] = duration.split(':').map(Number);
  411. totalSeconds += minutes * 60 + seconds;
  412. });
  413. // 计算小时、分钟和秒
  414. const hours = Math.floor(totalSeconds / 3600);
  415. const minutes = Math.floor(totalSeconds / 60); // 总分钟数
  416. const seconds = totalSeconds % 60; // 剩余秒数
  417. const remainingSecondsAfterHours = totalSeconds % 3600;
  418. const minutes2 = Math.floor(remainingSecondsAfterHours / 60);
  419. // 格式化输出
  420. function padZero(num) {
  421. return num.toString().padStart(2, '0');
  422. }
  423. if (mode === undefined) {
  424. // 返回一个常用格式
  425. return `${padZero(hours)}:${padZero(minutes2)}:${padZero(seconds)}`
  426. }
  427. else{
  428. // 第一种格式:HH:MM:SS
  429. const formatHMS = `${padZero(hours)}时${padZero(minutes2)}分${padZero(seconds)}秒`;
  430. // 第二种格式:总分钟数和秒数
  431. const totalMinutes = Math.floor(totalSeconds / 60);
  432. const formatMS = `${totalMinutes}分${padZero(seconds)}秒`;
  433. // 第三种格式:总秒数
  434. const formatS = totalSeconds+"秒";
  435. document.getElementById('mode_str').textContent = mode;
  436. document.getElementById('hms_str').textContent = formatHMS;
  437. document.getElementById('ms_str').textContent = formatMS;
  438. document.getElementById('s_str').textContent = formatS;
  439. }
  440. }
  441. //总时长归零
  442. function homing(){
  443. document.getElementById('mode_str').textContent = "未计算";
  444. document.getElementById('hms_str').textContent = "格式1";
  445. document.getElementById('ms_str').textContent = "格式2";
  446. document.getElementById('s_str').textContent = "格式3";
  447. }
  448. //选中全部
  449. function select_all(){
  450. let checkboxes = document.querySelectorAll('input.table_item_cbox[type="checkbox"]');
  451. checkboxes.forEach(function(item){
  452. item.checked = true;
  453. })
  454. }
  455. //清空选中
  456. function empty_select(){
  457. let checkboxes = document.querySelectorAll('input.table_item_cbox[type="checkbox"]');
  458. checkboxes.forEach(function(item){
  459. item.checked = false;
  460. })
  461. }
  462. //设置选中范围
  463. function set_scope(){
  464. empty_select();
  465. let min = Number(document.getElementById('min_num').value);
  466. let max = Number(document.getElementById('max_num').value);
  467. if(min>max){
  468. let a = min;
  469. min = max;
  470. max = a;
  471. }
  472. if(min<0 || max >= itemList.length){
  473. alert("设置不在合法范围内!")
  474. return;
  475. }
  476. let checkboxes = document.querySelectorAll('input.table_item_cbox[type="checkbox"]');
  477. for(let i=min; i<=max;i++){
  478. checkboxes[i].checked = true;
  479. }
  480. }
  481.  
  482. // 将函数挂载到全局对象
  483. window.count_all = count_all;
  484. window.count_before = count_before;
  485. window.count_later = count_later;
  486. window.count_select = count_select;
  487. window.select_all = select_all;
  488. window.empty_select = empty_select;
  489. window.set_scope = set_scope;
  490.  
  491.  
  492.  
  493. })();
  494.  

QingJ © 2025

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