// ==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.11
// 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 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
// ==/UserScript==
/*jshint esversion: 11 */
(function () {
"use strict";
// -------------------------------
// Default settings (for storage key "settings")
// -------------------------------
const DEFAULT_SETTINGS = {
targetResolution: "hd2160",
expandMenu: false,
debug: false
};
// -------------------------------
// Other constants and translations
// -------------------------------
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";
// -------------------------------
// Global variables
// -------------------------------
let userSettings = { ...DEFAULT_SETTINGS };
let useCompatibilityMode = false;
let menuItems = [];
let moviePlayer = null;
let isOldTampermonkey = false;
const updatedVersions = {
Tampermonkey: '5.4.624',
};
let isScriptRecentlyUpdated = false;
// -------------------------------
// 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 GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;
const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
const GMCustomNotification = useCompatibilityMode ? GM_notification : GM.notification;
// -------------------------------
// Debug logging helper
// -------------------------------
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";
if (!message.endsWith(".")) {
message += ".";
}
console.log(`[YTHD DEBUG] ${message} Function called ${callerLine}`);
}
// -------------------------------
// Video quality functions
// -------------------------------
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();
}
// -------------------------------
// Menu functions
// -------------------------------
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);
}
});
}
// The menu callbacks now use the helper "updateSetting" to update the stored settings.
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 === 0 ? GET_LOCALIZED_TEXT().autoModeName : 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());
}
}
// -------------------------------
// GreaseMonkey / Tampermonkey version checks
// -------------------------------
function compareVersions(v1, v2) {
try {
if (!v1 || !v2) throw "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 ("Error comparing versions: " + error);
}
}
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;
if (isScriptRecentlyUpdated) {
GMCustomNotification({
text: GET_LOCALIZED_TEXT().tampermonkeyOutdatedAlertMessage,
timeout: 15000
});
}
}
}
// -------------------------------
// Storage helper functions
// -------------------------------
// Load user settings from the "settings" key.
async function loadUserSettings() {
try {
// Retrieve stored settings or use the defaults.
userSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
debugLog(`Loaded user settings: [${Object.entries(userSettings)
.map(([key, value]) => `${key}: ${value}`)
.join(", ")}].`);
} catch (error) {
throw error;
}
}
// Update one setting in the stored settings.
async function updateSetting(key, value) {
try {
let currentSettings = await GMCustomGetValue('settings', DEFAULT_SETTINGS);
currentSettings[key] = value;
await GMCustomSetValue('settings', currentSettings);
} catch (error) {
debugLog("Error updating setting: " + error);
}
}
async function updateScriptInfo() {
try {
const oldScriptInfo = await GMCustomGetValue('scriptInfo', null);
debugLog(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
const newScriptInfo = {
version: getScriptVersionFromMeta(),
};
await GMCustomSetValue('scriptInfo', newScriptInfo);
if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
isScriptRecentlyUpdated = true;
}
debugLog(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
return oldScriptInfo;
} catch (error) {
debugLog("Error updating script info: " + error);
}
}
// Cleanup any leftover keys from previous versions.
async function cleanupOldStorage() {
try {
const allowedKeys = ['settings', 'scriptInfo'];
const keys = await GMCustomListValues();
for (const key of keys) {
if (!allowedKeys.includes(key)) {
await GMCustomDeleteValue(key);
debugLog(`Deleted leftover key: ${key}`);
}
}
} catch (error) {
debugLog("Error cleaning up old storage keys: " + error);
}
}
// -------------------------------
// Script metadata extraction
// -------------------------------
function getScriptVersionFromMeta() {
const meta = GM_info.scriptMetaStr;
const versionMatch = meta.match(/@version\s+([^\r\n]+)/);
return versionMatch ? versionMatch[1].trim() : null;
}
// -------------------------------
// Main function: add event listeners and initialize
// -------------------------------
function addEventListeners() {
if (window.location.hostname === "m.youtube.com") {
window.addEventListener('state-navigateend', processNewPage, true);
} else {
window.addEventListener('yt-player-updated', processNewPage, true);
window.addEventListener('yt-page-data-updated', processNewPage, true);
}
}
async function initialize() {
try {
if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
await cleanupOldStorage();
await loadUserSettings();
await updateScriptInfo();
CheckTampermonkeyUpdated();
} catch (error) {
debugLog(`Error loading user settings: ${error}. Loading with default settings.`);
}
if (window.self === window.top) {
processNewPage(); // Ensure initial resolution update on first load.
window.addEventListener('yt-navigate-finish', addEventListeners, { once: true });
showMenuOptions();
} else {
window.addEventListener('loadstart', processNewPage, true);
}
}
// -------------------------------
// Entry Point
// -------------------------------
initialize();
})();