// ==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.google.com/s2/favicons?sz=64&domain=youtube.com
// @author ElectroKnight22
// @namespace electroknight22_youtube_hd_namespace
// @version 2025.08.02
// @note 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.addValueChangeListener
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_notification
// @grant GM_addValueChangeListener
// @run-at document-idle
// @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://greasyforks.org/scripts/498145-youtube-hd-premium
// ==/UserScript==
/*jshint esversion: 11 */
(function () {
'use strict';
const DEFAULT_SETTINGS = {
targetResolution: 'hd2160',
expandMenu: false,
};
const QUALITIES = {
highres: 4320,
hd2160: 2160,
hd1440: 1440,
hd1080: 1080,
hd720: 720,
large: 480,
medium: 360,
small: 240,
tiny: 144,
};
const PREMIUM_INDICATOR = 'Premium';
const REQUIRED_TAMPERMONKEY_VERSION = '5.4.624';
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 ${REQUIRED_TAMPERMONKEY_VERSION} or later.`,
qualityMenu: 'Quality Menu',
},
'zh-TW': {
tampermonkeyOutdatedAlertMessage: `看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 ${REQUIRED_TAMPERMONKEY_VERSION} 或更高版本。`,
qualityMenu: '畫質選單',
},
'zh-CN': {
tampermonkeyOutdatedAlertMessage: `看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 ${REQUIRED_TAMPERMONKEY_VERSION} 或更高版本。`,
qualityMenu: '画质菜单',
},
ja: {
tampermonkeyOutdatedAlertMessage: `ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン${REQUIRED_TAMPERMONKEY_VERSION}以上に更新してください。`,
qualityMenu: '画質メニュー',
},
};
const state = {
userSettings: { ...DEFAULT_SETTINGS },
isIframe: window.self !== window.top,
isOldTampermonkey: false,
useCompatibilityMode: typeof GM === 'undefined' && typeof GM_info !== 'undefined',
moviePlayer: null,
registeredMenuIds: [],
};
const GM_API = {
registerMenuCommand: state.useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand,
unregisterMenuCommand: state.useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand,
getValue: state.useCompatibilityMode ? GM_getValue : GM.getValue,
setValue: state.useCompatibilityMode ? GM_setValue : GM.setValue,
deleteValue: state.useCompatibilityMode ? GM_deleteValue : GM.deleteValue,
listValues: state.useCompatibilityMode ? GM_listValues : GM.listValues,
notification: state.useCompatibilityMode ? GM_notification : GM.notification,
addValueChangeListener: state.useCompatibilityMode ? GM_addValueChangeListener : GM.addValueChangeListener,
};
const getLocalizedText = () => {
const lang = navigator.language || navigator.userLanguage;
const preferredLang = lang.startsWith('zh') && lang !== 'zh-TW' ? 'zh-CN' : lang;
return TRANSLATIONS[preferredLang] || TRANSLATIONS['en-US'];
};
function resolveOptimalQuality(videoQualityData, targetResolutionString) {
const availableQualities = [...new Set(videoQualityData.map((q) => q.quality))];
const targetValue = QUALITIES[targetResolutionString];
const bestQualityString = availableQualities
.filter((q) => QUALITIES[q] <= targetValue)
.sort((a, b) => QUALITIES[b] - QUALITIES[a])[0];
if (!bestQualityString) return null;
let normalCandidate = null;
let premiumCandidate = null;
for (const quality of videoQualityData) {
if (quality.quality === bestQualityString && quality.isPlayable) {
if (quality.qualityLabel?.trim().endsWith(PREMIUM_INDICATOR)) {
premiumCandidate = quality;
} else {
normalCandidate = quality;
}
}
}
return premiumCandidate || normalCandidate;
}
function setResolution() {
try {
if (!state.moviePlayer) throw new Error('Movie player not found.');
const videoQualityData = state.moviePlayer.getAvailableQualityData();
if (!videoQualityData.length) {
// Fallback for non-auto-playing videos that have preloading disabled
try {
const videoElement = state.moviePlayer.querySelector('video');
state.moviePlayer.setPlaybackQualityRange(state.userSettings.targetResolution); // Instant, but may break UI temporarily
videoElement.addEventListener('play', setResolution, { once: true }); // Slower, but guarantees correct quality and fixes UI
} catch {
console.error('Failed to set resolution using fallback method.');
}
return;
}
const optimalQuality = resolveOptimalQuality(videoQualityData, state.userSettings.targetResolution);
if (optimalQuality) {
state.moviePlayer.setPlaybackQualityRange(optimalQuality.quality, optimalQuality.quality, optimalQuality.formatId);
}
} catch (error) {
console.error('Did not set resolution.', error);
}
}
function handlePlayerStateChange(playerState) {
const playerElement = document.getElementById('movie_player');
if (!playerElement) return;
if (playerState === -1 && playerElement.hasAttribute('resolution-set')) {
playerElement.removeAttribute('resolution-set');
}
if (playerState === 1 && !playerElement.hasAttribute('resolution-set')) {
playerElement.setAttribute('resolution-set', '');
setResolution();
}
}
function processVideoLoad(event = null) {
state.moviePlayer = event?.target?.player_ ?? document.querySelector('#movie_player');
setResolution();
const playerElement = document.getElementById('movie_player');
if (playerElement && !playerElement.hasAttribute('state-change-listener-added')) {
playerElement.addEventListener('onStateChange', handlePlayerStateChange);
playerElement.setAttribute('state-change-listener-added', 'true');
}
}
function addEventListeners() {
const playerUpdateEvent = window.location.hostname === 'm.youtube.com' ? 'state-navigateend' : 'yt-player-updated';
window.addEventListener(playerUpdateEvent, processVideoLoad, true);
window.addEventListener('pageshow', processVideoLoad, true);
// Sync settings across tabs
GM_API.addValueChangeListener('settings', (key, oldValue, newValue, remote) => {
if (!remote) return;
state.userSettings = newValue;
showMenuOptions();
const applyResolutionChange = () => {
if (!document.hidden) setResolution();
};
if (document.hidden) {
window.addEventListener('visibilitychange', applyResolutionChange, { once: true });
} else {
applyResolutionChange();
}
});
}
function removeMenuOptions() {
while (state.registeredMenuIds.length) {
GM_API.unregisterMenuCommand(state.registeredMenuIds.pop());
}
}
function showMenuOptions() {
removeMenuOptions();
const shouldUseFallback = state.isOldTampermonkey || state.isIframe;
const localizedText = getLocalizedText();
const menuItems = [
!shouldUseFallback && {
label: () => `${localizedText.qualityMenu} ${state.userSettings.expandMenu ? '🔼' : '🔽'}`,
menuId: 'menuExpandBtn',
alwaysShow: true,
async handleClick() {
state.userSettings.expandMenu = !state.userSettings.expandMenu;
await updateSetting('expandMenu', state.userSettings.expandMenu);
showMenuOptions();
},
},
...Object.entries(QUALITIES).map(([label, resolution]) => ({
label: () => `${resolution}p ${label === state.userSettings.targetResolution ? '✅' : ''}`,
menuId: label,
alwaysShow: false,
async handleClick() {
if (state.userSettings.targetResolution === label) return;
state.userSettings.targetResolution = label;
await updateSetting('targetResolution', label);
setResolution();
showMenuOptions();
},
})),
];
menuItems.forEach((item) => {
if (!item) return; // Skip invalid items
if (item.alwaysShow || state.userSettings.expandMenu || shouldUseFallback) {
GM_API.registerMenuCommand(item.label(), item.handleClick, {
id: item.menuId,
autoClose: shouldUseFallback,
});
state.registeredMenuIds.push(item.menuId);
}
});
}
async function updateSetting(key, value) {
try {
const currentSettings = (await GM_API.getValue('settings')) || {};
currentSettings[key] = value;
await GM_API.setValue('settings', currentSettings);
} catch (error) {
console.error('Error updating setting:', error);
}
}
async function loadUserSettings() {
const storedSettings = (await GM_API.getValue('settings')) || {};
state.userSettings = { ...DEFAULT_SETTINGS, ...storedSettings };
if (!QUALITIES[state.userSettings.targetResolution]) {
state.userSettings.targetResolution = DEFAULT_SETTINGS.targetResolution;
}
await GM_API.setValue('settings', state.userSettings);
}
async function cleanupOldStorage() {
try {
const allowedKeys = ['settings', 'versionWarningShown'];
const keys = await GM_API.listValues();
for (const key of keys) {
if (!allowedKeys.includes(key)) {
await GM_API.deleteValue(key);
}
}
} catch (error) {
console.error('Error cleaning up old storage keys:', error);
}
}
function compareVersions(v1, v2) {
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;
}
function checkTampermonkeyVersion() {
if (state.useCompatibilityMode || GM_info.scriptHandler !== 'Tampermonkey') return;
if (compareVersions(GM_info.version, REQUIRED_TAMPERMONKEY_VERSION) < 0) {
state.isOldTampermonkey = true;
const versionWarningShown = GM_API.getValue('versionWarningShown', false);
if (!versionWarningShown) {
GM_API.setValue('versionWarningShown', true);
GM_API.notification({
text: getLocalizedText().tampermonkeyOutdatedAlertMessage,
timeout: 15000,
});
}
}
}
async function initialize() {
if (state.useCompatibilityMode) console.warn('Running in Greasemonkey compatibility mode. Some features might be limited.');
try {
await cleanupOldStorage();
await loadUserSettings();
checkTampermonkeyVersion();
} catch (error) {
console.error(`Error during initialization: ${error}. Loading with default settings.`);
}
addEventListeners();
showMenuOptions();
}
initialize();
})();