// ==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.07.02 // @match *://www.youtube.com/* // @grant GM.getValue // @grant GM.setValue // @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"; // --- SETTINGS ------- // !!! PLEASE NOTE: // !!! Having the incorrect "overwriteStoredSettings" value maybe cause your settings to not save. let settings = { // true = use the setting here. Use this when changing settings here. // false = use stored setting. Use this when changing settings from the "Storage" tab. overwriteStoredSettings: false, // true = use the setting you last chose on YouTube. // false = ignore YouTube settings. importYoutubeSettings: false, // The target resolution. If not available, the next best available resolution will be used. targetRes: "hd2160" // Choices for targetRes are currently: // "highres" >= ( 8K / 4320p / QUHD ) // "hd2880" = ( 5K / 2880p / UHD+ ) // "hd2160" = ( 4K / 2160p / UHD ) // "hd1440" = ( 1440p / QHD ) // "hd1080" = ( 1080p / FHD ) // "hd720" = ( 720p / HD ) // "large" = ( 480p ) // "medium" = ( 360p ) // "small" = ( 240p ) // "tiny" = ( 144p ) // "auto" = ( auto ) }; // -------------------- // --- GLOBALS -------- // -------------------- const 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 doc = document, win = window; let vidId = null; // -------------------- // --- 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) { let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0]; if (!isValidVideo(ytPlayer)) return; vidId = ytPlayer.getVideoData().video_id; let localItem = localStorage.getItem("yt-player-quality"); if (localItem && settings.importYoutubeSettings) { let youtubeSettings = fetchLocalSettings(localItem); if (qualityLevels[youtubeSettings.quality]) { target = qualityLevels[youtubeSettings.quality]; } } let limitList = ytPlayer.getAvailableQualityLevels(); let limit = limitList[0]; if (quality[target] > quality[limit]) { target = limit; } let premiumIndicator = "Premium"; let premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes(premiumIndicator) && q.isPlayable); ytPlayer.setPlaybackQualityRange(target, target, premiumData?.formatId); debugLog("Set quality to: " + target + (premiumData ? " Premium" : "")); if (localItem){ localStorage.setItem("yt-player-quality",localItem); } else { localStorage.removeItem("yt-player-quality"); } } function isValidVideo(ytPlayer) { if (!ytPlayer?.getAvailableQualityLabels()[0]) { debugLog("Video data missing"); return false; } if (vidId == ytPlayer.getVideoData().video_id) { debugLog("Duplicate load"); return false; } return true; } function main() { let target = settings.targetRes.toLowerCase(); if (target != 'auto') { setResolution(target); win.addEventListener("loadstart", () => { setResolution(target); }, true); } } function fetchLocalSettings(localItem) { let quality = 0; let previousQuality = 0; let expiration = 0; let creation = 0; if (localItem) { localItem = JSON.parse(localItem); let localData = JSON.parse(localItem.data); quality = localData.quality; previousQuality = localData.previousQuality; expiration = localItem.expiration; creation = localItem.creation; } return { quality, previousQuality, expiration, creation }; } async function applySettings() { if (typeof GM != 'undefined' && GM.getValue && GM.setValue && GM.deleteValue && GM.listValues) { if (settings.overwriteStoredSettings) { // Clear all values from GM const keys = await GM.listValues(); await Promise.all(keys.map(key => GM.deleteValue(key))); // Write each key-value pair from settings await Promise.all(Object.entries(settings).map(([k, v]) => GM.setValue(k, v))); } else { // Get all keys from GM const gmKeys = await GM.listValues(); // Write any missing key-value pairs from settings to GM await Promise.all(Object.entries(settings).map(async ([k, v]) => { if (!gmKeys.includes(k)) { await GM.setValue(k, v); } })); // Delete any extra keys in GM that are not in settings await Promise.all(gmKeys.map(async key => { if (!(key in settings)) { await GM.deleteValue(key); } })); // Update the value for .overwriteStoredSettings in GM await GM.setValue('overwriteStoredSettings', settings.overwriteStoredSettings); // Retrieve and update settings from GM await Promise.all( gmKeys.map(k => GM.getValue(k).then(v => [k, v])) ).then(c => c.forEach(([nk, nv]) => { if (settings[nk] !== null && nk !== "overwriteStoredSettings") { settings[nk] = nv; } })); } debugLog(Object.entries(settings).map(([k, v]) => k + ": " + v).join(", ")); } } applySettings().then(main); })();