您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。
// ==UserScript== // @name Youtube Remember Speed // @name:zh-TW YouTube 播放速度記憶 // @name:zh-CN YouTube 播放速度记忆 // @name:ja YouTube 再生速度メモリー // @icon https://www.google.com/s2/favicons?domain=youtube.com // @author ElectroKnight22 // @namespace electroknight22_youtube_remember_playback_rate_namespace // @version 2.1.1 // @match *://www.youtube.com/* // @match *://www.youtube-nocookie.com/* // @exclude *://music.youtube.com/* // @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'; class SpeedOverrideManager { constructor(maxSpeed, getSettings, setSpeed, updateSavedSpeed) { // Dependencies are injected for better encapsulation this.maxSpeed = maxSpeed; this.getSettings = getSettings; this.setSpeed = setSpeed; this.updateSavedSpeed = updateSavedSpeed; this.DOM = { speedTextElement: null, speedLabel: null, parentMenu: null, }; this.OBSERVERS = { speedTextObserver: null, sliderObserver: null, }; this.initialized = false; } setSpeedText(targetString) { try { if (!this.DOM.speedTextElement) return; const text = this.DOM.speedTextElement.textContent; const newValue = targetString; this.DOM.speedTextElement.textContent = /\(.*?\)/.test(text) ? text.replace(/\(.*?\)/, `(${newValue})`) : newValue; } catch (error) { console.error('Failed to set speed text.', error); } } overrideSpeedLabel(sliderElement) { try { if (!this.DOM.speedLabel || !this.DOM.speedLabel.isConnected) { this.DOM.speedLabel = sliderElement.closest('.ytp-menuitem-with-footer').querySelector('.ytp-menuitem-label'); } if (this.DOM.speedLabel?.textContent && !this.DOM.speedLabel.textContent.includes(`(${sliderElement.value})`)) { this.DOM.speedLabel.textContent = this.DOM.speedLabel.textContent.replace(/\(.*?\)/, `(${sliderElement.value})`); } } catch (error) { console.error('Failed to override speed label.', error); } } overrideSliderStyle(sliderElement) { if (!sliderElement) return; this.overrideSpeedLabel(sliderElement); sliderElement.style = `--yt-slider-shape-gradient-percent: ${(sliderElement.value / this.maxSpeed) * 100}%;`; document.querySelector('.ytp-speedslider-text').textContent = sliderElement.value + 'x'; } overrideCustomSpeedItem(sliderElement) { const speedMenuItems = sliderElement.closest('.ytp-panel-menu'); const customSpeedItem = speedMenuItems.children[0]; const selectCustomSpeedItem = () => { if ( customSpeedItem?.classList.contains('ytp-menuitem-with-footer') && customSpeedItem.getAttribute('aria-checked') !== 'true' ) { const currentActiveItem = speedMenuItems.querySelector('[aria-checked="true"]'); if (currentActiveItem && currentActiveItem !== customSpeedItem) { currentActiveItem.setAttribute('aria-checked', 'false'); } customSpeedItem.setAttribute('aria-checked', 'true'); } }; if (!customSpeedItem.dataset.listenerAttached) { customSpeedItem.addEventListener('click', () => { userSettings.targetSpeed = parseFloat(sliderElement.value); this.setSpeedText(userSettings.targetSpeed); this.setSpeed(userSettings.targetSpeed); this.updateSavedSpeed(userSettings.targetSpeed); }); customSpeedItem.dataset.listenerAttached = 'true'; } selectCustomSpeedItem(); } overrideSliderFunction(sliderElement) { sliderElement.addEventListener('input', () => { try { const newSpeed = parseFloat(sliderElement.value); this.updateSavedSpeed(newSpeed); this.setSpeed(newSpeed); this.overrideSliderStyle(sliderElement); this.overrideCustomSpeedItem(sliderElement); } catch (error) { console.error('Error during slider input event.', error); } }); sliderElement.addEventListener( 'change', (event) => { this.setSpeedText(sliderElement.value); event.stopImmediatePropagation(); }, true, ); } overrideSlider() { const sliderElement = this.DOM.parentMenu.querySelector('input.ytp-input-slider.ytp-speedslider'); if (!sliderElement) throw new Error('Slider element not found.'); this.overrideSpeedLabel(sliderElement); if (sliderElement.initialized) return; const targetSpeed = this.getSettings().targetSpeed; sliderElement.max = this.maxSpeed.toString(); sliderElement.setAttribute('value', targetSpeed.toString()); this.setSpeed(targetSpeed); this.overrideSliderStyle(sliderElement); this.overrideCustomSpeedItem(sliderElement); this.overrideSliderFunction(sliderElement); if (this.OBSERVERS.sliderObserver) this.OBSERVERS.sliderObserver.disconnect(); sliderElement.initialized = true; } async findSpeedTextElement() { const magicSpeed = 1.05; const pollForElement = () => { const settingItems = document.querySelectorAll('.ytp-menuitem'); return Array.from(settingItems).find((item) => item.textContent.includes(magicSpeed.toString())); }; const targetSpeed = this.getSettings().targetSpeed; const youtubeApi = document.querySelector('#movie_player'); // YouTube's API **MUST** be called here or things break for some unknown reason. youtubeApi.setPlaybackRate(magicSpeed); const pollingLimit = 500; let attempts = 0; while (attempts < pollingLimit) { const matchingItem = pollForElement(); if (matchingItem) { this.DOM.speedTextElement = matchingItem.querySelector('.ytp-menuitem-content'); break; } await new Promise((resolve) => setTimeout(resolve, 10)); attempts++; } this.setSpeed(targetSpeed); this.updateSavedSpeed(targetSpeed); this.setSpeedText(targetSpeed); } async initializeSpeedTextElement() { try { if (this.DOM.speedTextElement) { if (this.DOM.speedTextElement.initialized) return; this.DOM.speedTextElement.initialized = true; const youtubeApi = document.querySelector('#movie_player'); youtubeApi?.addEventListener('onPlaybackRateChange', this.updateSavedSpeed, true); this.setSpeedText(this.getSettings().targetSpeed); this.OBSERVERS.speedTextObserver?.disconnect(); // Disconnect after finding } else { await this.findSpeedTextElement(); } } catch (error) { console.error('Failed to initialize speed text element.', error); } } init() { try { this.DOM.parentMenu = document.querySelector('.ytp-popup.ytp-settings-menu'); if (!this.DOM.parentMenu) throw new Error('The parent menu was not found.'); if (!this.OBSERVERS.sliderObserver || this.initialized === false) { this.OBSERVERS.sliderObserver?.disconnect(); this.OBSERVERS.sliderObserver = new MutationObserver(this.overrideSlider.bind(this)); this.OBSERVERS.sliderObserver.observe(this.DOM.parentMenu, { childList: true, subtree: true }); } if (!this.OBSERVERS.speedTextObserver || this.initialized === false) { this.OBSERVERS.speedTextObserver?.disconnect(); this.OBSERVERS.speedTextObserver = new MutationObserver(this.initializeSpeedTextElement.bind(this)); this.OBSERVERS.speedTextObserver.observe(this.DOM.parentMenu, { childList: true, subtree: true, attributes: true }); } this.initialized = true; } catch (error) { console.error('Failed to initialize speed override manager.', error); } } } // --- Main Script Logic --- const DEFAULT_SETTINGS = { targetSpeed: 1 }; let userSettings = { ...DEFAULT_SETTINGS }; const maxSpeed = 8; let manager = null; function setSpeed(targetSpeed) { try { const video = document.querySelector('video'); if (video) { video.playbackRate = targetSpeed; } } catch (error) { console.error('Failed to set playback speed.', error); } } function updateSavedSpeed(speed) { userSettings.targetSpeed = speed; GM.setValue('targetSpeed', userSettings.targetSpeed); } async function applySettings() { try { const storedSpeed = await GM.getValue('targetSpeed', DEFAULT_SETTINGS.targetSpeed); userSettings.targetSpeed = storedSpeed; console.log(`Loaded speed setting: ${userSettings.targetSpeed}`); } catch (error) { console.error('Failed to apply stored settings.', error.message); } } function handleNewVideoLoad() { if (!manager) { // Pass helper functions and settings to the class instance manager = new SpeedOverrideManager( maxSpeed, () => userSettings, // Pass a function to get the latest settings setSpeed, updateSavedSpeed, ); } setSpeed(userSettings.targetSpeed); manager.init(); } function main() { window.addEventListener( 'pageshow', () => { handleNewVideoLoad(); window.addEventListener( 'yt-player-updated', () => { if (manager) { manager.initialized = false; manager.DOM.speedTextElement = null; } handleNewVideoLoad(); }, true, ); }, true, ); } applySettings().then(main); })();