Greasy Fork

Greasy Fork is available in English.

🚀Bilibili倍速与多P剩余时长显示增强脚本

🌟让您的b站(bilibili,哔哩哔哩)视频观看更加轻松愉快!本脚本支持:⭐倍速播放:按下 "Z" 恢复默认速度, "X" 降低速度, "C" 加快速度⭐显示多 P 视频的剩余时间,让您掌握自己的观看进度,随时调整观看计划!快来试试吧!🎉

当前为 2023-04-19 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name 🚀Bilibili倍速与多P剩余时长显示增强脚本
// @namespace http://tampermonkey
// @version 2.1
// @description  🌟让您的b站(bilibili,哔哩哔哩)视频观看更加轻松愉快!本脚本支持:⭐倍速播放:按下 "Z" 恢复默认速度, "X" 降低速度, "C" 加快速度⭐显示多 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 () {
    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);
    GM_registerMenuCommand("设置倍速步长", setSpeed);
    GM_registerMenuCommand("启用/禁用倍速视频功能", toggleSpeed);
    GM_registerMenuCommand("启用/禁用展示时间信息功能", toggleTime);
    // 原始播放速度
    let originalPlaybackRate = 1
    // 是否多p视频
    let isMultiPVideo = false
    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' && video.playbackRate !== 1) {
                video.playbackRate = 1
                showSpeed(1)
            }
            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-primary-area', function () {
        const div = document.createElement('div');
        div.setAttribute('id', 'speed');
        div.innerHTML = '<span></span>';
        document.querySelector('.bpx-player-primary-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`)
        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);
            }
        }
    }
})
()