// ==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.10.19.1 // 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/* // @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 resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto']; const quality = { highres: 4320, hd2880: 2880, hd2160: 2160, hd1440: 1440, hd1080: 1080, hd720: 720, large: 480, medium: 360, small: 240, tiny: 144, auto: 0 }; const qualityLevels = Object.fromEntries( Object.entries(quality).map(([key, value]) => [value, key]) ); let userSettings = { ...DEFAULT_SETTINGS }; let useCompatilibtyMode = false; let hasNewGMValueAPI = false; let hasNewGMMenuAPI = false; let hasOldGMValueAPI = false; let hasOldGMMenuAPI = false; let isBrokenOrMissingGMAPI = false; let menuItems = []; let videoId = ''; let resolvedTarget = ''; let targetLabel = ''; let ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0]; let video = document.querySelector('video'); const premiumIndicator = "Premium"; // --- CLASS DEFINITIONS ----------- class AllowedExceptionError extends Error { constructor(message) { super(message); this.name = "Allowed Exception"; } } // --- 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 { checkVideoValid(force); resolvedTarget = userSettings.targetResolution; let premiumData = null; if (resolvedTarget != 'auto') { const availableQualities = ytPlayer.getAvailableQualityLevels(); resolvedTarget = findNextAvailableQuality(resolvedTarget, availableQualities); debugLog("Resolved target resolution: " + resolvedTarget); premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality === resolvedTarget && q.qualityLabel.trim().endsWith(premiumIndicator) && q.isPlayable); } ytPlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId); targetLabel = quality[resolvedTarget] + "p" + (premiumData ? " " + premiumIndicator : ""); videoId = ytPlayer.getVideoData().video_id; debugLog(`Setting quality to: ${resolvedTarget == "auto" ? "auto" : targetLabel}`); } catch (error) { debugLog("Did not set resolution. " + error); } } function checkVideoValid(force) { //debugLog(JSON.stringify(ytPlayer?.getVideoData(), null, 2)); //debugLog(JSON.stringify(ytPlayer?.getAvailableQualityLevels(), null, 2)); try { if (/^https?:\/\/(www\.)?youtube\.com\/shorts\//.test(window.location.href)) { throw new AllowedExceptionError("Skipping Youtube Shorts."); } if (!ytPlayer?.getAvailableQualityLabels().length) { throw "Video data missing."; } if (videoId == ytPlayer.getVideoData().video_id && !force) { throw new AllowedExceptionError("Duplicate Load. Skipping."); } } catch (error) { throw error; } } function findNextAvailableQuality(target, availableQualities) { const targetValue = quality[target]; return availableQualities .map(q => ({ quality: q, value: quality[q] })) .find(q => q.value <= targetValue)?.quality || 'auto'; } function compareQualityLabelButIgnoreFrameRate(label1, label2) { const normalize = label => label.replace(new RegExp(`(\\d+p)\\d*( ${premiumIndicator})?$`), '$1').trim(); return normalize(label1) === normalize(label2); } // Tries to validate setting changes. If this fails to detect a successful update it will call setResolution to attempt to force the playback quality to update again. function verifyChange(retries = 10) { const retryDelay = 500; try { checkVideoValid(true); let success = ytPlayer.getPreferredQuality() !== "auto" || userSettings.targetResolution === "auto"; let currentQualityLabel = ytPlayer.getPlaybackQualityLabel(); let isManualChange = !compareQualityLabelButIgnoreFrameRate(currentQualityLabel, targetLabel); if (success) { debugLog(`Quality is ${currentQualityLabel}. ${isManualChange ? "Manual change detected!" : "Change verified!"}`); } else { throw "Unable to verify change"; } } catch (error) { if (error instanceof AllowedExceptionError) { debugLog(error); } else if (retries) { debugLog(`Error when verifying quality change. [${error}] Attempting quality change and reverifying after ${retryDelay}ms...`); setResolution(true); setTimeout(() => verifyChange(retries - 1), retryDelay); } else { debugLog(`Cannot verify quality change after retrying.`); } } } function processNewVideo() { try { video?.removeEventListener('playing', verifyChange, true); videoId = ''; ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0] || ytPlayer; video = ytPlayer?.querySelector('video') || document.querySelector('video'); if (!video) throw "Video element not found."; video.addEventListener('playing', verifyChange, true); setResolution(); } catch (error) { debugLog("Load error: " + error); } } // ---------------------------------------- // Functions for the quality selection menu // -------- // GM 4+ function removeMenuOptions() { if (useCompatilibtyMode) { removeMenuOptionsCompatibility(); return; } while (menuItems.length) { GM.unregisterMenuCommand(menuItems.pop()); } } function showMenuOptions() { if (useCompatilibtyMode) { showMenuOptionsCompatibility(); return; } removeMenuOptions(); const isExpandMenu = userSettings.expandMenu; GM.registerMenuCommand(`Quality Settings [${ isExpandMenu ? "HIDE" : "SHOW" }]`, () => { userSettings.expandMenu = !isExpandMenu; GM.setValue('expandMenu', userSettings.expandMenu); showMenuOptions(); }, { id: "menuExpandBtn", autoClose: false }); menuItems.push("menuExpandBtn"); if (!isExpandMenu) return; resolutions.forEach((resolution) => { let qualityText = (resolution === 'auto') ? 'auto' : quality[resolution] + "p"; if (resolution === userSettings.targetResolution) { qualityText += " ✅"; } GM.registerMenuCommand(qualityText, () => { setSelectedResolution(resolution); }, { id: qualityText, autoClose: false }); menuItems.push(qualityText); }); const showDebug = userSettings.debug; GM.registerMenuCommand(`DEBUG [${showDebug ? "ON" : "OFF"}]`, () => { userSettings.debug = !showDebug; GM.setValue('debug', userSettings.debug); showMenuOptions(); }, { id: "debugBtn", autoClose: false }); menuItems.push("debugBtn"); } function setSelectedResolution(resolution) { if (useCompatilibtyMode) { setSelectedResolutionCompatibility(resolution); return; } if (userSettings.targetResolution == resolution) return; userSettings.targetResolution = resolution; GM.setValue('targetResolution', resolution); showMenuOptions(); setResolution(true); } // ------- // old GM function removeMenuOptionsCompatibility() { while (menuItems.length) { GM_unregisterMenuCommand(menuItems.pop()); } } function showMenuOptionsCompatibility() { removeMenuOptions(); const isExpandMenu = userSettings.expandMenu; const menuButton = GM_registerMenuCommand(`Quality Settings [${ isExpandMenu ? "HIDE" : "SHOW" }]`, () => { userSettings.expandMenu = !isExpandMenu; GM_setValue('expandMenu', userSettings.expandMenu); showMenuOptions(); }, { autoClose: false }); menuItems.push(menuButton); if (!isExpandMenu) return; resolutions.forEach((resolution) => { let qualityText = (resolution === 'auto') ? 'auto' : quality[resolution] + "p"; if (resolution === userSettings.targetResolution) { qualityText += " ✅"; } const qualityOption = GM_registerMenuCommand(qualityText, () => { setSelectedResolution(resolution); }, { autoClose: false, }); menuItems.push(qualityOption); }); const showDebug = userSettings.debug; const debugButton = GM_registerMenuCommand(`DEBUG [${showDebug ? "ON" : "OFF"}]`, () => { userSettings.debug = !showDebug; GM_setValue('debug', userSettings.debug); showMenuOptions(); }, { autoClose: false }); menuItems.push(debugButton); } function setSelectedResolutionCompatibility(resolution) { if (userSettings.targetResolution == resolution) return; userSettings.targetResolution = resolution; GM_setValue('targetResolution', resolution); showMenuOptions(); setResolution(true); } // ----------------------------------------------- // Verify Grease Monkey API exists and is working. function checkGMAPI() { try { if (typeof GM != 'undefined') { if (GM.getValue && GM.setValue && GM.deleteValue && GM.listValues) { hasNewGMValueAPI = true; } if (GM.registerMenuCommand && GM.unregisterMenuCommand){ hasNewGMMenuAPI = true; } } else { if (GM_setValue && GM_setValue && GM_deleteValue && GM_listValues) { hasOldGMValueAPI = true; } if (GM_registerMenuCommand && GM_unregisterMenuCommand) { hasOldGMMenuAPI = true; } } if (hasNewGMValueAPI && hasNewGMMenuAPI) { } else if (hasOldGMValueAPI && hasOldGMMenuAPI) { useCompatilibtyMode = true; debugLog("Running in compatiblity mode.") } else { isBrokenOrMissingGMAPI = true; } } catch (error) { debugLog("Error when checking GM API. Error: " + error); } } // ----------------------------------------------- // User setting handling async function loadUserSettings() { if (useCompatilibtyMode) { await loadUserSettingsCompatiblity(); return; } // Get all keys from GM const storedValues = await GM.listValues(); // 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 GM.setValue(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 GM.deleteValue(key); } } // Retrieve and update user settings from GM const keyValuePairs = await Promise.all(storedValues.map(async key => [key, await GM.getValue(key)])); keyValuePairs.forEach(([newKey, newValue]) => { userSettings[newKey] = newValue; }); debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`); } async function loadUserSettingsCompatiblity() { const storedValues = await GM_listValues(); for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) { if (!storedValues.includes(key)) { await GM_setValue(key, value); } } for (const key of storedValues) { if (!(key in DEFAULT_SETTINGS)) { await GM_deleteValue(key); } } const keyValuePairs = await Promise.all(storedValues.map(async key => [key, await GM_getValue(key)])); keyValuePairs.forEach(([newKey, newValue]) => { userSettings[newKey] = newValue; }); debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`); } // ---------------- // Main function async function initialize() { checkGMAPI(); try { if (isBrokenOrMissingGMAPI) { throw "Did not detect valid Grease Monkey API"; } await loadUserSettings(); } catch (error) { debugLog(`Error loading user settings: ${error}. Loading with default settings.`); } window.addEventListener('popstate', () => { videoId = ''; }); if (window.self == window.top) { // handle youtube website window.addEventListener("yt-navigate-finish", processNewVideo, true ); showMenuOptions(); } else { // handle iframes window.addEventListener('load', processNewVideo, true ); } } // Entry Point initialize(); })();