Greasy Fork

Greasy Fork is available in English.

BiliBili 高级倍速功能

BiliBili倍速插件,支持自定义速度、记忆上一次速度、快捷键调速。

当前为 2023-11-05 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            BiliBili 高级倍速功能
// @namespace       cec8225d12878f0fc33997eb79a69894
// @version         1.0
// @description     BiliBili倍速插件,支持自定义速度、记忆上一次速度、快捷键调速。
// @author          TheBszk
// @match           https://www.bilibili.com/video/*
// @match           https://www.bilibili.com/list/*
// @match           https://www.bilibili.com/bangumi/play/*
// @icon            https://www.bilibili.com/favicon.ico
// @license         AGPL
// ==/UserScript==

(function () {
	"use strict";
	const CUSTOM_RATE_ARRAY = "custom_rate_array";
	const CUSTOM_RATE = "custom_rate";

	function getPageType() {
		const path = window.location.pathname;

		if (path.startsWith("/video/")) {
			return "video";
		} else if (path.startsWith("/list/")) {
			return "list";
		} else if (path.startsWith("/bangumi/play/")) {
			return "bangumi";
		} else {
			return "unknown";
		}
	}

	const pageType = getPageType();

	if (pageType == "video" || pageType == "list") {
		var MENUCLASS = "bpx-player-ctrl-playbackrate-menu";
		var MENUCLASS_ITEM = "bpx-player-ctrl-playbackrate-menu-item";
		var MENUCLASS_ACTIVE = "bpx-state-active";
	} else if (pageType == "bangumi") {
		var MENUCLASS = "squirtle-speed-select-list";
		var MENUCLASS_ITEM = "squirtle-select-item";
		var MENUCLASS_ACTIVE = "active";
	}

	function getRate() {
		let rate = localStorage.getItem(CUSTOM_RATE);
		if (rate <= 0) {
			rate = 1;
		}
		return rate;
	}

	function getRateArray() {
		let storageData = localStorage.getItem(CUSTOM_RATE_ARRAY);
		let rates;
		if (storageData == null) {
			rates = [];
		} else {
			rates = storageData.split(",");
		}
		if (rates.length === 0) {
			//如果没有,则初始化一个默认的
			rates = [0.5, 1.0, 1.5, 2, 2.5, 3.0, 4.0];
			localStorage.setItem(CUSTOM_RATE_ARRAY, rates.join(","));
		}
		return rates;
	}

	// 创建元素在速度变化时显示
	var speedDisplay = document.createElement("div");
	speedDisplay.style.display = "none";
	speedDisplay.style.position = "absolute";
	speedDisplay.style.bottom = "20px";
	speedDisplay.style.right = "20px";
	speedDisplay.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
	speedDisplay.style.color = "white";
	speedDisplay.style.padding = "5px";
	speedDisplay.style.borderRadius = "5px";
	speedDisplay.style.zIndex = "1000";
	speedDisplay.style.fontSize = "22px";

	let hideTimer;
	function showPlayRate(rate) {
		speedDisplay.textContent = `速度: ${rate}x`;
		speedDisplay.style.display = "block";

		if (!hideTimer) {
			clearTimeout(hideTimer);
		}
		hideTimer = setTimeout(function () {
			speedDisplay.style.display = "none";
		}, 1200);
	}

	class PlayRateMenu {
		init(menu) {
			this.menu = menu;
			this.rates = getRateArray();
			this.videoObj = document.querySelector("video");
		}

		insertRate(rateValue) {
			this.rates.push(rateValue);
			this.render();
		}

		insertItem(content, rate, event) {
			const item = document.createElement("li");
			item.textContent = content;
			item.classList.add(MENUCLASS_ITEM);
			item.setAttribute("data-value", rate);
			item.addEventListener("click", event);
			this.menu.appendChild(item);
		}

		render() {
			this.menu.innerHTML = "";
			this.rates.sort((a, b) => b - a); //排序

			this.rates.forEach((rate) => {
				this.insertItem(rate % 1 == 0 ? rate + ".0x" : rate + "x", rate, (e) => {
					e.stopPropagation();
					const rateValue = e.target.getAttribute("data-value");
					this.setVideoRate(rateValue);
					this.setActiveRate(rateValue);
					localStorage.setItem(CUSTOM_RATE, rateValue);
				});
			});

			//插入一个设置按钮
			this.insertItem("设置", 0, (e) => {
				e.stopPropagation();
				let inputStr = window.prompt("请输入自定义倍速,以英文逗号隔开。", getRateArray().join(","));
				if (inputStr === null || inputStr.trim() === "") return;

				let rates = inputStr
					.split(",")
					.map((s) => s.trim())
					.filter((s) => s);
				if (rates.length === 0) return;

				// 检查输入是否全部为有效数字
				if (!rates.every((s) => isFinite(s))) {
					alert("输入包含无效的倍速值,请输入有效的数字。");
					return;
				}

				localStorage.setItem(CUSTOM_RATE_ARRAY, rates.join(","));
				this.rates = rates;

				this.render();

				let nowRate = getRate();
				if (rates.indexOf(nowRate) === -1) {
					this.setRate(1);
				} else {
					this.setRate(nowRate);
				}
			});
		}

		setActiveRate(rateValue) {
			const items = this.menu.querySelectorAll(`.${MENUCLASS_ITEM}`);
			items.forEach((item) => {
				const value = item.getAttribute("data-value");
				if (value === rateValue) {
					item.classList.add(MENUCLASS_ACTIVE);
				} else {
					item.classList.remove(MENUCLASS_ACTIVE);
				}
			});
		}

		setVideoRate(rate) {
			this.videoObj.playbackRate = rate;
		}

		//使用此函数前提:速度列表必须存在该速度值
		setRate(rate) {
			const item = document.querySelector(`.${MENUCLASS_ITEM}[data-value="${rate}"]`);
			if (item) {
				item.classList.add(MENUCLASS_ACTIVE);
				item.click(); // 模拟点击事件
			} else {
				console.error("未找到匹配元素");
			}
		}

		changeRate(up) {
			let nowRate = getRate();
			let index = this.rates.indexOf(nowRate);
			if ((index == 0 && up) || (index == this.rates.length && !up)) {
				return nowRate;
			} else {
				index += up ? -1 : 1;
				this.setRate(this.rates[index]);
				return this.rates[index];
			}
		}
	}

	let menu = new PlayRateMenu();

	let _interval = setInterval(function () {
		let element = document.querySelector(`.${MENUCLASS}`);
		if (element) {
			document.querySelector(".bpx-player-video-wrap").appendChild(speedDisplay);
			menu.init(element);
			menu.render();
			menu.setRate(getRate());
			clearInterval(_interval);
		}
	}, 500);

	let ArrowRightTime = 0;
	let ArrowRightRate = 0;
	document.addEventListener("keydown", function (e) {
		e = e || window.event;
		if (e.ctrlKey == true && e.code == "ArrowUp") {
			let rate = menu.changeRate(true);
			showPlayRate(rate);
		} else if (e.ctrlKey == true && e.code == "ArrowDown") {
			let rate = menu.changeRate(false);
			showPlayRate(rate);
		} else if (e.code == "ArrowRight") {
			if (ArrowRightTime == 0) {
				ArrowRightTime = e.timeStamp;
			} else {
				if (e.timeStamp - ArrowRightTime > 500) {
					if (ArrowRightRate == 0) {
						ArrowRightRate = getRate();
						menu.setVideoRate(ArrowRightRate * 2);
						showPlayRate(ArrowRightRate * 2);
						e.preventDefault();
					}
				}
			}
		}
	});

	document.addEventListener("keyup", function (e) {
		if (e.code == "ArrowRight") {
			ArrowRightTime = 0;
			if (ArrowRightRate != 0) {
				menu.setVideoRate(ArrowRightRate);
				showPlayRate(ArrowRightRate);
				ArrowRightRate = 0;
				e.preventDefault();
			}
		}
	});

	window.addEventListener("focus", function () {
		menu.setRate(getRate());
	});
})();