BiliBili 高级倍速功能

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

Fra og med 05.11.2023. Se den nyeste version.

// ==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());
	});
})();