// ==UserScript== // @name Youtube HD Premium // @name:zh-TW Youtube HD Premium // @name:zh-CN Youtube HD Premium // @name:ja Youtube HD Premium // @icon https://www.youtube.com/img/favicon_48.png // @author ElectroKnight22 // @namespace electroknight22_youtube_hd_namespace // @version 2025.01.13 // I would prefer semantic versioning but it's a bit too late to change it at this point. Calendar versioning was originally chosen to maintain similarity to the adisib's code. // @match *://www.youtube.com/* // @match *://m.youtube.com/* // @match *://www.youtube-nocookie.com/* // @exclude *://www.youtube.com/live_chat* // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM.registerMenuCommand // @grant GM.unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @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のビットレートを優先的に選択します。 // @homepage https://greasyfork.org/en/scripts/498145-youtube-hd-premium // @downloadURL none // ==/UserScript== /*jshint esversion: 11 */ (function () { "use strict"; const DEFAULT_SETTINGS = { targetResolution: "hd2160", expandMenu: false, debug: false }; const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage; const GET_PREFERRED_LANGUAGE = () => { if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') { return 'zh-CN'; } else { return BROWSER_LANGUAGE; } }; const TRANSLATIONS = { 'en-US': { qualityMenu: 'Quality Menu', debug: 'DEBUG' }, 'zh-TW': { qualityMenu: '畫質選單', debug: '偵錯' }, 'zh-CN': { qualityMenu: '画质菜单', debug: '排错' }, 'ja': { qualityMenu: '画質メニュー', debug: 'デバッグ' } }; const GET_LOCALIZED_TEXT = () => { const language = GET_PREFERRED_LANGUAGE(); return TRANSLATIONS[language] || TRANSLATIONS['en-US']; }; const QUALITIES = { highres: 4320, hd2880: 2880, hd2160: 2160, hd1440: 1440, hd1080: 1080, hd720: 720, large: 480, medium: 360, small: 240, tiny: 144, }; const PREMIUM_INDICATOR_LABEL = "Premium"; let userSettings = { ...DEFAULT_SETTINGS }; let useCompatibilityMode = false; let isBrokenOrMissingGMAPI = false; let menuItems = []; let moviePlayer = null; // --- CLASS DEFINITIONS ----------- class AllowedExceptionError extends Error { constructor(message) { super(message); this.name = "Allowed Exception"; } } // --- GM FUNCTION OVERRIDES ------ const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand; const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand; const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue; const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue; const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues; const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue; // --- FUNCTIONS ------ function debugLog(message) { if (!userSettings.debug) return; const stack = new Error().stack; const stackLines = stack.split("\n"); const callerLine = stackLines[2] ? stackLines[2].trim() : "Line not found"; message += ""; if (!message.endsWith(".")) { message += "."; } console.log(`[YTHD DEBUG] ${message} Function called ${callerLine}`); } // Attempt to set the video resolution to target quality or the next best quality function setResolution(force = false) { try { if (!moviePlayer?.getAvailableQualityData().length) throw "Quality options missing."; let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer.getAvailableQualityLevels()); const premiumData = moviePlayer.getAvailableQualityData().find(q => q.quality === resolvedTarget && q.qualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL) && q.isPlayable ); moviePlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId); debugLog(`Setting quality to: ${resolvedTarget}${premiumData ? " Premium" : ""}`); } catch (error) { debugLog("Did not set resolution. " + error); } } function findNextAvailableQuality(target, availableQualities) { const targetValue = QUALITIES[target]; return availableQualities .map(q => ({ quality: q, value: QUALITIES[q] })) .find(q => q.value <= targetValue)?.quality; } function processNewPage() { debugLog('Processing new page...'); moviePlayer = document.querySelector('#movie_player'); setResolution(); } // ---------------------------------------- // Functions for the quality selection menu function processMenuOptions(options, callback) { Object.values(options).forEach(option => { if (!option.alwaysShow && !userSettings.expandMenu) return; if (option.items) { option.items.forEach(item => callback(item)); } else { callback(option); } }); } function showMenuOptions() { removeMenuOptions(); const menuOptions = { expandMenu: { alwaysShow: true, label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`, menuId: "menuExpandBtn", handleClick: function () { userSettings.expandMenu = !userSettings.expandMenu; GMCustomSetValue('expandMenu', userSettings.expandMenu); showMenuOptions(); }, }, qualities: { items: Object.entries(QUALITIES).map(([label, resolution]) => ({ label: () => `${resolution}p ${label === userSettings.targetResolution ? "✅" : ""}`, menuId: label, handleClick: function () { if (userSettings.targetResolution === label) return; userSettings.targetResolution = label; GMCustomSetValue('targetResolution', label); setResolution(); showMenuOptions(); }, })), }, debug: { label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`, menuId: "debugBtn", handleClick: function () { userSettings.debug = !userSettings.debug; GMCustomSetValue('debug', userSettings.debug); showMenuOptions(); }, }, }; processMenuOptions(menuOptions, (item) => { GMCustomRegisterMenuCommand(item.label(), item.handleClick, { id: item.menuId, autoClose: false, }); menuItems.push(item.menuId); }); } function removeMenuOptions() { while (menuItems.length) { GMCustomUnregisterMenuCommand(menuItems.pop()); } } // ----------------------------------------------- // Verify Grease Monkey API exists and is working. function hasGreasyMonkeyAPI() { if (typeof GM != 'undefined') return true; if (typeof GM_info != 'undefined') { useCompatibilityMode = true; debugLog("Running in compatibility mode."); return true; } return false; } // ----------------------------------------------- // User setting handling async function loadUserSettings() { try { // Get all keys from GM const storedValues = await GMCustomListValues(); // Write any missing key-value pairs from DEFAULT_SETTINGS to GM for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) { if (!storedValues.includes(key)) { await GMCustomSetValue(key, value); } } // Delete any extra keys in GM that are not in DEFAULT_SETTINGS for (const key of storedValues) { if (!(key in DEFAULT_SETTINGS)) { await GMCustomDeleteValue(key); } } // Retrieve and update user settings from GM const keyValuePairs = await Promise.all( storedValues.map(async key => [key, await GMCustomGetValue(key)]) ); keyValuePairs.forEach(([newKey, newValue]) => { userSettings[newKey] = newValue; }); debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`); } catch (error) { throw error; } } // ---------------- // Main function async function initialize() { try { if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API"; await loadUserSettings(); } catch (error) { debugLog(`Error loading user settings: ${error}. Loading with default settings.`); } if (window.self == window.top) { processNewPage(); // event listeners fire too late on first page load if premium bitrate is available and selected window.addEventListener('yt-player-updated', processNewPage, true); //handle desktop site window.addEventListener('yt-page-data-updated', processNewPage, true); //handle desktop site lazy reload window.addEventListener('state-navigateend', processNewPage, true); //handle mobile site showMenuOptions(); } else { window.addEventListener('loadstart', processNewPage, true); } } // Entry Point initialize(); })();