// ==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.05.05 // 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 // @run-at document-start // @license MIT // @description Automatically 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', debug: 'DEBUG' }, 'zh-TW': { tampermonkeyOutdatedAlertMessage: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。", qualityMenu: '畫質選單', debug: '偵錯' }, 'zh-CN': { tampermonkeyOutdatedAlertMessage: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。", qualityMenu: '画质菜单', debug: '调试' }, 'ja': { tampermonkeyOutdatedAlertMessage: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。", qualityMenu: '画質メニュー', 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, }; const PREMIUM_INDICATOR_LABEL = "Premium"; // ------------------------------- // Global variables // ------------------------------- let userSettings = { ...DEFAULT_SETTINGS }; let useCompatibilityMode = false; let menuItems = []; let moviePlayer = null; let isIframe = false; let isOldTampermonkey = false; const updatedVersions = { Tampermonkey: '5.4.624', }; // ------------------------------- // GM COMPATIBILITY LAYER // ------------------------------- 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 GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue; const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues; const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification; function printDebug(consoleMethod = console.log, ...args) { if (!userSettings.debug) return; if (typeof consoleMethod !== 'function') { args.unshift(consoleMethod); consoleMethod = console.log; } consoleMethod(...args); } function setResolution() { try { if (!moviePlayer) throw new Error("Movie player not found."); const videoQualityData = moviePlayer.getAvailableQualityData(); // fixes non-auto-playing iframes if (isIframe && !videoQualityData.length) { printDebug("Performing iframe magic..."); const videoElement = moviePlayer.querySelector('video'); moviePlayer.setPlaybackQualityRange(userSettings.targetResolution); // Force set quality to user preference. Breaks the UI but quality will be mostly correct. Instant. videoElement.addEventListener('play', setResolution, { once: true }); // Waits for playback to set quality properly. Fixes the UI and guarantees correct quality. Slow. return; } if (!videoQualityData.length) throw new Error("Quality options missing."); 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); printDebug(`Setting quality to: [${resolvedTarget}${premiumData ? " Premium" : ""}]`); } catch (error) { printDebug(console.error, "Did not set resolution. ", error); } } function findNextAvailableQuality(target, availableQualities) { const targetValue = QUALITIES[target]; return availableQualities .filter(q => QUALITIES[q] <= targetValue) .sort((a, b) => QUALITIES[b] - QUALITIES[a])[0]; } function processVideoLoad(event = null) { console.log('event', event); printDebug('Processing video load...'); moviePlayer = event?.target?.player_ ?? document.querySelector('#movie_player'); setResolution(); } 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: async function () { userSettings.expandMenu = !userSettings.expandMenu; await updateSetting('expandMenu', userSettings.expandMenu); showMenuOptions(); }, }, }; const menuOptions = { ...menuExpandButton, qualities: { items: Object.entries(QUALITIES).map(([label, resolution]) => ({ label: () => `${resolution}p ${label === userSettings.targetResolution ? "✅" : ""}`, menuId: label, handleClick: async function () { if (userSettings.targetResolution === label) return; userSettings.targetResolution = label; await updateSetting('targetResolution', label); setResolution(); showMenuOptions(); }, })), }, debug: { label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`, menuId: "debugBtn", handleClick: async function () { userSettings.debug = !userSettings.debug; await updateSetting('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()); } } function compareVersions(v1, v2) { try { if (!v1 || !v2) throw new Error("Invalid version string."); if (v1 === v2) return 0; 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; const num2 = parts2[i] ?? 0; if (num1 > num2) return 1; if (num1 < num2) return -1; } return 0; } catch (error) { throw new Error("Error comparing versions: " + error); } } function hasGreasyMonkeyAPI() { if (typeof GM !== 'undefined') return true; if (typeof GM_info !== 'undefined') { useCompatibilityMode = true; printDebug(console.warn, "Running in compatibility mode."); return true; } return false; } function CheckTampermonkeyUpdated() { if (GM_info.scriptHandler === "Tampermonkey") { if (compareVersions(GM_info.version, updatedVersions.Tampermonkey) === 1) return; isOldTampermonkey = true; const versionWarningShown = GMCustomGetValue('versionWarningShown', false); if (!versionWarningShown) { GMCustomSetValue('versionWarningShown', true); GMCustomNotification({ text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage, timeout: 15000 }); } } } // ------------------------------- // Storage helper functions // ------------------------------- /** * Load user settings from the "settings" key. * Ensures that only keys existing in DEFAULT_SETTINGS are kept. * If no stored settings are found, defaults are used. */ async function loadUserSettings() { try { const storedSettings = await GMCustomGetValue('settings', {}); userSettings = Object.keys(DEFAULT_SETTINGS).reduce((accumulator, key) => { accumulator[key] = storedSettings.hasOwnProperty(key) ? storedSettings[key] : DEFAULT_SETTINGS[key]; return accumulator; }, {}); if (!QUALITIES[userSettings.targetResolution]) userSettings.targetResolution = DEFAULT_SETTINGS.targetResolution; await GMCustomSetValue('settings', userSettings); printDebug(`Loaded user settings: ${JSON.stringify(userSettings)}.`); } catch (error) { throw error; } } async function updateSetting(key, value) { try { let currentSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS); currentSettings[key] = value; await GMCustomSetValue('settings', currentSettings); } catch (error) { printDebug(console.error, "Error updating setting: ", error); } } // Cleanup any leftover keys from previous versions. async function cleanupOldStorage() { try { const allowedKeys = ['settings', 'versionWarningShown']; const keys = await GMCustomListValues(); for (const key of keys) { if (!allowedKeys.includes(key)) { await GMCustomDeleteValue(key); printDebug(`Deleted leftover key: ${key}`); } } } catch (error) { printDebug(console.error, "Error cleaning up old storage keys: ", error); } } function addEventListeners() { if (window.location.hostname === "m.youtube.com") { window.addEventListener('state-navigateend', processVideoLoad, true); } else { window.addEventListener('yt-player-updated', processVideoLoad, true); } } async function initialize() { try { if (!hasGreasyMonkeyAPI()) throw new Error("Did not detect valid Grease Monkey API"); await cleanupOldStorage(); await loadUserSettings(); CheckTampermonkeyUpdated(); } catch (error) { printDebug(console.error, `Error loading user settings: ${error}. Loading with default settings.`); } window.addEventListener('yt-page-data-updated', () => { console.log('yt-page-data-updated'); }, true); window.addEventListener('yt-page-data-fetched', () => { console.log('yt-page-data-fetched'); }, true); window.addEventListener('pageshow', processVideoLoad, true); if (window.self === window.top) { addEventListeners(); showMenuOptions(); } else { isIframe = true; } } initialize(); })();