// ==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.09.01 // 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 // @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のビットレートを優先的に選択します。 // @downloadURL none // ==/UserScript== /*jshint esversion: 11 */ (function() { "use strict"; const DEBUG = true; const DEFAULT_SETTINGS = { targetResolution: "hd2160" }; const lifeSpan = 31104000000; 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 menuCommandIds = []; let doc = document, win = window; let videoId = null; let resolvedTarget = 0; let targetLabel = ''; let attempts = 0; let localItemString = ""; // -------------------- // --- FUNCTIONS ------ // -------------------- function debugLog(message, shouldShow = true) { if (DEBUG && shouldShow) { console.log("YTHD DEBUG | " + message); } } // -------------------- // Attempt to set the video resolution to target quality or the next best quality function setResolution(target, force = false) { if (target === 'auto') return; let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0]; if (!isValidVideo(ytPlayer, force)) return; videoId = ytPlayer.getVideoData().video_id; try { localItemString = localStorage.getItem("yt-player-quality"); } catch { debugLog("Fetching last used quality failed catastrophically. Likely the website is not YouTube. If website is YouTube then YouTube changed something."); } let availableQualities = ytPlayer.getAvailableQualityLevels(); resolvedTarget = findNextAvailableQuality(target, availableQualities); debugLog("Resolved target resolution: " + resolvedTarget); let premiumIndicator = "Premium"; let premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality === resolvedTarget && q.qualityLabel.includes(premiumIndicator) && q.isPlayable); ytPlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId); targetLabel = quality[resolvedTarget] + "p" + (premiumData ? " Premium" : ""); debugLog("Trying to set quality to: " + targetLabel); } function isValidVideo(ytPlayer, force) { //debugLog(JSON.stringify(ytPlayer?.getVideoData(), null, 2)); if (win.location.href.startsWith("https://www.youtube.com/shorts/")) { debugLog("Skipping Youtube Shorts"); videoId = null; return false; } if (!ytPlayer?.getAvailableQualityLabels()[0]) { debugLog("Video data missing"); videoId = null; return false; } if (videoId == ytPlayer.getVideoData().video_id && !force) { debugLog("Duplicate load"); return false; } return true; } function findNextAvailableQuality(target, availableQualities) { const available = availableQualities.map(q => ({ quality: q, value: quality[q] })); const targetValue = quality[target]; const smallerOrEqualQualities = available.filter(q => q.value <= targetValue); smallerOrEqualQualities.sort((a, b) => b.value - a.value); return smallerOrEqualQualities.length > 0 ? smallerOrEqualQualities[0].quality : 'auto'; } // 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) { let retryDelay = 500; let ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0]; if (!ytPlayer) { debugLog("YouTube player not found. Retrying after " + retryDelay + "ms..."); if (retries > 0) { setTimeout(() => verifyChange(retries - 1), retryDelay); } else { debugLog("Failed to find YouTube player after multiple attempts."); } return; } if (!isValidVideo(ytPlayer, true)) { debugLog("Could not find valid video to verify."); return; } let currentLabel = ytPlayer.getPlaybackQualityLabel(); let currentQuality = ytPlayer.getPlaybackQuality(); if (!currentLabel || !currentQuality) { debugLog("Failed to fetch quality label. Retrying after " + retryDelay + "ms..."); if (retries > 0) { setTimeout(() => verifyChange(retries - 1), retryDelay); } else { debugLog("Failed to fetch quality label after multiple attempts."); } return; } let success = targetLabel.endsWith("Premium") ? currentLabel === targetLabel : currentQuality === resolvedTarget; debugLog("Current Label: " + currentLabel); debugLog("Target Label: " + targetLabel); debugLog("Current Quality: " + currentQuality); debugLog("Target Quality: " + resolvedTarget); if (success) { debugLog("Quality set successfully!"); saveQualityPreferenceToBrowser(); } else { debugLog("Quality failed to set, retrying after " + retryDelay + "ms..."); if (retries > 0) { setResolution(resolvedTarget, true); setTimeout(() => verifyChange(retries - 1), retryDelay); } else { debugLog("Maximum attempts reached. Either YouTube cannot respond fast enough or the this script is no longer compatible."); } } } function saveQualityPreferenceToBrowser() { try { let birth = Date.now(); let death = birth + lifeSpan; let previousQuality = 0; try { previousQuality = JSON.parse(localItemString).data.quality || JSON.parse(JSON.parse(localItemString).data).quality; debugLog("Previous Quality: " + previousQuality); } catch (error) { debugLog("Error when parsing previousQuality. Error: " + error); } let settingData = { quality: quality[resolvedTarget], previousQuality: previousQuality }; let localItem = { data: JSON.stringify(settingData), expiration: death, creation: birth } localItemString = JSON.stringify(localItem); localStorage.setItem("yt-player-quality", localItemString); debugLog("Saving quality preference to browser storage. Saved: " + localItemString); } catch (error) { debugLog("Error when tring to save quality preference to browser storage. Error: " + error); } } // -------------------- // Functions for the quality selection menu function createQualityMenu() { GM_registerMenuCommand("Set Preferred Quality (show/hide)", () => { menuCommandIds.length ? removeQualityMenuItems() : showQualityMenuItems(); }, { autoClose: false }); } function showQualityMenuItems() { removeQualityMenuItems(); resolutions.forEach((resolution) => { let qualityText = (resolution === 'auto') ? 'auto' : quality[resolution] + "p"; if (resolution === userSettings.targetResolution) { qualityText += " (selected)"; } let menuCommandId = GM_registerMenuCommand(qualityText, () => { setSelectedResolution(resolution); }, { autoClose: false, }); menuCommandIds.push(menuCommandId); }); } function removeQualityMenuItems() { while (menuCommandIds.length) { GM_unregisterMenuCommand(menuCommandIds.pop()); } } function setSelectedResolution(resolution) { if (userSettings.targetResolution == resolution) return; userSettings.targetResolution = resolution; GM.setValue('targetResolution', resolution); removeQualityMenuItems(); showQualityMenuItems(); setResolution(resolution, true); } // -------------------- // Sync settings with locally stored values async function applySettings() { try { // Get all keys from GM const storedValues = await GM.listValues(); // Write any missing key-value pairs from DEFAULT_SETTINGS to GM await Promise.all(Object.entries(DEFAULT_SETTINGS).map(async ([key, value]) => { if (!storedValues.includes(key)) { await GM.setValue(key, value); } })); // Delete any extra keys in GM that are not in DEFAULT_SETTINGS await Promise.all(storedValues.map(async key => { if (!(key in DEFAULT_SETTINGS)) { await GM.deleteValue(key); } })); // Retrieve and update user settings from GM await Promise.all( storedValues.map(key => GM.getValue(key).then(value => [key, value])) ).then(keyValuePairs => keyValuePairs.forEach(([newKey, newValue]) => { userSettings[newKey] = newValue; })); debugLog(Object.entries(userSettings).map(([key, value]) => key + ": " + value).join(", ")); } catch (error) { debugLog("Error when applying settings: " + error.message); } } // -------------------- // Main function function main() { if (win.self == win.top) { createQualityMenu(); } setResolution(userSettings.targetResolution); win.addEventListener('popstate', () => { videoId = null; }); win.addEventListener("loadstart", () => { setResolution(userSettings.targetResolution); }, true); win.addEventListener("yt-navigate-finish", () => { verifyChange(); }, true); } // -------------------- // Entry Point applySettings().then(main); })();