您需要先安装一个扩展,例如 篡改猴、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.2.0 // @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倍速まで対応させます。 // @homepageURL https://greasyfork.org/scripts/503771-youtube-remember-speed // ==/UserScript== /*jshint esversion: 11 */ (function () { 'use strict'; class SpeedOverrideManager { constructor(maxSpeed, getSettings, setSpeed, updateSavedSpeed) { this.maxSpeed = maxSpeed; this.getSettings = getSettings; this.setSpeed = setSpeed; this.updateSavedSpeed = updateSavedSpeed; this.DOM = { speedTextElement: null, speedLabel: null, parentMenu: null, }; this.mainObserver = null; this.observerOptions = { childList: true, subtree: true, characterData: true }; } disconnect() { if (this.mainObserver) { this.mainObserver.disconnect(); } this.mainObserver = null; } overrideSpeedText(targetString) { try { if (!this.DOM.speedTextElement || !this.DOM.speedTextElement.isConnected) return; const text = this.DOM.speedTextElement.textContent; this.DOM.speedTextElement.textContent = /\(.*?\)/.test(text) ? text.replace(/\(.*?\)/, `(${targetString})`) : targetString; } catch (error) { console.error('Failed to set speed text.', error); } } overrideSpeedLabel(sliderElement) { try { const speedLabel = this.DOM.speedLabel; if (speedLabel?.isConnected && speedLabel.textContent && !speedLabel.textContent.includes(`(${sliderElement.value})`)) { speedLabel.textContent = 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}%;`; const speedSliderText = document.querySelector('.ytp-speedslider-text'); if (speedSliderText) { speedSliderText.textContent = sliderElement.value + 'x'; } } overrideCustomSpeedItem(sliderElement) { const speedMenuItems = sliderElement.closest('.ytp-panel-menu'); if (!speedMenuItems) return; const customSpeedItem = speedMenuItems.children[0]; if (!customSpeedItem) return; if (!customSpeedItem.dataset.customSpeedClickListener) { customSpeedItem.addEventListener('click', () => { const newSpeed = parseFloat(sliderElement.value); this.setSpeed(newSpeed); this.updateSavedSpeed(newSpeed); this.overrideSpeedText(newSpeed); }); customSpeedItem.dataset.customSpeedClickListener = 'true'; } 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'); } } 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.overrideSpeedText(sliderElement.value); event.stopImmediatePropagation(); }, true, ); } setupSliderOnce(sliderElement) { if (!sliderElement) return; if (!sliderElement.dataset.listenerAttached) { this.overrideSliderFunction(sliderElement); sliderElement.dataset.listenerAttached = 'true'; } sliderElement.max = this.maxSpeed.toString(); sliderElement.setAttribute('value', this.getSettings().targetSpeed.toString()); this.setSpeed(this.getSettings().targetSpeed); this.DOM.speedLabel = sliderElement.closest('.ytp-menuitem-with-footer')?.querySelector('.ytp-menuitem-label'); } async findSpeedTextElement() { // We use a magic number to manually create a stable selector for the speed menu item. const magicSpeedNumber = 1.05; const pollForElement = () => { const settingItems = document.querySelectorAll('.ytp-menuitem'); return Array.from(settingItems).find((item) => item.textContent.includes(magicSpeedNumber.toString())); }; const targetSpeed = this.getSettings().targetSpeed; const youtubeApi = document.querySelector('#movie_player'); // YouTube API must be called here regardless to allow the YouTube UI to updated normally. (reason unclear) youtubeApi.setPlaybackRate(magicSpeedNumber); let attempts = 0; while (attempts < 50) { 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.overrideSpeedText(targetSpeed); } _monitorUI() { const sliderElement = document.querySelector('input.ytp-input-slider.ytp-speedslider'); if (sliderElement) { if (!sliderElement.dataset.speedScriptSetup) { this.setupSliderOnce(sliderElement); sliderElement.dataset.speedScriptSetup = 'true'; } if (!this.DOM.speedLabel || !this.DOM.speedLabel.isConnected) { this.DOM.speedLabel = sliderElement.closest('.ytp-menuitem-with-footer')?.querySelector('.ytp-menuitem-label'); } const speedLabel = this.DOM.speedLabel; const expectedLabel = `(${sliderElement.value})`; if (speedLabel && !speedLabel.textContent.includes(expectedLabel)) { this.mainObserver.disconnect(); this.overrideSliderStyle(sliderElement); this.overrideCustomSpeedItem(sliderElement); this.mainObserver.observe(this.DOM.parentMenu, this.observerOptions); } return; } if (!this.DOM.speedTextElement || !this.DOM.speedTextElement.isConnected) { this.findSpeedTextElement(); } else { const currentText = this.DOM.speedTextElement.textContent; const targetSpeed = this.getSettings().targetSpeed.toString(); if (!currentText.includes(targetSpeed)) { this.mainObserver.disconnect(); this.overrideSpeedText(targetSpeed); this.mainObserver.observe(this.DOM.parentMenu, this.observerOptions); } } } init() { try { this.DOM.parentMenu = document.querySelector('.ytp-popup.ytp-settings-menu'); if (!this.DOM.parentMenu) return; this.mainObserver = new MutationObserver(this._monitorUI.bind(this)); this.mainObserver.observe(this.DOM.parentMenu, this.observerOptions); } 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) { video.playbackRate = targetSpeed; } } catch (error) { console.error('Failed to set playback speed.', error); } } function updateSavedSpeed(speed) { const newSpeed = parseFloat(speed); if (userSettings.targetSpeed !== newSpeed) { userSettings.targetSpeed = newSpeed; GM.setValue('targetSpeed', userSettings.targetSpeed); } } async function applySettings() { try { const storedSpeed = await GM.getValue('targetSpeed', DEFAULT_SETTINGS.targetSpeed); userSettings.targetSpeed = storedSpeed; } catch (error) { console.error('Failed to apply stored settings.', error.message); } } function handleNewVideoLoad() { if (!manager) { manager = new SpeedOverrideManager(maxSpeed, () => userSettings, setSpeed, updateSavedSpeed); } setSpeed(userSettings.targetSpeed); manager.init(); const youtubeApi = document.querySelector('#movie_player'); if (youtubeApi && !youtubeApi.dataset.speedScriptListenerAttached) { youtubeApi.addEventListener('onPlaybackRateChange', (newSpeed) => { updateSavedSpeed(newSpeed); if (manager) { manager.overrideSpeedText(newSpeed); } }); youtubeApi.dataset.speedScriptListenerAttached = 'true'; } } function main() { window.addEventListener( 'pageshow', () => { handleNewVideoLoad(); window.addEventListener( 'yt-player-updated', () => { if (manager) { manager.disconnect(); manager = null; } handleNewVideoLoad(); }, true, ); }, true, ); } applySettings().then(main); })();