您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
监听URL变化并计算分P总时长/已播放/剩余(右下角 box 背景进度 + 分P卡片背景进度)
// ==UserScript== // @name B站分P时长统计 // @namespace http://tampermonkey.net/ // @version 0.83 // @description 监听URL变化并计算分P总时长/已播放/剩余(右下角 box 背景进度 + 分P卡片背景进度) // @author kirari // @match *://*.bilibili.com/video/* // @icon https://www.google.com/s2/favicons?sz=64&domain=openuserjs.org // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; /** ---------------- 工具函数 ---------------- */ function onUrlChange(callback) { let last = location.href; const fire = () => { const now = location.href; if (now !== last) { last = now; callback(now); } }; const wrap = name => { const orig = history[name]; return function () { const ret = orig.apply(this, arguments); fire(); return ret; }; }; history.pushState = wrap('pushState'); history.replaceState = wrap('replaceState'); window.addEventListener('popstate', fire); window.addEventListener('hashchange', fire); setInterval(fire, 500); callback(last); } function waitFor(selector, root = document, timeout = 10000) { return new Promise((resolve, reject) => { const found = root.querySelector(selector); if (found) return resolve(found); const obs = new MutationObserver(() => { const el = root.querySelector(selector); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); reject(new Error('waitFor timeout: ' + selector)); }, timeout); }); } function parseDuration(str) { if (!str) return 0; const parts = str.split(':').map(s => parseInt(s, 10) || 0); if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; if (parts.length === 2) return parts[0] * 60 + parts[1]; return parts[0] || 0; } function secondsToHMS(sec) { sec = Math.max(0, Math.floor(sec || 0)); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } /** ---------------- UI: 右下角 box ---------------- */ function ensureBox() { const id = 'bili-total-hours-badge'; let box = document.getElementById(id); if (!box) { box = document.createElement('div'); box.id = id; Object.assign(box.style, { position: 'fixed', right: '14px', bottom: '14px', zIndex: '999999', padding: '10px 12px', borderRadius: '12px', boxShadow: '0 4px 16px rgba(0,0,0,.2)', color: '#fff', fontSize: '12px', lineHeight: '1.6', backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', userSelect: 'text', backgroundColor: 'rgba(0,0,0,0.45)', overflow: 'hidden', // minWidth: '150px' }); const inner = document.createElement('div'); inner.id = 'bili-total-hours-badge-inner'; inner.style.position = 'relative'; inner.style.zIndex = '2'; box.appendChild(inner); const progressLayer = document.createElement('div'); progressLayer.id = 'bili-total-hours-progress-layer'; Object.assign(progressLayer.style, { position: 'absolute', left: '0', top: '0', bottom: '0', width: '0%', zIndex: '1', pointerEvents: 'none' }); box.appendChild(progressLayer); document.body.appendChild(box); } return box; } function showBadge({ total, played, remain }) { const box = ensureBox(); const inner = document.getElementById('bili-total-hours-badge-inner'); const progressLayer = document.getElementById('bili-total-hours-progress-layer'); if (!total || total <= 0) { box.style.display = 'none'; return; } else { box.style.display = 'block'; } const ratio = Math.max(0, Math.min(1, (played || 0) / total)); const percent = (ratio * 100).toFixed(2) + '%'; progressLayer.style.width = percent; progressLayer.style.background = 'linear-gradient(90deg, rgba(76,175,80,0.9), rgba(76,175,80,0.7))'; inner.innerHTML = ` <div>⏱ 总时长:${secondsToHMS(total)}</div> <div>▶ 已播放:${secondsToHMS(played)}</div> <div>⏭ 剩余:${secondsToHMS(remain)}</div> `; } /** ---------------- 卡片背景进度 ---------------- */ let lastCard = null; // function updateCardProgress(video) { // const card = document.querySelector('.simple-base-item.page-item.active.sub,.video-pod__item.active,.simple-base-item.active'); // if (!card || !video.duration) return; // if (lastCard && lastCard !== card) { // lastCard.style.background = ''; // } // lastCard = card; // const percent = (video.currentTime / video.duration) * 100; // card.style.background = `linear-gradient( // to right, // rgba(76, 175, 80, 0.4) ${percent}%, // transparent ${percent}% // )`; // } function updateCardProgress({ total, played, remain }) { // function updateCardProgress(video) { // if (!video || !video.duration) return; // 优先匹配最精确的 active 分P const ratio = Math.max(0, Math.min(1, (played || 0) / total)); const percent = (ratio * 100).toFixed(2) ; // const percent = Math.floor(ratio * 100) + '%'; // const percent = played/total* 100; const card = document.querySelector('.video-pod.video-pod') || document.querySelector('.simple-base-item.page-item.active.sub') || document.querySelector('.simple-base-item.active') || document.querySelector('.video-pod__item.active'); const active = document.querySelector('.simple-base-item.page-item.active.sub') || document.querySelector('.simple-base-item.active') || document.querySelector('.video-pod__item.active'); // if (!card) return; // 如果上一次记录的卡片和当前不同,清除上一个背景 if (lastCard && lastCard !== active) { lastCard.style.border = ''; } lastCard = active; // const percent = (video.currentTime / video.duration) * 100; card.style.background = `linear-gradient( to right, rgba(76, 175, 80, 0.4) ${percent}%, transparent ${percent}% )`; card.style.border=`solid 1px black`; active.style.border=`solid 1px black`; } // rgba(76, 175, 80, 0.4) ${percent}%,transparent 100% /** ---------------- 全局状态 ---------------- */ let totalSeconds = 0; let basePlayed = 0; let videoElement = null; function getCurrentVideoProgress() { return videoElement ? Math.floor(videoElement.currentTime || 0) : 0; } /** ---------------- 核心逻辑 ---------------- */ async function recalcSegments() { try { await waitFor('.simple-base-item .duration').catch(() => waitFor('.video-pod__item .duration')); let items = Array.from(document.querySelectorAll('.simple-base-item')); if (items.length === 0) { items = Array.from(document.querySelectorAll('.video-pod__item')); } if (items.length === 0) { const b = document.getElementById('bili-total-hours-badge'); if (b) b.style.display = 'none'; return; } totalSeconds = 0; basePlayed = 0; let passedActive = false; let hasActive = false; for (const item of items) { const tEl = item.querySelector('.duration'); if (!tEl) continue; const sec = parseDuration(tEl.textContent.trim()); totalSeconds += sec; if (item.classList.contains('active')) { hasActive = true; passedActive = true; } if (!passedActive) basePlayed += sec; } if (!hasActive) basePlayed = 0; bindVideoListener(); updateProgress(); } catch (e) { console.warn('统计失败:', e); } } function updateProgress() { const current = getCurrentVideoProgress(); const played = basePlayed + current; const remain = Math.max(0, totalSeconds - played); showBadge({ total: totalSeconds, played, remain }); if (videoElement) { // updateCardProgress(videoElement); updateCardProgress({ total: totalSeconds, played, remain }); } } function bindVideoListener() { const v = document.querySelector('video'); if (!v) return; if (videoElement !== v) { if (videoElement) { videoElement.removeEventListener('timeupdate', updateProgress); } videoElement = v; videoElement.addEventListener('timeupdate', updateProgress); } } /** ---------------- 启动 ---------------- */ window.addEventListener('load', () => setTimeout(recalcSegments, 1500)); onUrlChange(href => { if (/^https?:\/\/(www\.)?bilibili\.com\/video\//.test(href)) { setTimeout(recalcSegments, 400); } }); })();