// ==UserScript==
// @name YouTube Auto High Quality
// @namespace https://www.youtube.com
// @license GPL-3.0
// @version 1.0.0
// @description Auto select the highest quality on YouTube (incl. Premium, if applicable)
// @author CJMAXiK
// @license GPL-3.0
// @match https://*.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @website https://gist.github.com/cjmaxik/889bdc983f95d1b589464a655e0ce5bf
// @grant none
// ==/UserScript==
// Partially ported from the Chrome extention by Avi
// Code: https://github.com/avi12/youtube-auto-hd
// Avi: https://avi12.com/
const SELECTORS = {
title: "title",
video: "video",
buttonSettings: ".ytp-settings-button",
sizeToggle: ".ytp-size-button#original-size, .ytp-size-button",
optionQuality: ".ytp-settings-menu[data-layer] .ytp-menuitem:last-child",
menuOption: ".ytp-settings-menu[data-layer] .ytp-menuitem",
menuOptionContent: ".ytp-menuitem-content",
panelHeaderBack: ".ytp-panel-header button",
player: ".html5-video-player:not(#inline-preview-player)",
donationInjectParent: "ytd-comments",
// Premium
labelPremium: ".ytp-premium-label",
};
const OBSERVER_OPTIONS = {
childList: true,
subtree: true,
};
const SUFFIX_EBR = "ebr";
let qualityChanged = false;
function isElementVisible(element) {
return element?.offsetWidth > 0 && element?.offsetHeight > 0;
}
async function getCurrentQualityElements() {
const elPlayer = await waitElement(SELECTORS.player);
const elMenuOptions = [...elPlayer.querySelectorAll(SELECTORS.menuOption)];
return elMenuOptions.filter(getIsQualityElement);
}
function convertQualityToNumber(elQuality) {
const isPremiumQuality = Boolean(
elQuality.querySelector(SELECTORS.labelPremium)
);
const qualityNumber = parseInt(elQuality.textContent.substring(0, 4));
if (isPremiumQuality) {
return qualityNumber + SUFFIX_EBR;
}
return qualityNumber;
}
async function getAvailableQualities() {
const elQualities = await getCurrentQualityElements();
const qualities = elQualities.map(convertQualityToNumber);
return qualities;
}
function getPlayerDiv(elVideo) {
const elPlayer = elVideo.closest(SELECTORS.player);
if (!elPlayer) {
console.warn(
"Player div not found. Is the video element correct?",
elVideo,
elVideo.parentElement
);
}
return elPlayer;
}
function getIsQualityElement(element) {
const isQuality = Boolean(element.textContent.match(/\d/));
const isHasChildren = element.children.length > 1;
return isQuality && !isHasChildren;
}
async function getIsSettingsMenuOpen() {
const elButtonSettings = await waitElement(SELECTORS.buttonSettings);
return elButtonSettings?.ariaExpanded === "true";
}
function getIsLastOptionQuality(elVideo) {
const elOptionInSettings = getPlayerDiv(elVideo).querySelector(
SELECTORS.optionQuality
);
if (!elOptionInSettings) {
return false;
}
const elQualityName = elOptionInSettings.querySelector(
SELECTORS.menuOptionContent
);
// If the video is a channel trailer, the last option is initially the speed one,
// and the speed setting can only be a single digit
const matchNumber = elQualityName?.textContent?.match(/\d+/);
if (!matchNumber) {
return false;
}
const numberString = matchNumber[0];
const minQualityCharLength = 3; // e.g. 3 characters in 720p
return numberString.length >= minQualityCharLength;
}
async function changeQualityAndClose(elVideo, elPlayer) {
await changeQualityWhenPossible(elVideo);
await closeMenu(elPlayer);
qualityChanged = true;
}
async function changeQualityWhenPossible(elVideo) {
console.log("Trying...");
openQualityMenu(elVideo);
await changeQuality();
}
async function changeQuality() {
const elQualities = await getCurrentQualityElements();
const qualitiesAvailable = await getAvailableQualities();
const applyQuality = (iQuality) => {
const quality = qualitiesAvailable[iQuality];
console.log(`Setting up the ${quality}`);
elQualities[iQuality]?.click();
};
const isQualityPreferredEBR = qualitiesAvailable[0]
.toString()
.endsWith(SUFFIX_EBR);
if (isQualityPreferredEBR) {
applyQuality(0);
return;
}
const iQualityFallback = qualitiesAvailable.findIndex(
(quality) => !quality.toString().endsWith(SUFFIX_EBR)
);
applyQuality(iQualityFallback);
}
async function closeMenu(elPlayer) {
const clickPanelBackIfPossible = () => {
const elPanelHeaderBack = elPlayer.querySelector(SELECTORS.panelHeaderBack);
if (elPanelHeaderBack) {
elPanelHeaderBack.click();
return true;
}
return false;
};
if (clickPanelBackIfPossible()) {
return;
}
const observer = new MutationObserver((_, observer) => {
if (clickPanelBackIfPossible()) {
observer.disconnect();
}
});
observer.observe(elPlayer, OBSERVER_OPTIONS);
}
async function openQualityMenu(elVideo) {
const elSettingQuality = getPlayerDiv(elVideo).querySelector(
SELECTORS.optionQuality
);
elSettingQuality.click();
}
function waitElement(selector) {
return new Promise((resolve) => {
let element = [...document.querySelectorAll(selector)].find(
isElementVisible
);
if (element) {
return resolve(element);
}
const observer = new MutationObserver((mutations) => {
let element = [...document.querySelectorAll(selector)].find(
isElementVisible
);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, OBSERVER_OPTIONS);
});
}
async function setEventListeners(event) {
if (qualityChanged) return;
const elVideo = document.querySelector(SELECTORS.video);
if (!elVideo) {
console.error("Auto HD: Video element was not found.");
return;
}
const elPlayer = getPlayerDiv(elVideo);
if (!elPlayer) {
console.error("Auto HD: Player div was not found.");
return;
}
const elSettings = elPlayer.querySelector(SELECTORS.buttonSettings);
if (!elSettings) {
console.error("Auto HD: Settings button was not found.");
return;
}
const isSettingsMenuOpen = await getIsSettingsMenuOpen();
if (!isSettingsMenuOpen) {
elSettings.click();
}
elSettings.click();
await changeQualityAndClose(elVideo, elPlayer);
elPlayer.querySelector(SELECTORS.buttonSettings).blur();
}
(function () {
"use strict";
window.addEventListener(
"yt-navigate-start",
() => (qualityChanged = false),
true
);
window.addEventListener("yt-navigate-finish", setEventListeners, true);
setEventListeners();
})();
// document.addEventListener("yt-navigate-start", function () {
// console.log("document.yt-navigate-start", arguments);
// });
// document.addEventListener("yt-navigate-finish", function () {
// console.log("document.yt-navigate-finish", arguments);
// });
// document.addEventListener("yt-navigate-error", function () {
// console.log("document.yt-navigate-error", arguments);
// });
// document.addEventListener("yt-navigate-redirect", function () {
// console.log("document.yt-navigate-redirect", arguments);
// });
// document.addEventListener("yt-navigate-cache", function () {
// console.log("document.yt-navigate-cache", arguments);
// });
// document.addEventListener("yt-navigate-action", function () {
// console.log("document.yt-navigate-action", arguments);
// });
// document.addEventListener("yt-navigate-home-action", function () {
// console.log("document.yt-navigate-home-action", arguments);
// });
// document.addEventListener("yt-page-data-fetched", function () {
// console.log("document.yt-page-data-fetched", arguments);
// });