Youtube Remember Speed

Remembers the speed that you last used. Now hijacks YouTube's custom speed slider and gives you up to 8x speed.

Install this script?
Author's suggested script

Вам таксама можа спадабацца YouTube HD Premium.

Install this script
// ==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.3.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;
    const stepSize = 0.25;
    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 setupHotkeys() {

        let speedTimeoutId = null;
        const INDICATOR_DURATION_MS = 450;
        const FADE_OUT_DURATION_MS = 200;

        const _showNewSpeedVisual = () => {
            const currentSpeed = userSettings.targetSpeed;
            let indicator = document.getElementById('speed-indicator-overlay');

            if (speedTimeoutId) {
                clearTimeout(speedTimeoutId);
                speedTimeoutId = null;
            }

            if (!indicator) {
                indicator = document.createElement('div');
                indicator.id = 'speed-indicator-overlay';
                indicator.style.cssText = `
                    position: fixed;
                    top: 17%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(0, 0, 0, 0.75);
                    color: white;
                    padding: 16px 24px;
                    border-radius: 4px;
                    font-size: 3em; /* Big enough to read easily */
                    font-weight: regular;
                    z-index: 99999;
                    opacity: 0.85;
                    transition: opacity ${FADE_OUT_DURATION_MS}ms ease-out; /* Smooth fade-out */
                    pointer-events: none; /* Stops the indicator from blocking clicks */
                `;
                document.body.appendChild(indicator);
            } else {
                indicator.style.opacity = 1;
            }

            indicator.textContent = `${currentSpeed.toFixed(2)}x`;
            speedTimeoutId = setTimeout(() => {
                indicator.style.opacity = 0;
                setTimeout(() => {
                    if (document.body.contains(indicator)) {
                        document.body.removeChild(indicator);
                    }
                    speedTimeoutId = null;
                }, FADE_OUT_DURATION_MS);
            }, INDICATOR_DURATION_MS);
        };
        const _nudgeSpeed = (isIncrease) => {
            const speedChange = isIncrease ? stepSize : -stepSize;
            userSettings.targetSpeed = Math.max(stepSize, Math.min(maxSpeed, userSettings.targetSpeed + speedChange));
            setSpeed(userSettings.targetSpeed);
            updateSavedSpeed(userSettings.targetSpeed);
            _showNewSpeedVisual();
        };
        window.addEventListener('keydown', (event) => {
            if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
            if (event.key === '<') {
                event.preventDefault();
                _nudgeSpeed(false);
            } else if (event.key === '>') {
                event.preventDefault();
                _nudgeSpeed(true);
            }
        }, true); // use capture needs to be true to override existing youtube hotkeys
    }

    function main() {
        window.addEventListener(
            'pageshow',
            () => {
                handleNewVideoLoad();
                window.addEventListener(
                    'yt-player-updated',
                    () => {
                        if (manager) {
                            manager.disconnect();
                            manager = null;
                        }
                        handleNewVideoLoad();
                    },
                    true,
                );
            },
            true,
        );
        setupHotkeys();
    }

    applySettings().then(main);
})();