记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。
当前为
// ==UserScript==
// @name Youtube Remember Speed
// @name:zh-TW YouTube 播放速度記憶
// @name:zh-CN YouTube 播放速度记忆
// @name:ja YouTube 再生速度メモリー
// @icon https://www.youtube.com/img/favicon_48.png
// @author ElectroKnight22
// @namespace electroknight22_youtube_remember_playback_rate_namespace
// @version 2.0.0
// @match *://www.youtube.com/*
// @match *://www.youtube-nocookie.com/*
// @exclude *://www.youtube.com/live_chat*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @license MIT
// @description Remembers the speed that you last used. Now hijacks YouTube's custom speed slider and gives you up to 8x speed.
// @description:zh-TW 記住上次使用的播放速度,並改造YouTube的速度調整滑桿,最高支援8倍速。
// @description:zh-CN 记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。
// @description:ja 最後に使った再生速度を覚えておき、YouTubeの速度スライダーを改造して最大8倍速まで対応させます。
// ==/UserScript==
/*jshint esversion: 11 */
(function() {
"use strict";
const DEFAULT_SETTINGS = {
targetSpeed: 1
};
let userSettings = { ...DEFAULT_SETTINGS };
let shouldInitialize = false;
const maxSpeed = 8;
function setSpeed(targetSpeed) {
try {
let video = document.querySelector('video');
video.playbackRate = targetSpeed;
} catch (error) {
console.error("Error when trying to set speed. Error: " + error);
}
}
function overrideSpeedElements() {
if (!window.location.pathname.startsWith('/watch')) return;
const parentSelector = '.ytp-popup.ytp-settings-menu';
const sliderSelector = 'input.ytp-input-slider.ytp-speedslider';
let speedTextElement = null;
let speedLabel = null
function setSpeedText(targetString) {
try {
const text = speedTextElement.textContent;
const newValue = targetString;
speedTextElement.textContent = /\(.*?\)/.test(text)
? text.replace(/\(.*?\)/, `(${newValue})`)
: newValue;
} catch (error) {
console.error("Error when trying to set speed text. Error: " + error);
}
}
function overrideSpeedLabel(sliderElement) {
try {
console.log('overrideSpeedLabel');
if (!speedLabel || !speedLabel.isConnected) {
speedLabel = sliderElement.closest('.ytp-menuitem-with-footer').querySelector('.ytp-menuitem-label');
}
if (speedLabel?.textContent) {
speedLabel.textContent = speedLabel.textContent.replace(/\(.*?\)/, `(${sliderElement.value})`);
}
} catch (error) {
console.error("Error when trying to override speed label. Error: " + error);
}
}
function overrideSliderStyle(sliderElement) {
if (!sliderElement) return;
const speedMenuItems = sliderElement.closest('.ytp-panel-menu').children;
if (speedMenuItems[0].classList.contains('ytp-menuitem-with-footer')) {
Array.from(speedMenuItems).forEach(item => { item.setAttribute('aria-checked', 'false'); });
speedMenuItems[0].setAttribute('aria-checked', 'true');
}
overrideSpeedLabel(sliderElement);
sliderElement.style = `--yt-slider-shape-gradient-percent: ${sliderElement.value / maxSpeed * 100}%;`;
document.querySelector('.ytp-speedslider-text').textContent = sliderElement.value + 'x';
}
function overrideSliderFunction() {
const sliderElement = parentMenu.querySelector(sliderSelector);
if (!sliderElement || !shouldInitialize) return;
overrideSpeedLabel(sliderElement);
shouldInitialize = false;
if (sliderElement.initialized) return;
sliderElement.max = maxSpeed.toString();
sliderElement.setAttribute('value', userSettings.targetSpeed.toString());
setSpeed(userSettings.targetSpeed);
overrideSliderStyle(sliderElement);
sliderElement.addEventListener('input', () => {
const newSpeed = parseFloat(sliderElement.value);
updateSavedSpeed(newSpeed);
setSpeed(newSpeed);
overrideSliderStyle(sliderElement)
});
// Since we are using the html speed control we should suppress youtube's own speed control to prevent unwanted updates.
sliderElement.addEventListener('change', (event) => {
setSpeedText(sliderElement.value);
event.stopImmediatePropagation();
}, true);
sliderElement.initialized = true;
}
async function findSpeedTextElement() {
const youtubeApi = document.querySelector('#movie_player');
await youtubeApi.setPlaybackRate(1.05);
const settingItems = document.querySelectorAll('.ytp-menuitem');
const matchingItem = Array.from(settingItems).find(item =>
item.textContent.includes('1.05')
);
speedTextElement = matchingItem?.querySelector('.ytp-menuitem-content');
}
const speedTextObserver = new MutationObserver(initializeSpeedTextElement);
async function initializeSpeedTextElement() {
try {
if (speedTextElement) {
if (speedTextElement.initialized) return;
const youtubeApi = document.querySelector('#movie_player');
youtubeApi?.addEventListener('onPlaybackRateChange', updateSavedSpeed, true);
speedTextElement.initialized = true;
setSpeedText(userSettings.targetSpeed);
speedTextObserver.disconnect();
} else {
const requestedSpeed = userSettings.targetSpeed;
await findSpeedTextElement();
setSpeed(requestedSpeed);
updateSavedSpeed(requestedSpeed);
setSpeedText(requestedSpeed);
}
} catch (error) {
console.error("Error when trying to initialize speed text element. Error: " + error);
}
}
const parentMenu = document.querySelector(parentSelector);
if (parentMenu) {
const sliderObserver = new MutationObserver(overrideSliderFunction);
sliderObserver.observe(parentMenu, {
childList: true,
subtree: true,
});
speedTextObserver.observe(parentMenu, {
childList: true,
subtree: true,
attributes: true,
});
} else {
console.error('Whoops! The parent menu was not found. The script can\'t run. 😥');
}
}
// syncs the user's settings on load
async function applySettings() {
try {
const storedValues = await GM.listValues();
await Promise.all(Object.entries(DEFAULT_SETTINGS).map(async ([key, value]) => {
if (!storedValues.includes(key)) {
await GM.setValue(key, value);
}
}));
await Promise.all(storedValues.map(async key => {
if (!(key in DEFAULT_SETTINGS)) {
await GM.deleteValue(key);
}
}));
await Promise.all(
storedValues.map(key => GM.getValue(key).then(value => [key, value]))
).then(keyValuePairs => keyValuePairs.forEach(([newKey, newValue]) => {
userSettings[newKey] = newValue;
}));
console.log(Object.entries(userSettings).map(([key, value]) => key + ": " + value).join(", "));
} catch (error) {
console.error("Error when applying settings: " + error.message);
}
}
function updateSavedSpeed(speed) {
userSettings.targetSpeed = speed;
GM.setValue('targetSpeed', userSettings.targetSpeed);
}
function handleNewVideoLoad() {
shouldInitialize = true;
setSpeed(userSettings.targetSpeed);
overrideSpeedElements();
}
function main() {
window.addEventListener("pageshow", () => {
handleNewVideoLoad();
window.addEventListener('yt-player-updated', () => {
handleNewVideoLoad();
}, true);
}, true);
}
applySettings().then(main);
})();