// ==UserScript==
// @name 🚀哔哩哔哩(bilibili)倍速与多P剩余时长显示增强脚本
// @namespace http://tampermonkey
// @version 2.7
// @description 🌟让您的b站(bilibili,哔哩哔哩)视频观看更加轻松愉快!本脚本支持:⭐倍速播放:按下 "Z" 恢复默认速度, "X" 降低速度, "C" 加快速度⭐显示多 P 视频的剩余时间,让您掌握自己的观看进度,随时调整观看计划!⭐屏蔽adblock导致的提示框,快来试试吧!🎉
// @updateNote v2.7 - 菜单项增加了「启用/禁用记忆倍速」功能。
// @updateNote v2.6 - 修复了切换视频时间统计不更新的问题。
// @updateNote v2.5 - 修复了视频跳转、页面显示相关问题。
// @updateNote v2.4 - 修复了某些情况下多P失效的问题。
// @updateNote v2.3 - 现在按下 'Z' 键可以切换默认倍速和记忆倍速,修复了某些情况下倍速失效的问题。
// @updateNote v2.2 - 速度调节提示框位置优化,屏蔽adblock提示。
// @updateNote v2.1 - 倍速步长调节(默认0.1),倍速功能启用/禁用,多P视频信息剩余时间展示启用/禁用。
// @updateNote v2.0 - 增加了倍速功能,每次步进为0.25,记忆上一次播放速度,优化多P视频信息剩余时间计算性能,修复了已知的一些问题,提高了脚本的稳定性和兼容性。
// @updateNote v1.1 - 开源协议调整。
// @updateNote v1.0 - 第一版发布,多P视频的剩余时间显示。
// @author txsxcy
// @license GPL
// @match *://www.bilibili.com/*
// @icon chrome://favicon/http://www.bilibili.com/
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
const style = `
.video-info {
overflow: hidden;
text-align: center;
box-sizing: border-box;
height: 100%;
width: 100%;
background-color: rgb(241, 242, 243);
border-radius: 6px;
font-size: 15px;
line-height: 30px;
margin-bottom: 25px;
padding: 10px 10px 0px 10px;
pointer-events: all;
}
.video-info li {
width: 30%;
float: left;
margin-right: 10px;
margin-bottom: 10px;
list-style: none;
}
.video-info ul li:hover {
background-color: rgb(255, 255, 255);
border-radius: 12px;
color: #00aeec;
cursor:pointer
}
.video-info ul li:hover span {
color: #00aeec;
}
.video-info span {
display: block;
width: 100%;
}
.video-info li span:first-child {
color: #222;
font-weight: 700;
}
.video-info li span:last-child {
font-size: 12px;
color: #18191c;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = style;
document.head.appendChild(styleEl);
})();
(function () {
'use strict';
const style = `
#speed {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 50%;
left: 50%;
width: 100px;
height: 32px;
padding: 8px;
color: #000;
font-size: 20px;
border-radius: 7px;
background-color: hsla(0, 0%, 100%, .6);
transform: translate(-50%, -50%);
z-index: 77;
visibility: hidden;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = style;
document.head.appendChild(styleEl);
})();
(function () {
// 隐藏adblock提示
let banner = document.querySelector('.adblock-tips');
if (banner) {
// 隐藏横幅元素
banner.style.display = 'none';
}
})();
(function () {
const SPEED_INTERVAL = 1000
// 倍速步长
let SPEED_DELTA = GM_getValue("SPEED_DELTA", 0.1);
// 菜单栏设置项
let speedEnabled = GM_getValue("speedEnabled", true);
let timeEnabled = GM_getValue("timeEnabled", true);
// 存储记忆倍速的状态
let rememberSpeedEnabled = GM_getValue("rememberSpeedEnabled", true);
GM_registerMenuCommand("设置倍速步长", setSpeed);
GM_registerMenuCommand("启用/禁用倍速视频功能", toggleSpeed);
GM_registerMenuCommand("启用/禁用展示时间信息功能", toggleTime);
GM_registerMenuCommand("启用/禁用记忆倍速", toggleRememberSpeed);
// 原始播放速度
let originalPlaybackRate = 1
// 是否多p视频
let isMultiPVideo = false
//实现保存Z键切换速率
let savedSpeed = 1
let video = document.querySelector('video') || document.querySelector('bwp-video')
if (speedEnabled) {
// 初始化倍速
let playbackRateStorage = localStorage.getItem('playbackRate')
if (playbackRateStorage) {
originalPlaybackRate = parseFloat(playbackRateStorage)
}
// 保存初始倍速
if (video) {
video.playbackRate = originalPlaybackRate
}
}
// 对按键监听函数进行节流
const throttleKeydown = throttle((event) => {
if (!speedEnabled) {
// 视频功能禁用
return
}
if (!event.ctrlKey) {
let video = document.querySelector('video') || document.querySelector('bwp-video')
let keyValue = event.key.toUpperCase()
if (keyValue === 'X' && video.playbackRate > SPEED_DELTA) {
video.playbackRate = formatNumber(video.playbackRate - SPEED_DELTA)
showSpeed(video.playbackRate)
}
if (keyValue === 'C' && video.playbackRate < 16) {
video.playbackRate = formatNumber(video.playbackRate + SPEED_DELTA)
showSpeed(video.playbackRate)
}
if (keyValue === 'Z') {
if (video.playbackRate === 1) {
video.playbackRate = savedSpeed
} else {
savedSpeed = video.playbackRate
video.playbackRate = 1
}
showSpeed(video.playbackRate)
}
localStorage.setItem('playbackRate', video.playbackRate.toString())
if (isMultiPVideo) {
showRemainingDuration(video.playbackRate)
}
}
})
// 对 document 的 keydown 事件进行绑定,调用节流函数
document.addEventListener('keydown', throttleKeydown)
// 监听 URL 变化并恢复倍速
let currentUrl = window.location.href
setInterval(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href
if (video) {
let playbackRateStorage = localStorage.getItem('playbackRate')
if (playbackRateStorage) {
let playbackRate = parseFloat(playbackRateStorage)
if (playbackRate !== video.playbackRate) {
// 如果记忆倍速被启用,则恢复上次播放速度
if (rememberSpeedEnabled) {
if (playbackRate !== video.playbackRate) {
video.playbackRate = playbackRate;
showSpeed(playbackRate);
}
} else {
// 禁用记忆倍速时,设置为默认倍速1
video.playbackRate = 1;
showSpeed(video.playbackRate);
}
if (isMultiPVideo) {
setTimeout(() => {
showRemainingDuration(video.playbackRate);
}, 500);
}
}
}
}
}
}, 100)
let videoTimes = [];
// 等待元素加载完成
onReady('.bpx-player-video-area', function () {
const div = document.createElement('div');
div.setAttribute('id', 'speed');
div.innerHTML = '<span></span>';
document.querySelector('.bpx-player-video-area').appendChild(div);
}, 100)
onReady('.stats .duration', function () {
setTimeout(() => {
// 兼容性检查
if (checkThirdPartyScript()) {
return
}
isMultiPVideo = true;
videoTimes = getVideoTimes();
showRemainingDuration(video.playbackRate);
}, 3000);
}, 100)
// 小数精度处理
function formatNumber(num) {
let decimalNum = Number(num.toString().match(/\.\d+/));
if (isNaN(decimalNum)) {
return num;
} else if (decimalNum === Math.round(decimalNum)) {
return num.toFixed(1);
} else {
return num.toFixed(2);
}
}
// 设置节流函数
function throttle(fn) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, 100)
}
}
}
// 获取视频播放时间数组
function getVideoTimes() {
if (videoTimes.length > 0) {
return videoTimes;
}
let lis = document.querySelectorAll('.stats .duration');
if (lis.length === 0) {
lis = document.querySelectorAll('.video-sections-item .video-episode-card__info-duration')
}
lis.forEach((currentValue, index) => {
const time = currentValue.innerText.replace(/\.\d+/g, '');
videoTimes.push({
timeStr: time,
timeSeconds: timeToSeconds(time)
});
});
return videoTimes;
}
function showRemainingDuration(speed = 1) {
if (!timeEnabled) {
return
}
let currentspeed = speed
let matches = document.querySelector('.amt').innerText.match(/\((\d+)\/(\d+)\)/);
let start = parseInt(matches[1]);
let end = parseInt(matches[2]);
let videoData = document.querySelector('#danmukuBox');
// let videoData = document.querySelector('#viewbox_report');
let duration = calTime(start, end);
// 获取要插入的元素的父元素
let parent = videoData.parentElement;
// 查找是否有类名为 "video-info" 的元素
let info = parent.querySelector(".video-info");
// 如果存在,则删除它
if (info) {
info.remove();
}
const videoInfo = [{
title: '总时长',
duration: durationToString(calTime(1, end).total)
}, {
title: '已看时长',
duration: durationToString(calTime(1, start - 1).total)
}, {
title: '剩余时长',
duration: durationToString(calTime(start, end).total)
}, {
title: '1.5x',
duration: durationToString(Math.floor(duration.total / 1.5))
}, {
title: '2x',
duration: durationToString(Math.floor(duration.total / 2))
}, {
title: `${currentspeed}x`,
duration: durationToString(Math.floor(duration.total / currentspeed))
}];
let html = '';
videoInfo.forEach(info => {
html += `<li>
<span>${info.title}</span>
<span>${info.duration}</span>
</li>`;
});
html = `<div>
<ul>
${html}
</ul>
</div>`;
videoData.insertAdjacentHTML('afterend', `<div class="video-info">${html}</div>`);
}
// 根据视频播放时间数组和范围计算时间数据
function calTime(start, end) {
const duration = {
total: 0,
watched: 0,
remaining: 0
};
const endIndex = Math.min(videoTimes.length, end);
for (let i = start - 1; i < endIndex; i++) {
const data = videoTimes[i];
if (i < end - 1) {
duration.watched += data.timeSeconds;
} else {
duration.remaining += data.timeSeconds;
}
duration.total += data.timeSeconds;
}
return duration;
}
// 秒转hh:mm:ss
function durationToString(duration) {
const h = parseInt(duration / 3600);
const m = parseInt(duration / 60) % 60;
const s = duration % 60;
if (h > 0) {
return `${h}h ${m}min ${s}s`;
} else {
return `${m}min ${s}s`;
}
}
// 等待元素加载完成函数
function onReady(selector, func, times = -1, interval = 20) {
let intervalId = setInterval(() => {
if (times === 0) {
clearInterval(intervalId)
} else {
times -= 1
}
if (document.querySelector(selector)) {
clearInterval(intervalId)
func()
}
}, interval)
}
// 显示速度函数
function showSpeed(speed, index = 1) {
let speedDiv = document.querySelector(`#speed`);
if (!speedDiv) {
const div = document.createElement('div');
div.setAttribute('id', 'speed');
div.innerHTML = '<span></span>';
document.querySelector('.bpx-player-video-area').appendChild(div);
speedDiv = div;
}
let speedSpan = speedDiv.querySelector('span')
if (index == 1) {
speedSpan.innerHTML = `${speed} X`
} else {
speedSpan.innerHTML = `${speed}`
}
speedDiv.style.visibility = 'visible'
clearTimeout(window.speedTimer)
window.speedTimer = setTimeout(() => {
speedDiv.style.visibility = 'hidden'
}, SPEED_INTERVAL)
}
// 检测第三方倍速插件
function checkThirdPartyScript() {
//没有开倍速就不用检测了
if (!speedEnabled) {
return false
}
if (document.querySelector(".html_player_enhance_tips")) {
document.querySelector('#danmukuBox').insertAdjacentHTML('afterend', `<div class="video-info"><div> 请禁用第三方倍速脚本<br>- 🚀Bilibili 倍速与多P剩余时长显示增强脚本 - </div></div>`);
return true;
} else {
return false;
}
}
// 将时间字符串转换为秒数
function timeToSeconds(time) {
const timeArr = time.split(':');
let timeSeconds = 0;
if (timeArr.length === 3) {
timeSeconds += Number(timeArr[0]) * 60 * 60;
timeSeconds += Number(timeArr[1]) * 60;
timeSeconds += Number(timeArr[2]);
} else {
timeSeconds += Number(timeArr[0]) * 60;
timeSeconds += Number(timeArr[1]);
}
return timeSeconds;
}
// 菜单栏切换倍速功能状态
function toggleSpeed() {
speedEnabled = !speedEnabled;
GM_setValue("speedEnabled", speedEnabled);
if (speedEnabled) {
showSpeed("倍速:启用", 2)
} else {
showSpeed("倍速:禁用", 2)
}
}
// 菜单栏切换时间展示功能状态
function toggleTime() {
timeEnabled = !timeEnabled;
GM_setValue("timeEnabled", timeEnabled);
if (timeEnabled) {
showSpeed("展示:启用", 2)
showRemainingDuration(video.playbackRate);
} else {
showSpeed("展示:禁用", 2)
let info = document.querySelector('#danmukuBox').parentElement.querySelector(".video-info");
// 如果存在,则删除它
if (info) {
info.remove();
}
}
}
// 菜单栏设置倍速步长
function setSpeed() {
var input = prompt("请输入倍速步长(默认0.1):", SPEED_DELTA);
if (input === null) {
return;
}
if (isNaN(input) || input === "") {
alert("请输入数字!");
} else {
if (Number(input) > 0) {
SPEED_DELTA = Number(input);
GM_setValue("SPEED_DELTA", SPEED_DELTA);
}
}
}
// 菜单栏切换记忆倍速功能的状态
function toggleRememberSpeed() {
rememberSpeedEnabled = !rememberSpeedEnabled;
GM_setValue("rememberSpeedEnabled", rememberSpeedEnabled);
if (rememberSpeedEnabled) {
showSpeed("记忆倍速:启用", 2)
} else {
showSpeed("记忆倍速:禁用", 2)
}
}
})
()