// ==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.02.10 // 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.notification // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_notification // @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': { tampermonkeyOutdatedAlertMessage: "It looks like you're using an older version of Tampermonkey that might cause menu issues. For the best experience, please update to version 5.4.6224 or later.", qualityMenu: 'Quality Menu', autoModeName: 'Optimized Auto', debug: 'DEBUG' }, 'zh-TW': { tampermonkeyOutdatedAlertMessage: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。", qualityMenu: '畫質選單', autoModeName: '優化版自動模式', debug: '偵錯' }, 'zh-CN': { tampermonkeyOutdatedAlertMessage: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。", qualityMenu: '画质菜单', autoModeName: '优化版自动模式', debug: '调试' }, 'ja': { tampermonkeyOutdatedAlertMessage: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。", qualityMenu: '画質メニュー', autoModeName: '最適化自動モード', debug: 'デバッグ' } }; const GET_LOCALIZED_TEXT = () => { const language = GET_PREFERRED_LANGUAGE(); return TRANSLATIONS[language] || TRANSLATIONS['en-US']; }; const QUALITIES = { highres: 4320, hd2160: 2160, hd1440: 1440, hd1080: 1080, hd720: 720, large: 480, medium: 360, small: 240, tiny: 144, auto: 0 }; const PREMIUM_INDICATOR_LABEL = "Premium"; let userSettings = { ...DEFAULT_SETTINGS }; let useCompatibilityMode = false; let menuItems = []; let moviePlayer = null; let isOldTampermonkey = false; const updatedVersions = { Tampermonkey: '5.4.624', }; // --- 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; const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification; // --- 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) throw "Movie player not found."; const videoQualityData = moviePlayer.getAvailableQualityData(); const currentPlaybackQuality = moviePlayer.getPlaybackQuality(); const currentQualityLabel = moviePlayer.getPlaybackQualityLabel(); if (!videoQualityData.length) throw "Quality options missing."; if (userSettings.targetResolution === 'auto') { if (!currentPlaybackQuality || !currentQualityLabel) throw "Unable to determine current playback quality."; const isOptimalQuality = videoQualityData.filter(q => q.quality == currentPlaybackQuality).length <= 1 || currentQualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL); if (!isOptimalQuality) moviePlayer.loadVideoById(moviePlayer.getVideoData().video_id); debugLog(`Setting quality to: [${GET_LOCALIZED_TEXT().autoModeName}]`); } else { let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer.getAvailableQualityLevels()); const premiumData = videoQualityData.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 && !isOldTampermonkey) return; if (option.items) { option.items.forEach(item => callback(item)); } else { callback(option); } }); } function showMenuOptions() { const shouldAutoClose = isOldTampermonkey; removeMenuOptions(); const menuExpandButton = isOldTampermonkey ? {} : { expandMenu: { alwaysShow: true, label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`, menuId: "menuExpandBtn", handleClick: function () { userSettings.expandMenu = !userSettings.expandMenu; GMCustomSetValue('expandMenu', userSettings.expandMenu); showMenuOptions(); }, }, } const menuOptions = { ...menuExpandButton, qualities: { items: Object.entries(QUALITIES).map(([label, resolution]) => ({ label: () => `${resolution === 0 ? GET_LOCALIZED_TEXT().autoModeName : 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: shouldAutoClose, }); menuItems.push(item.menuId); }); } function removeMenuOptions() { while (menuItems.length) { GMCustomUnregisterMenuCommand(menuItems.pop()); } } // ----------------------------------------------- // Verify Grease Monkey API exists and is working. function compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); const len = Math.max(parts1.length, parts2.length); for (let i = 0; i < len; i++) { const num1 = parts1[i] || 0; // If undefined, treat as 0 const num2 = parts2[i] || 0; if (num1 > num2) return 1; // v1 is greater if (num1 < num2) return -1; // v2 is greater } return 0; // Both versions are equal } function hasGreasyMonkeyAPI() { if (typeof GM != 'undefined') return true; if (typeof GM_info != 'undefined') { useCompatibilityMode = true; debugLog("Running in compatibility mode."); return true; } return false; } function CheckTampermonkeyUpdated() { if (GM_info.scriptHandler == "Tampermonkey" && compareVersions(GM_info.version, updatedVersions.Tampermonkey) != 1) { isOldTampermonkey = true; GMCustomNotification({ text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage, timeout: 15000 }); } } // ----------------------------------------------- // 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 function addEventListeners() { if (window.location.hostname === "m.youtube.com") { window.addEventListener('state-navigateend', processNewPage, true); //handle mobile site } else { window.addEventListener('yt-player-updated', processNewPage, true); //handle desktop site window.addEventListener('yt-page-data-updated', processNewPage, true); //handle desktop site lazy reload } } async function initialize() { try { if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API"; CheckTampermonkeyUpdated(); await loadUserSettings(); } catch (error) { debugLog(`Error loading user settings: ${error}. Loading with default settings.`); } window 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-navigate-finish', addEventListeners, { once: true }); showMenuOptions(); } else { window.addEventListener('loadstart', processNewPage, true); // handle iframes } } // Entry Point initialize(); })();