您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
查看b站合集已观看时长,单击便会更新,时长会显示在观看量后
// ==UserScript== // @name 查看b站合集已观看时长 // @version 1.7 // @license MIT // @description 查看b站合集已观看时长,单击便会更新,时长会显示在观看量后 // @author 白夜 // @match *://*.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @namespace http://greasyfork.icu/users/1486583 // ==/UserScript== (function() { 'use strict'; let haveShowFirst = false; let pageHaveShow=false; let chapterTimerHaveShow=false; let titleEl = document.querySelector('.view-text'); let titleElinner = document.querySelector('.view-text').innerText; let videoList; let activeVideo; let statItems; let chapterMap = new Map(); let isOverButtonOrPanel = false; let features; const states = {}; function parseTime(text) { const [min, sec] = text.split(':').map(Number); return min * 60 + (sec || 0); } function getAllVideo(){ videoList=document.querySelector('.video-pod__list'); if (!videoList)return false; return true; } function getActiveVideo(){ activeVideo = videoList.querySelectorAll('.active'); if (!activeVideo)return false; activeVideo=activeVideo[activeVideo.length-1] if (!activeVideo)return false; console.log(activeVideo) return true; } function getStatItems(){ let classMenu = activeVideo.parentNode; if (!classMenu) return false; let allItems = classMenu.querySelectorAll('.stats .stat-item'); if (!allItems || allItems.length === 0) return false; statItems = Array.from(allItems); try { const rules = JSON.parse(localStorage.getItem('bilibili_stat_rules') || '[]'); for (const rule of rules) { if (window.location.href.includes(rule.url)) { const startIndex = Math.max(rule.start - 1, 0); const endIndex = Math.min(rule.end, allItems.length); statItems = statItems.slice(startIndex, endIndex); console.log(`匹配规则 ${rule.url},截取 P${rule.start} ~ P${rule.end}`); break; } } } catch (e) { console.warn('规则解析失败', e); } return true; } function getAllTimer(){ let totalSeconds = 0; let toActiveSeconds = 0; let reachedActive = false; for (const stat of statItems) { const timeText = stat.textContent.trim(); const seconds = parseTime(timeText); totalSeconds += seconds; const parent = stat.parentNode.parentNode; const isHaveActive = parent.classList.contains('active'); if (isHaveActive) { reachedActive = true; } if (!reachedActive) { toActiveSeconds += seconds; } } const format = sec => { const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return [h, m, s].map(unit => String(unit).padStart(2, '0')).join(':'); }; const timerShow = ` ${format(toActiveSeconds)} / ${format(totalSeconds)}`; if (titleEl) { titleEl.innerText =titleElinner+ timerShow; } else { console.warn('未找到 h1 标签'); } } function getChapterTime(){ const reportDiv = document.querySelector('.video-toolbar-container'); let chapterDiv; if (reportDiv) { chapterDiv = document.createElement('div'); chapterDiv.className = 'ChapterTime'; reportDiv.parentNode.insertBefore(chapterDiv, reportDiv.nextSibling); } else { console.warn('未找到 .arc_toolbar_report 元素'); } chapterMap.clear(); // 清空旧数据 for (const stat of statItems) { const parent = stat.parentNode.parentNode; const titleEl = parent.firstElementChild; if (!titleEl) continue; const titleText = titleEl.innerText.trim(); const match = titleText.match(/^P?(\d+)/); if (!match) continue; const chapterNumber = match[1]; const seconds = parseTime(stat.textContent.trim()); if (!chapterMap.has(chapterNumber)) { chapterMap.set(chapterNumber, { title: "第"+chapterNumber+"章", total: 0 }); } chapterMap.get(chapterNumber).total += seconds; } const format = sec => { const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return [h, m, s].map(unit => String(unit).padStart(2, '0')).join(':'); }; let chapterStatsText = '章节统计:\n'; [...chapterMap.entries()] .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) .forEach(([chapter, data]) => { chapterStatsText += `${data.title}:${format(data.total)}\n`; }); if (titleEl) { chapterDiv.innerText = chapterStatsText; } else { console.warn('未找到标题元素用于追加章节统计'); } } function setP(){ let count = 1; for (const stat of statItems) { const parent = stat.parentNode.parentNode; const firstChild = parent.firstElementChild; // 如果已经有我们加过的标记 div,就跳过(避免重复插入) if (firstChild && firstChild.classList.contains('p-label')) continue; const label = document.createElement('div'); label.innerText = "P" + count; label.className = 'p-label'; label.style.color = '#888'; label.style.fontSize = '15px'; label.style.marginRight = '5px'; parent.insertBefore(label, firstChild); count++; } } function createControlPanel() { const toggleButton = document.createElement('div'); toggleButton.className="setPanel"; toggleButton.innerText = '⚙️'; toggleButton.style.position = 'fixed'; toggleButton.style.left = '0'; toggleButton.style.top = '50%'; toggleButton.style.transform = 'translateY(-50%)'; toggleButton.style.width = '30px'; toggleButton.style.height = '30px'; toggleButton.style.background = '#333'; toggleButton.style.color = '#fff'; toggleButton.style.display = 'flex'; toggleButton.style.alignItems = 'center'; toggleButton.style.justifyContent = 'center'; toggleButton.style.cursor = 'pointer'; toggleButton.style.zIndex = '9999'; toggleButton.style.borderRadius = '0 5px 5px 0'; document.body.appendChild(toggleButton); const panel = document.createElement('div'); panel.className="setPanel"; panel.style.position = 'fixed'; panel.style.left = '35px'; panel.style.top = '50%'; panel.style.transform = 'translateY(-50%)'; panel.style.width = '20vw'; panel.style.padding = '10px'; panel.style.background = '#fff'; panel.style.border = '1px solid #ccc'; panel.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)'; panel.style.zIndex = '9999'; panel.style.display = 'none'; const defaultFeatures = [ { key: 'showChapterTime', label: '显示章节时间', default: true }, { key: 'showTotalTimer', label: '显示总时长', default: true }, { key: 'showPageP', label: '显示页面编号', default: true } ]; let storedFeatures = localStorage.getItem('bilibili_features_config'); if (storedFeatures) { try { features = JSON.parse(storedFeatures); } catch (e) { console.warn('本地 feature 配置解析失败,使用默认配置。'); features = defaultFeatures; localStorage.setItem('bilibili_features_config', JSON.stringify(defaultFeatures)); } } else { features = defaultFeatures; localStorage.setItem('bilibili_features_config', JSON.stringify(defaultFeatures)); } features.forEach(feature => { states[feature.key] = feature.default; }); features.forEach(feature => { const saved = localStorage.getItem(`feature_${feature.key}`); states[feature.key] = saved === null ? true : saved === 'true'; const button = document.createElement('button'); button.innerText = `${feature.label}: ${states[feature.key] ? '✅开启' : '❌关闭'}`; button.style.marginBottom = '5px'; button.style.width = '100%'; button.style.padding = '5px'; button.style.cursor = 'pointer'; button.style.border = '1px solid #ccc'; button.style.borderRadius = '4px'; button.style.background = states[feature.key] ? '#d4edda' : '#f8d7da'; button.className = 'setPanel'; button.addEventListener('click', () => { states[feature.key] = !states[feature.key]; localStorage.setItem(`feature_${feature.key}`, states[feature.key]); button.innerText = `${feature.label}: ${states[feature.key] ? '✅开启' : '❌关闭'}`; button.style.background = states[feature.key] ? '#d4edda' : '#f8d7da'; console.log(`功能[${feature.label}] 状态: ${states[feature.key]}`); }); panel.appendChild(button); }); document.body.appendChild(panel); toggleButton.addEventListener('mouseenter', () => { isOverButtonOrPanel = true; panel.style.display = 'block'; }); toggleButton.addEventListener('mouseleave', () => { isOverButtonOrPanel = false; setTimeout(() => { if (!isOverButtonOrPanel) panel.style.display = 'none'; }, 200); }); panel.addEventListener('mouseenter', () => { isOverButtonOrPanel = true; panel.style.display = 'block'; }); panel.addEventListener('mouseleave', () => { isOverButtonOrPanel = false; setTimeout(() => { if (!isOverButtonOrPanel) panel.style.display = 'none'; }, 200); }); window.featureFlags = states; // 分割线 panel.appendChild(document.createElement('hr')); // 规则编辑区域 const ruleTitle = document.createElement('div'); ruleTitle.innerText = 'statItems 规则设置:'; ruleTitle.style.margin = '8px 0'; ruleTitle.style.fontWeight = 'bold'; panel.appendChild(ruleTitle); // 规则容器 const ruleContainer = document.createElement('div'); ruleContainer.style.display = 'flex'; ruleContainer.style.flexDirection = 'column'; ruleContainer.style.gap = '4px'; ruleContainer.style.width='100%' panel.appendChild(ruleContainer); // 加载现有规则 let rules = JSON.parse(localStorage.getItem('bilibili_stat_rules') || '[]'); function renderRules() { ruleContainer.innerHTML = ''; rules.forEach((rule, idx) => { const row = document.createElement('div'); row.style.display = 'flex'; row.style.gap = '4px'; row.style.alignItems = 'center'; row.style.width='100%' const inputUrl = document.createElement('input'); inputUrl.type = 'text'; inputUrl.value = rule.url; inputUrl.placeholder = '链接匹配'; inputUrl.style.flex = '2'; inputUrl.style.width='70%'; const inputStart = document.createElement('input'); inputStart.type = 'number'; inputStart.value = rule.start; inputStart.placeholder = '起始P'; inputStart.style.width = '10%'; const inputEnd = document.createElement('input'); inputEnd.type = 'number'; inputEnd.value = rule.end; inputEnd.placeholder = '结束P'; inputEnd.style.width = '10%'; const deleteBtn = document.createElement('button'); deleteBtn.innerText = '❌'; deleteBtn.style.width = '30px'; deleteBtn.addEventListener('click', () => { rules.splice(idx, 1); localStorage.setItem('bilibili_stat_rules', JSON.stringify(rules)); renderRules(); }); [inputUrl, inputStart, inputEnd].forEach((input, fieldIndex) => { input.addEventListener('change', () => { rules[idx] = { url: inputUrl.value.trim(), start: parseInt(inputStart.value), end: parseInt(inputEnd.value) }; localStorage.setItem('bilibili_stat_rules', JSON.stringify(rules)); }); }); row.appendChild(inputUrl); row.appendChild(inputStart); row.appendChild(inputEnd); row.appendChild(deleteBtn); ruleContainer.appendChild(row); }); } renderRules(); // 添加规则按钮 const addRuleBtn = document.createElement('button'); addRuleBtn.innerText = '➕ 添加规则'; addRuleBtn.style.marginTop = '6px'; addRuleBtn.style.padding = '5px'; addRuleBtn.style.width = '100%'; addRuleBtn.addEventListener('click', () => { rules.push({ url: '', start: 1, end: 1 }); localStorage.setItem('bilibili_stat_rules', JSON.stringify(rules)); renderRules(); }); // 添加注意事项标题 const noteTitle = document.createElement('div'); noteTitle.innerText = '注意事项'; noteTitle.style.marginTop = '12px'; noteTitle.style.fontWeight = 'bold'; panel.appendChild(noteTitle); // 添加注意事项内容 const noteContent = document.createElement('div'); noteContent.innerText = '• 规则匹配时会根据链接包含关系截取显示章节。\n• 修改规则后请刷新页面以确保生效。\n• 关闭功能后相应统计不会显示。\n•课程链接/开始p/结束p(课程链接仅需到:https://www.bilibili.com/video/...xx.../)\n'; noteContent.style.whiteSpace = 'pre-wrap'; // 保持换行 noteContent.style.fontSize = '12px'; noteContent.style.color = '#666'; panel.appendChild(noteContent); panel.appendChild(addRuleBtn); } function waitForVideoAndRun(retry = 0) { if (retry > 30) { console.warn("超过最大等待次数,未能获取视频列表。"); return; } if (!getAllVideo() || !getActiveVideo() || !getStatItems()) { setTimeout(() => waitForVideoAndRun(retry + 1), 500); return; } haveShowFirst = true; if (states.showChapterTime && !chapterTimerHaveShow) { getChapterTime(); chapterTimerHaveShow = true; } if (states.showPageP && !pageHaveShow) { setP(); pageHaveShow = true; } if (states.showTotalTimer) { getAllTimer(); } window.addEventListener("click", function () { if(states.showTotalTimer) getAllTimer(); }); } setTimeout(() => { createControlPanel(); waitForVideoAndRun() },1500); })();