// ==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 // @description Automcatically switches to your pre-selected resolution. Enables premium when possible. Tested on the newest beta premium-only UI. // @version 2024.06.28 // @match https://*.youtube.com/* // @noframes // @grant GM.getValue // @grant GM.setValue // @license MIT // @downloadURL none // ==/UserScript== // By default only runs on youtube website, not players embedded on other websites, but there is experimental support for embeds. // To enable experimental support for embedded players outside of YouTube website, do the following steps: // add " @include * " to the script metadata // remove " @noframes " from the script metadata (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. changeResolution: true, 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 sses 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) { win.setTimeout(setResolution, 100, ytPlayer); } // 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 target = settings.targetRes; let limitList = ytPlayer.getAvailableQualityLevels(); let limit = limitList[0]; 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 usePremium = settings.preferPremium && ytPlayer.getAvailableQualityData().some(q => q.quality == target && q.qualityLabel.includes("Premium") && q.isPlayable); // Premium quality does not have an direct API call so using emulated clicks instead if (usePremium) { let resLabel = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes("Premium")).qualityLabel; let settingsButton = doc.querySelector(".ytp-settings-button:not(#ScaleBtn)"); settingsButton.click(); let qualityMenuButton = document.evaluate('.//*[contains(text(),"Quality")]/ancestor-or-self::*[@class="ytp-menuitem-label"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; qualityMenuButton.click(); let qualityButton = document.evaluate('.//*[contains(text(),"' + resLabel + '") and not(@class)]/ancestor::*[@class="ytp-menuitem"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; qualityButton.click(); debugLog("Resolution Set To: " + target + (usePremium ? " Premium" : "")); return; } ytPlayer.setPlaybackQualityRange(target); debugLog("Resolution Set To: " + target); } // --- MAIN ----------- function main() { if (!settings.changeResolution) { return; } let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0]; let vidId = ytPlayer.getVideoData().video_id; setResolution(ytPlayer); win.addEventListener("loadstart", function(e) { if (!(e.target instanceof win.HTMLMediaElement)) { return; } if (vidId == ytPlayer.getVideoData().video_id) { debugLog("Duplicated loading of same video"); return; } debugLog("new video"); 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(); }); })();