YouTube hotkeys

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

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

/**
 * Hotkeys are:
 * numpad + and - for +-0.25x speed (shift = +-0.5, alt = +-1);
 * 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 D = 100; // milliseconds delay for YouTube elements to pop up (maybe MutationObserver or direct player access is better)
    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|Nochmal|Revoir|Ver de nuevo/i;

    function dc(element) { element.dispatchEvent(new Event("click")); } // dispatch click
    function cn(className) { return document.getElementsByClassName(className)[0]; } // (get by) classname
    function qfunc(acs) { // quality selection function: ctrl+click = 720p, shift+click = 1080p. Highest resolution is chosen if it's lower.
        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 res = +item.textContent.match(/\d+/) || 0;
            if (res <= desired)
                return dc(item);
        }
    }
    function dcList(list, regexp) {
        for (var i = 0; i < list.length; i++)
            if (regexp.test(list[i].textContent))
                return dc(list[i]);
    }
    function changeSpeed(shift) {
        var list = document.getElementsByClassName("ytp-panel-menu")[1].children;
        for (var i = 0; i < list.length; i++)
            if (list[i].ariaChecked == "true")
                return dc(list[Math.max(0, Math.min(list.length - 1, i + shift))]);
    }

    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 sm    = cn("ytp-settings-menu");        // settings menu (easily checked for being open/closed)
        var rebtn = cn("ytp-play-button");          // replay button

        switch (key) {
        case 81: // q for quality
            if (sm.style.display != "none") return dc(sb);
            dc(sb);
            setTimeout(() => {
                dcList(list, REGEXP_Q);
                if (acs) setTimeout(() => qfunc(acs), D);
            }, D);
            break;
        case 83: // s for speed
            if (sm.style.display != "none") return dc(sb);
            dc(sb);
            setTimeout(() => dcList(list, REGEXP_S), D);
            break;
        case 88: // x for subtitles
            if (sm.style.display != "none") 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 (sm.style.display != "none") 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
        }
    });
})();