Greasy Fork is available in English.

YouTube hotkeys

quick hotkeys for quality, speed, subtitles and replay (for RU, EN, DE, FR, SP)

// ==UserScript==
// @name         YouTube hotkeys
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  quick hotkeys for quality, speed, subtitles and replay (for RU, EN, DE, FR, SP)
// @author       Roman Sergeev aka naxombl4 aka Brutal Doomer
// @license      MIT
// @match        https://www.youtube.com/*
// ==/UserScript==

/**
 * v1.2 fixed wrong behavior after YT interface updates
 *      moved surveyLoadProgress to another script
 *      fixed subtitles regexp
 *      added comments
 * v1.1 x hotkey for subtitles, alt+q for 480p quality, plus some readability alignments.
 *
 * Marker is added at the end of loaded video part (gray bar) to indicate loaded part more clearly.
 * Hotkeys are:
 * numpad + and - for +-0.25x speed (shift = +-0.5, alt = +-1);
 * s to open playback speed menu
 * shift+q for 1080p quality, ctrl+q for 720p quality, alt+q for 480p quality;
 * x to open subtitles menu;
 * r for replay when video is over (video container should be active);
 * q and s for quality and speed menus (just open, no choosing);
 * My first script posted, not familiar with TM's and YouTube's tricks, so eager to see informative feedback!
 */

(function() {
    'use strict';

    const RES_HI   = 1080;
    const RES_MID  =  720;
    const RES_LOW  =  480;
    const REGEXP_Q = /Качество|Quality|Qualität|Qualité|Calidad/i;
    const REGEXP_S = /Скорость|Speed|Wiedergabegeschwindigkeit|Vitesse de lecture|Velocidad de reproducción/i;
    const REGEXP_R = /Повтор|Replay|Nochmal|Revoir|Ver de nuevo/i;
    const REGEXP_X = /Субтитры|Subtitles|Untertitel|Sous-titres|Subtítulos/i;
    const D        = 100; // milliseconds delay for YouTube elements to pop up (maybe MutationObserver or direct player access is better)

    function cn(className) { var cns = document.getElementsByClassName(className); return cns[cns.length - 1]; } // (get by) classname
    function settingsOpened() { return cn("ytp-settings-menu").style.display != "none"; }
    function dc(element) { element.dispatchEvent(new Event("click")); } // dispatch click

    function dcList(list, regexp) {
        for (var i = 0; i < list.length; i++)
            if (regexp.test(list[i].textContent))
                return dc(list[i]);
    }

    function selectedSpeedIndex(list) {
        for (var i = 0; i < list.length; ++i)
            if (list[i].getAttribute("aria-checked") === "true") // can also use list[i].ariaChecked
                return i;
        return 0;
    }

    // quality selection function: alt+click = 480p, ctrl+click = 720p, shift+click = 1080p. Highest resolution available up to desirable is selected.
    function qfunc(acs) {
        var qlist = cn("ytp-quality-menu").children[1]; // list of options to choose from
        var desired = (acs&4) ? RES_HI : (acs&2) ? RES_MID : (acs&1) ? RES_LOW : -1;
        for (var i = 0; i < qlist.children.length; i++) {
            var item = qlist.children[i];
            var premium = item.firstChild?.firstChild?.firstChild?.childElementCount;
            if (premium > 1) continue; // v1.2: ignore "1080p premium" and other categories with enhanced bitrate
            var res = +item.textContent.match(/\d+/) || 0;
            if (res <= desired)
                return dc(item);
        }
    }

    // v1.2 enhanced speed changing algorithm, based on current, possibly custom, speed
    function changeSpeed(shift) {
        var list = document.getElementsByClassName("ytp-panel-menu")[1].children;
        var currentIndex = selectedSpeedIndex(list);
        if (currentIndex == 0) { // obtain current video speed from the "Custom" list element, since it can be anything from 0.25 to 2
            currentIndex = list[0].firstChild?.firstChild?.textContent;
            if (!currentIndex) return;
            currentIndex = currentIndex.match(/[\d\.]+/);
            if (!currentIndex) return;
            currentIndex = +currentIndex[0] * 4;
        }
        var indexFrom = shift > 0 ? Math.floor(currentIndex) : Math.ceil(currentIndex); // example: from 0.65, "+" -> 0.75, "-" -> 0.5
        var newIndex = Math.max(1, Math.min(8, indexFrom + shift));
        return dc(list[newIndex]);
    }

    // add global hotkeys
    window.addEventListener("keydown", function (event) {
        if (document.activeElement.id == "contenteditable-root" ||
            document.activeElement.id == "search") return; // whether writing a comment or searching

        var acs   = event.altKey + (event.ctrlKey << 1) + (event.shiftKey << 2);
        var key   = event.keyCode;
        var sb    = cn("ytp-settings-button");      // settings button - main to click on
        var list  = cn("ytp-panel-menu").children;  // list of main settings items
        var rebtn = cn("ytp-play-button");          // replay button
        var sopen = settingsOpened();

        switch (key) {
        case 81: // q for quality
            if (sopen) return dc(sb);
            dc(sb);
            setTimeout(() => {
                dcList(list, REGEXP_Q);
                if (acs) setTimeout(() => qfunc(acs), D);
            }, D);
            break;
        case 83: // s for speed
            if (sopen) return dc(sb);
            dc(sb);
            setTimeout(() => dcList(list, REGEXP_S), D);
            break;
        case 88: // x for subtitles
            if (sopen) return dc(sb);
            dc(sb);
            setTimeout(() => dcList(list, REGEXP_X), D);
            break;
        break;
        case 107:
        case 109: // + and - for speed. Default +(=) and - are untouched, because in YT they change captions size
            if (event.ctrlKey) return; // do not conflict with default zoom
            if (sopen) return dc(sb);
            dc(sb);
            setTimeout(() => {
                dcList(list, REGEXP_S);
                var shift = (key == 109 ? -1 : 1) * Math.max(1, event.shiftKey * 2 + event.altKey * 4); // any formula you want
                setTimeout(() => {
                    changeSpeed(shift);
                    dc(sb);
                }, D);
            }, D);
            break;
        case 68: if(!acs) cn("html5-video-info-panel").classList.toggle("displayNone"); break; // doesn't work (stats for nerds)
        case 82: if(!acs && document.activeElement.classList.contains("html5-video-player") && REGEXP_R.test(rebtn.title)) dc(rebtn); // replay
        }
    });
})();