// ==UserScript==
// @name Youtube HD Premium
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAqhJREFUaEPtmc9rE0EUxz+DjSh6UAQRxP4F9uhBRKjipef+FwqtoZdYEk3U4jGn0FJ6KrQnj6X0EKVKKIi9tAotPZSCYilFoq0/sK1Z92V329XGENiZSRZ2LtllZ9+8z/e9ncy8UcS8qZj7TwLQ7ggmEUgiEFGB/6aQAxeBq8Al4GxonDPAydD9+dB1qkFfefy9iZ9fgRrwC/jh96v6vz+Bj8B7BduNbBwDcOA6UABuAyciCqTr9d/ACxf0oYI3YaOHAA71KfWpq8QDF6BTP27H9/GRArk+ctSBZ0BGl2SG7YwoyB4COF66lDtY+X/1EPVvKXhVTxUHKsANw6rpNl9RcFM50A1sxEj9QAiJQrcA9LvT5XPd8liy1y8Ad4GSpQF1D3NPAO4DRd2WLdlL6wUYH4dKBSYnLfmPZoDZWejrg/l5GByE5WXTIIYAxO1aDaamYGgIthsuY3TAGQQI3KtWoVCAUgkODnQ4HbZhASAYbnUV0mmYm9MJYREgcHtmxvs+1td1gLQBQNze24OxMchmYXc3CkibAOQDl6k2k4GtrZgBLC56KbSwEMXx4F2LEdjchHweJia8KVZPswCwvw+jo5DLwc6OHrePrBgGKJdhYABWVnQ7bjiF1ta8OV+WFmab5ghMT8PSEhSL3lRpvmkGSKVAct5eqwPEfkMT+y3lZeBDbDf1kq6xLqv4AL3AyxhFQUoqvQpeh2ujI+46cdjeBBJppL9Li34UBCYP5Do4ErKIeiLV82PF3UAPB64Bj4E7biW4K5JO+l6WvajUbqW8/jZsttkBxwWgB7gCnPZfCg4z5P6UH6lzTfyUgxGp7ctBRdBkBxNsjiWXv4Seyd93+DDkG/AJeKfgc6NxOvUcoOXYJQAtS2WoYxIBQ8K2bDaJQMtSGer4B8aT1sve/dr7AAAAAElFTkSuQmCC
// @author ElectroKnight22
// @namespace electroknight22_youtube_hd_namespace
// @version 2024.06.31.3
// @match *://www.youtube.com/*
// @grant GM.getValue
// @grant GM.setValue
// @license MIT
// @description Automcatically switches to your pre-selected resolution. Enables premium when possible.
// @description:zh-TW 自動切換到你預先設定的畫質。會優先使用Premium位元率。
// @description:zh-CN 自动切换到你预先设定的画质。会优先使用Premium比特率。
// @description:ja 自動的に設定した画質に替わります。Premiumのビットレートを優先的に選択します。
// ==/UserScript==
(function() {
"use strict";
// --- SETTINGS -------
// PLEASE NOTE:
// Settings will be saved the first time the script is loaded so that your changes aren't undone by an update.
// If you want to make adjustments, please set "overwriteStoredSettings" to true.
// Otherwise, your settings changes will NOT have an effect because it will use the saved settings.
// After the script has next been run by loading a video with "overwriteStoredSettings" as true, your settings will be updated.
// Then after that, you can set it to false again to prevent your settings from being changed by an update.
let settings = {
// Target Resolution to always set to. If not available, the next best resolution will be used.
preferPremium: true,
targetRes: "hd2160",
// Choices for targetRes are currently:
// "highres" >= ( 8K / 4320p / QUHD )
// "hd2880" = ( 5K / 2880p / UHD+ )
// "hd2160" = ( 4K / 2160p / UHD )
// "hd1440" = ( 1440p / QHD )
// "hd1080" = ( 1080p / FHD )
// "hd720" = ( 720p / HD )
// "large" = ( 480p )
// "medium" = ( 360p )
// "small" = ( 240p )
// "tiny" = ( 144p )
// "auto" = ( auto )
// This make it so the scripts uses the settings coded here instead of a seperate save file.
overwriteStoredSettings: false
};
// --------------------
// --- GLOBALS --------
// --------------------
const DEBUG = false;
// Possible resolution choices (in decreasing order, i.e. highres is the largest):
const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto'];
// YouTube has to be at least 480x270 for the player UI
const ranks = {
highres: "10",
hd2880: "9",
hd2160: "8",
hd1440: "7",
hd1080: "6",
hd720: "5",
large: "4",
medium: "3",
small: "2",
tiny: "1",
auto: "0"
};
let doc = document, win = window;
// --------------------
function debugLog(message) {
if (DEBUG) {
console.log("YTHD DEBUG | " + message);
}
}
// --------------------
// Attempt to set the video resolution to desired quality or the next best quality
function setResolution(ytPlayer) {
if (!ytPlayer) {
return;
}
// No idea why anyone would use auto when using this script. But this handles that edge case.
if (settings.targetRes.toLowerCase() == "auto") {
ytPlayer.setPlaybackQuality("auto");
return;
}
let limitList = ytPlayer.getAvailableQualityLevels();
let limit = limitList[0];
let target = settings.targetRes;
if (ranks[target] > ranks[limit]) {
target = limit;
}
if (!limitList.includes(target)) {
for (let L of limitList) {
if (ranks[L] < ranks[target]) {
target = L;
break;
}
}
}
let premiumLabel = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes("Premium") && q.isPlayable)?.qualityLabel;
let usePremium = settings.preferPremium && premiumLabel != undefined;
debugLog(usePremium);
// Premium quality does not have an direct API call so using emulated clicks instead
if (usePremium) {
let settingsButton = doc.querySelector(".ytp-settings-button:not(#ScaleBtn)");
settingsButton.click();
let qualityArray = ytPlayer.getAvailableQualityLabels();
let xpathExpression = './/*[contains(text(),"' + qualityArray.join('") or contains(text(),"') + '")]/ancestor-or-self::*[@class="ytp-menuitem-content"]';
clickQualityMenuButton();
clickQualityButton();
function clickQualityMenuButton() {
let qualityMenuButton = document.evaluate(xpathExpression, ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (qualityMenuButton) {
qualityMenuButton.click();
} else {
setTimeout(clickQualityMenuButton, 100); // Try again after 100 milliseconds
}
}
function clickQualityButton() {
let qualityButton = document.evaluate('.//*[contains(text(),"' + premiumLabel + '") and not(@class)]/ancestor::*[@class="ytp-menuitem"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (qualityButton) {
qualityButton.click();
} else {
setTimeout(clickQualityButton, 100); // Try again after 100 milliseconds
}
}
debugLog("Resolution Set To: " + target + (usePremium ? " Premium" : ""));
return;
}
ytPlayer.setPlaybackQualityRange(target);
debugLog("Resolution Set To: " + target);
}
// --- MAIN -----------
function main() {
let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
let vidId = null;
if (ytPlayer && ytPlayer.getAvailableQualityLabels()[0]) {
vidId = ytPlayer.getVideoData().video_id;
setResolution(ytPlayer);
}
win.addEventListener("loadstart", function() {
var ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
if (!ytPlayer || !ytPlayer.getAvailableQualityLabels()[0]) {
return;
}
if (vidId == ytPlayer.getVideoData().video_id) {
return;
}
vidId = ytPlayer.getVideoData().video_id;
setResolution(ytPlayer);
}, true);
}
async function applySettings() {
if (typeof GM != 'undefined' && GM.getValue && GM.setValue) {
let settingsSaved = await GM.getValue("SettingsSaved");
if (settings.overwriteStoredSettings || !settingsSaved) {
Object.entries(settings).forEach(([k, v]) => GM.setValue(k, v));
await GM.setValue("SettingsSaved", true);
} else {
await Promise.all(
Object.keys(settings).map(k => { let newval = GM.getValue(k); return newval.then(v => [k, v]); })
).then((c) => c.forEach(([nk, nv]) => {
if (settings[nk] !== null && nk !== "overwriteStoredSettings") {
settings[nk] = nv;
}
}));
}
debugLog(Object.entries(settings).map(([k, v]) => k + ": " + v).join(", "));
}
}
applySettings().then(() => {
main();
});
})();