Greasy Fork

来自缓存

Greasy Fork is available in English.

Bilibili自定义倍速播放

B站自定义倍速播放

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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