您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
quick hotkeys for quality, speed, time jumps, subtitles and replay (for RU, UA, EN, DE, FR, SP)
// ==UserScript== // @name YouTube hotkeys // @namespace http://tampermonkey.net/ // @version 1.3.2 // @description quick hotkeys for quality, speed, time jumps, subtitles and replay (for RU, UA, EN, DE, FR, SP) // @author Roman Sergeev aka naxombl4 aka Brutal Doomer // @license MIT // @match https://www.youtube.com/* // @grant GM_addStyle // @icon https://www.youtube.com/s/desktop/78bc1359/img/logos/favicon_144x144.png // ==/UserScript== /** * Happy 20 years of YouTube! * v1.3.2 added time jumps (like +-5s, +-10s) * !WARNING! this overrides browser's default execution of alt+(left/right arrow) for navigation, feel free to comment after : sign on line 248 * v1.3.1 organized Player into a class * prevented popup duplication * added UA * v1.3 changed script from keypress emulation to direct player access (as it should have always been) * added popup to indicate speed/quality change * added mutation observer to track that correct player settings are loaded * added some common code structure * 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. * * 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; * shift+(left/right arrow) for +-30s, alt+(left/right arrow) for +-1min, alt+shift+(left/right arrow) for +-5min * q, s and x for quality, speed and subtitle menus (just open, no choosing); * r for replay when video is over (video container should be active, unreliable); * speed change is rounded to the nearest multiplier of 0.25 (1.4 -> "+" -> 1.5; 1.4 -> "-" -> 1.25) * * If you have feedback, post it as a comment on my channel (The Brutal Doomer) for the quickest response. */ (function() { 'use strict'; // speed/quality popup copycat (at the middle top of the player) const POPUP_ID = "customPopup"; GM_addStyle(` div#`+POPUP_ID+` { position: absolute; top: 10%; left: 50%; transform: translatex(-50%); z-index: 10; padding: 10px 12px; border-radius: 3px; font-size: 18px; color: white; background-color: #000a; pointer-events: none; opacity: 0; } `); const PLAYER_ID = "movie_player"; const REGEXP_R = /Повтор|Повторити|Replay|Nochmal|Revoir|Ver de nuevo/i; const DISPATCH = { "81": /Качество|Якість|Quality|Qualität|Qualité|Calidad/i, // q: quality regexp "83": /Скорость|Швидкість|Speed|Wiedergabegeschwindigkeit|Vitesse de lecture|Velocidad de reproducción/i, // s: speed regexp "88": /Субтитры|Субтитри|Subtitles|Untertitel|Sous-titres|Subtítulos/i // x: subtitles regexp } const CONSECUTIVE_CLICK_EMULATION_DELAY = 100; // milliseconds delay for YouTube elements to pop up (some hotkeys call the player menus) const SPEEDMIN = 0.25; const SPEEDMAX = 2; const SPEEDALT = 0.25; const TIMESHIFT = [ [undefined, "error"], [55 , " 1 minute"], // account for 5 seconds already included. Spaces in text are en spaces (U+2002) [25 , "30 seconds"], [295, " 5 minutes"] ]; var player; function Player(div) { const QUALITY = { large: "480p", medium: "360p", small: "240p", tiny: "144p", auto: "auto" } const POPUP_STATIC_SHOWN_FOR = 300; const TIME_SHIFT_SHOWN_FOR = 700; const ALIGN_TOP = h => h/2 + 5; // time shift bubble positioning functions depending on player scroll width and height const ALIGN_LEFT_LEFT = w => w * 0.1 - 15; // reverse ingeneered (linearly interpolated) const ALIGN_LEFT_RIGHT = w => w * 0.8 - 30; var levels = div.getAvailableQualityLevels(); // array of quality strings inside YT player: see QUALITY keys var qualityTexts = levels.map(x => { // corresponding array of displayed "selected quality" strings var m = x.match(/\d+/); return m ? (m[0] + "p") : (QUALITY[x] || "null"); }); // copycat of YT default speed/volume top player popup. Feedbacks change of speed/quality. var popup = false; var popupHidingTimeout; for (const child of div.children) // search for it to prevent duplication if (child.id === POPUP_ID) { popup = child; break; } if (!popup) { popup = document.createElement("div"); popup.id = POPUP_ID; div.appendChild(popup); } var list = cn("ytp-panel-menu").children; // list of main settings items var sb = cn("ytp-settings-button"); // settings button - main to click on var rebtn = cn("ytp-play-button"); // replay button var jumpBubbleHolder = cn("ytp-doubletap-ui-legacy"); var jumpBubble = cn("ytp-doubletap-static-circle"); var jumpText = cn("ytp-doubletap-tooltip-label"); function cn(className) { var cns = document.getElementsByClassName(className); return cns[cns.length - 1]; } // (get by) classname function sopen() { return cn("ytp-settings-menu").style.display != "none"; } // whether settings window is open function dc(element) { element.dispatchEvent(new Event("click")); } // dispatch click function tev(element, show) { element.style.display = show ? "block" : "none"; } // toggle element visibility function dcList(regexp) { for (var i = 0; i < list.length; i++) if (regexp.test(list[i].textContent)) return dc(list[i]); } this.showPopup = function(text) { popup.textContent = text; popup.style.transition = "none"; // a trick to show it immediately, but hide with fade-out popup.style.opacity = 1; setTimeout(() => { popup.style.transition = "opacity 0.3s ease-out"; popup.style.opacity = 0; }, POPUP_STATIC_SHOWN_FOR); } this.setQualityByText = function(text) { var index = levels.indexOf(text); if (index == -1) index = 0; // choose the best if selected isn't present div.setPlaybackQualityRange(levels[index]); this.showPopup(qualityTexts[index]); } this.shift = function(delta, text) { div.seekTo(div.getCurrentTime() + delta, true); const right = delta > 0; const w = div.scrollWidth, h = div.scrollHeight; tev(jumpBubbleHolder, true); jumpBubbleHolder.setAttribute("data-side", right ? "forward" : "back"); jumpBubble.style.top = ALIGN_TOP(h) + "px"; // for some reason, is set on YT in absolute numbers jumpBubble.style.left = (right ? ALIGN_LEFT_RIGHT(w) : ALIGN_LEFT_LEFT(w)) + "px"; jumpText.textContent = text; clearTimeout(popupHidingTimeout); popupHidingTimeout = setTimeout(() => tev(jumpBubbleHolder, false), TIME_SHIFT_SHOWN_FOR); } this.isActive = function() { return document.activeElement === div; } this.getSpeed = function() { return div.getPlaybackRate(); } this.setSpeed = function(speed) { div.setPlaybackRate(speed); } this.dispatchReplayButtonClick = function() { if (REGEXP_R.test(rebtn.title)) dc(rebtn); } this.dispatchSettingsButtonClick = function() { dc(sb); } this.dispatchSettingsItemClick = function(regexp) { dcList(regexp); } this.settingsAreOpen = function() { return sopen(); } this.closeSettings = function() { if (this.settingsAreOpen()) this.dispatchSettingsButtonClick(); } return this; } // floor1 and ceil1 are internal for round1 - round playback speed to nearest multiplier of SPEEDALT function floor1(x, step) { return Math.floor(x / step) * step; } function ceil1(x, step) { return Math.ceil (x / step) * step; } function round1(x, delta) { return (delta > 0) ? Math.min(SPEEDMAX, floor1(x + delta, SPEEDALT)) : Math.max(SPEEDMIN, ceil1(x + delta, SPEEDALT)); } // ChatGPT helped with this: observer waits for a real player (on /watch page) to load, then loads its available quality levels function awaitPlayerLoad(callback) { function isRealPlayer(div) { return div && typeof div.getAvailableQualityLevels === 'function' && div.getAvailableQualityLevels().length > 0; } const tryInitialize = () => { const div = document.getElementById(PLAYER_ID); if (isRealPlayer(div)) { callback(div); return true; } return false; }; if (tryInitialize()) return; // Set up observer to watch for the player appearing const observer = new MutationObserver((mutations, obs) => { if (tryInitialize()) obs.disconnect(); }); observer.observe(document.body, { childList: true, subtree: true }); } function playerLoadedCallback(div) { player = new Player(div); } function clickMenu(regexp) { player.dispatchSettingsButtonClick(); if (player.settingsAreOpen()) setTimeout(() => player.dispatchSettingsItemClick(regexp), CONSECUTIVE_CLICK_EMULATION_DELAY); } // quality selection function: alt+click = 480p, ctrl+click = 720p, shift+click = 1080p. Highest resolution available up to desirable is selected. function changeQuality(asc) { var desiredQuality = (asc&2) ? "hd1080" : (asc&4) ? "hd720" : (asc&1) ? "large" : -1; player.setQualityByText(desiredQuality); } // v1.3 simplified to using player API function changeSpeed(shift) { var oldSpeed = player.getSpeed(); var newSpeed = round1(oldSpeed, shift); player.setSpeed(newSpeed); player.showPopup(newSpeed + "x"); } function changeTime(forward, asc) { if (!asc || asc&4) return; var delta = TIMESHIFT[asc][0] * (forward ? 1 : -1); var text = TIMESHIFT[asc][1]; console.log(delta); console.log(text); player.shift(delta, text); } // add global hotkeys window.addEventListener("keydown", function (event) { if (!player) return; if (document.activeElement.id == "contenteditable-root" || document.activeElement.id == "search") return; // whether writing a comment or searching var asc = event.altKey + (event.shiftKey << 1) + (event.ctrlKey << 2); var key = event.keyCode; switch (key) { case 37: // left and right with alt/ctrl/shift for advanced jumps case 39: if (asc&1) event.preventDefault(); // comment out after colon sign to restore browser default behavior on alt+arrow return changeTime(key == 39, asc); case 81: // q for quality if (asc) changeQuality(asc); case 83: // s for speed case 88: // x for subtitles return !asc && clickMenu(DISPATCH[key]); case 107: case 109: // + and - for speed. Default +(=) and - are untouched, because in YT they change captions size if (asc&4) return; // do not conflict with default zoom (ctrl plus/minus) var shift = (key == 109 ? -SPEEDALT : SPEEDALT) * Math.max(1, event.shiftKey * 2 + event.altKey * 4); // any formula you want changeSpeed(shift); player.closeSettings(); break; //case 68: if(!acs) cn("html5-video-info-panel").classList.toggle("displayNone"); break; // doesn't work (stats for nerds) case 82: if(!asc && player.isActive()) player.dispatchReplayButtonClick(); // replay (sometimes works) } }); window.addEventListener('yt-navigate-finish', () => { awaitPlayerLoad(playerLoadedCallback); }); awaitPlayerLoad(playerLoadedCallback); })();