Greasy Fork is available in English.

Bilibili自定义倍速播放

B站自定义倍速播放

// ==UserScript==
// @name Bilibili自定义倍速播放
// @namespace http://tampermonkey.net/
// @version 0.8
// @description  B站自定义倍速播放
// @updateNote   添加类似 Potplayer 的功能,默认倍速和记忆倍速,方便用户快速切换播放速度;2.修复了某些情况下倍速失效的问题。
// @author 小明
// @license MIT
// @match        https://www.bilibili.com/*
// @icon         chrome://favicon/http://www.bilibili.com/
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at document-end
// ==/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.05);
    // 菜单栏设置项
    let speedEnabled = GM_getValue("speedEnabled", true);
    let timeEnabled = GM_getValue("timeEnabled", true);
    GM_registerMenuCommand("设置倍速步长", setSpeed);
    GM_registerMenuCommand("启用/禁用倍速视频功能", toggleSpeed);
    GM_registerMenuCommand("启用/禁用展示时间信息功能", toggleTime);
    // 原始播放速度
    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) => {
		//拦截器1
        if (!speedEnabled) {
            // 视频功能禁用
            return
        }
		//拦截器2
		if(iSearching()){
			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 (speedEnabled) {
                            video.playbackRate = playbackRate
                            showSpeed(playbackRate)
                        }
                        if (isMultiPVideo) {
                            showRemainingDuration(video.playbackRate)
                        }
                    }
                }
            }
        }
    }, 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('.list-box .duration', function () {
        // 兼容性检查
        if (checkThirdPartyScript()) {
            return
        }
        isMultiPVideo = true;
        videoTimes = getVideoTimes();
        showRemainingDuration(video.playbackRate)
    }, 100)
    onReady('.video-episode-card__info', 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('.list-box .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('.cur-page').innerText.match(/\((\d+)\/(\d+)\)/);
        let start = parseInt(matches[1]);
        let end = parseInt(matches[2]);
        let videoData = document.querySelector('#danmukuBox');
        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.05):", 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 iSearching(){
		let s1 = false;
		let s2 = false;
		//焦点在搜索框	
		if(document.getElementById('nav-searchform').length>0){
			s1 = document.getElementById('nav-searchform').classList.contains('is-actived');
		}
		//焦点在评论区
		if(document.getElementsByClassName('reply-box-textarea').length>0){
			s2 = document.getElementsByClassName('reply-box-textarea')[0].classList.contains('focus');
		}
		let s = s1 || s2;
		return s;
	}
})();

(function () {
	//标题简洁
	setTimeout(function(){
		ptile();
	},6000)
})();


//--------------标题简洁 函数--------------------start
//标题简洁风
function ptile(){
	if(document.getElementsByClassName('base-video-sections-v1') == null){
		return;
	}
    console.info('---ptile---');

	let arr = document.querySelectorAll('.video-episode-card .video-episode-card__info-title');
	let prefix = findPrefix();
	for(var i = 0;i<arr.length;i++){
		var str = arr[i].innerText;
		str = str.replace(prefix,'');
		arr[i].innerText = str;
	}
}

//寻找标题公共前缀
function findPrefix(){
	var prefix = '';
	// NodeList 不是一个数组,是一个类似数组的对象.可以使用 Array.from() 将其转换为数组
	var liArr = document.querySelectorAll('.video-episode-card .video-episode-card__info-title');
	liArr = Array.from(liArr);
	var arr = liArr.map( (item, index) => {
		return item.title
	})

	//console.log("a标签的title集合", arr)
	if(arr.length>=3){

		//随机采样
		var index1 = getRndInteger(0,arr.length);
		var index2 = getRndInteger(0,arr.length);
		var index3 = getRndInteger(0,arr.length);
		var s1s2 = [arr[index1],arr[index2]];
		var s2s3 = [arr[index2],arr[index3]];
		console.info(s1s2);
		console.info(s2s3);
		var s1s2_Pre = longestCommonPrefix(s1s2);
		var s2s3_Pre = longestCommonPrefix(s2s3);
		if(s1s2_Pre == s2s3_Pre){
			prefix = s1s2_Pre;
		}
	}
	return prefix;
}


//JavaScript 最长公共前缀
function longestCommonPrefix(strs) {
    if(strs.length == 0)
        return "";
    let ans = strs[0];
    for(let i =1;i<strs.length;i++) {
        let j=0;
        for(;j<ans.length && j < strs[i].length;j++) {
            if(ans[j] != strs[i][j])
                break;
        }
        ans = ans.substr(0, j);
        if(ans === "")
            return ans;
    }
    return ans;
};

//返回 min(包含)~ max(不包含)之间的数字
function getRndInteger(min, max) {
  return Math.floor(Math.random() * (max - min) ) + min;
}
//--------------标题简洁 函数--------------------end