Greasy Fork

Greasy Fork is available in English.

YouTube 播放速度记忆

记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。

当前为 2025-08-05 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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