// ==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.09.05.3
// @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 none
// @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 STORAGE_KEY = 'YTHD_settings';
const DEFAULT_SETTINGS = {
targetResolution: 'hd2160',
};
const QUALITIES = {
highres: { p: 4320, label: '8K' },
hd2160: { p: 2160, label: '4K' },
hd1440: { p: 1440, label: '1440p' },
hd1080: { p: 1080, label: '1080p' },
hd720: { p: 720, label: '720p' },
large: { p: 480, label: '480p' },
medium: { p: 360, label: '360p' },
small: { p: 240, label: '240p' },
tiny: { p: 144, label: '144p' },
};
const PREMIUM_INDICATOR = 'Premium';
const SVG_NS = 'http://www.w3.org/2000/svg';
const ICONS = {
createPinIcon: () => {
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('height', '24');
svg.setAttribute('width', '24');
const path = document.createElementNS(SVG_NS, 'path');
path.setAttribute('d', 'M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z');
path.setAttribute('fill', 'currentColor');
svg.appendChild(path);
return svg;
},
};
const state = {
userSettings: { ...DEFAULT_SETTINGS },
moviePlayer: null,
};
// --- Core Logic ---
function resolveOptimalQuality(videoQualityData, targetResolutionString) {
const availableQualities = [...new Set(videoQualityData.map((q) => q.quality))];
const targetValue = QUALITIES[targetResolutionString].p;
const bestQualityString = availableQualities
.filter((q) => QUALITIES[q] && QUALITIES[q].p <= targetValue)
.sort((a, b) => QUALITIES[b].p - QUALITIES[a].p)[0];
if (!bestQualityString) return null;
let normalCandidate = null,
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 || typeof state.moviePlayer.getAvailableQualityData !== 'function') throw new Error('No valid video player found.');
const videoQualityData = state.moviePlayer.getAvailableQualityData();
if (!videoQualityData || !videoQualityData.length) throw new Error('Cannot determine available video qualities.');
const optimalQuality = resolveOptimalQuality(videoQualityData, state.userSettings.targetResolution);
if (optimalQuality)
state.moviePlayer.setPlaybackQualityRange(optimalQuality.quality, optimalQuality.quality, optimalQuality.formatId);
} catch (error) {
console.error('Error when setting resolution:', error);
}
}
function fallbackGetPlayer() {
if (window.location.pathname === '/watch') {
return document.querySelector('#movie_player');
} else if (window.location.pathname.startsWith('/shorts')) {
return document.querySelector('#shorts-player');
}
}
function handlePlayerStateChange(playerState) {
const playerElement = state.moviePlayer ?? fallbackGetPlayer();
if (!playerElement) return;
if (playerState === 1 && !playerElement.hasAttribute('YTHD-resolution-set')) {
playerElement.setAttribute('YTHD-resolution-set', 'true');
setResolution();
} else if (playerState === -1 && playerElement.hasAttribute('YTHD-resolution-set')) {
playerElement.removeAttribute('YTHD-resolution-set');
}
}
function processVideoLoad(event = null) {
state.moviePlayer = event?.target?.player_ ?? fallbackGetPlayer();
setResolution();
const playerElement = document.getElementById('movie_player');
if (playerElement && !playerElement.hasAttribute('YTHD-listener-added')) {
playerElement.addEventListener('onStateChange', handlePlayerStateChange);
playerElement.setAttribute('YTHD-listener-added', 'true');
}
}
// --- UI Logic ---
function createYTHDHeaderTrigger(titleText) {
const header = document.createElement('div');
header.id = 'ythd-header-trigger';
header.className = 'ytp-panel-header';
header.style.cursor = 'pointer';
const title = document.createElement('div');
title.className = 'ytp-panel-title';
title.style.display = 'flex';
title.style.justifyContent = 'space-between';
title.style.width = '100%';
title.style.alignItems = 'center';
title.style.padding = '16px';
const leftGroup = document.createElement('span');
leftGroup.style.display = 'flex';
leftGroup.style.alignItems = 'center';
leftGroup.style.gap = '8px';
leftGroup.append(ICONS.createPinIcon(), titleText);
const rightGroup = document.createElement('span');
rightGroup.id = 'ythd-header-label';
rightGroup.textContent = `${QUALITIES[state.userSettings.targetResolution].label} >`;
title.append(leftGroup, rightGroup);
header.appendChild(title);
return header;
}
function setupQualityMenuNavigation(qualityPanel) {
if (qualityPanel.querySelector('#ythd-animation-wrapper')) return;
const nativeHeader = qualityPanel.querySelector('.ytp-panel-header');
const settingsPopup = qualityPanel.closest('.ytp-popup.ytp-settings-menu');
const nativeTitleText = nativeHeader?.querySelector('.ytp-panel-title')?.textContent.trim() || 'Quality';
if (!nativeHeader || !settingsPopup) return;
const ythdHeaderTrigger = createYTHDHeaderTrigger(nativeTitleText);
nativeHeader.after(ythdHeaderTrigger);
const animationWrapper = document.createElement('div');
animationWrapper.id = 'ythd-animation-wrapper';
animationWrapper.style.position = 'relative';
animationWrapper.style.overflow = 'hidden';
const originalWrapperChildren = [...qualityPanel.children];
animationWrapper.append(...originalWrapperChildren);
qualityPanel.replaceChildren(animationWrapper);
const animateAndSwap = (contentSetupCallback, isForward) => {
const animationDuration = 250;
const oldContent = document.createElement('div');
oldContent.style.position = 'absolute';
oldContent.style.width = '100%';
oldContent.append(...animationWrapper.childNodes);
const newContent = document.createElement('div');
newContent.style.position = 'absolute';
newContent.style.width = '100%';
contentSetupCallback(newContent);
const oldFinalX = isForward ? '-100%' : '100%';
const newInitialX = isForward ? '100%' : '-100%';
oldContent.style.transform = 'translateX(0)';
newContent.style.transform = `translateX(${newInitialX})`;
const oldHeight = animationWrapper.offsetHeight;
animationWrapper.replaceChildren(oldContent, newContent);
const newHeight = newContent.offsetHeight;
animationWrapper.style.height = `${oldHeight}px`;
requestAnimationFrame(() => {
animationWrapper.style.transition = `height ${animationDuration}ms linear`;
oldContent.style.transition = `transform ${animationDuration}ms ease-in-out`;
newContent.style.transition = `transform ${animationDuration}ms ease-in-out`;
animationWrapper.style.height = `${newHeight}px`;
oldContent.style.transform = `translateX(${oldFinalX})`;
newContent.style.transform = 'translateX(0)';
});
setTimeout(() => {
animationWrapper.replaceChildren(...newContent.childNodes);
animationWrapper.style.cssText = 'position: relative;';
}, animationDuration);
};
const switchToNativeMenu = () => {
animateAndSwap((newWrapper) => {
newWrapper.append(...originalWrapperChildren);
const restoredTriggerLabel = newWrapper.querySelector('#ythd-header-label');
if (restoredTriggerLabel) {
restoredTriggerLabel.textContent = `${QUALITIES[state.userSettings.targetResolution].label} >`;
}
}, false);
};
const switchToYTHDMenu = () => {
animateAndSwap((newWrapper) => {
const backButton = document.createElement('button');
backButton.className = 'ytp-panel-back-button ytp-button';
const title = document.createElement('div');
title.className = 'ytp-panel-title';
title.style.display = 'flex';
title.style.alignItems = 'center';
title.style.gap = '8px';
title.append(ICONS.createPinIcon(), nativeTitleText);
const ythdHeader = document.createElement('div');
ythdHeader.className = 'ytp-panel-header';
ythdHeader.append(backButton, title);
const ythdMenu = document.createElement('div');
ythdMenu.className = 'ytp-panel-menu';
Object.entries(QUALITIES).forEach(([key, value]) => {
const menuItem = document.createElement('div');
menuItem.className = 'ytp-menuitem';
menuItem.setAttribute('role', 'menuitemradio');
menuItem.setAttribute('aria-checked', (state.userSettings.targetResolution === key).toString());
menuItem.dataset.resolutionKey = key;
const labelDiv = document.createElement('div');
labelDiv.className = 'ytp-menuitem-label';
labelDiv.textContent = `${value.p}p ${value.label.includes('K') ? `(${value.label})` : ''}`.trim();
menuItem.append(labelDiv);
ythdMenu.appendChild(menuItem);
});
newWrapper.append(ythdHeader, ythdMenu);
backButton.addEventListener('click', (event) => {
event.stopPropagation();
switchToNativeMenu();
});
ythdMenu.querySelectorAll('.ytp-menuitem').forEach((item) => {
item.addEventListener('click', (event) => {
event.stopPropagation();
const newResolution = item.dataset.resolutionKey;
if (state.userSettings.targetResolution === newResolution) return;
ythdMenu.querySelector('[aria-checked="true"]')?.setAttribute('aria-checked', 'false');
item.setAttribute('aria-checked', 'true');
state.userSettings.targetResolution = newResolution;
saveUserSettings();
setResolution();
document.getElementById('ythd-header-label').textContent = `${QUALITIES[newResolution].label} >`;
switchToNativeMenu();
});
});
}, true);
};
animationWrapper.querySelector('#ythd-header-trigger').addEventListener('click', (event) => {
event.stopPropagation();
switchToYTHDMenu();
});
if (!settingsPopup.dataset.ythdResetObserverAdded) {
settingsPopup.dataset.ythdResetObserverAdded = 'true';
const resetObserver = new MutationObserver(() => {
if (settingsPopup.style.display === 'none' && animationWrapper.querySelector('#ythd-header-trigger') === null) {
animationWrapper.replaceChildren(...originalWrapperChildren);
const restoredTriggerLabel = animationWrapper.querySelector('#ythd-header-label');
if (restoredTriggerLabel) {
restoredTriggerLabel.textContent = `${QUALITIES[state.userSettings.targetResolution].label} >`;
}
}
});
resetObserver.observe(settingsPopup, { attributes: true, attributeFilter: ['style'] });
}
}
// --- Settings Persistence ---
function saveUserSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.userSettings));
} catch (error) {
console.error('Error saving settings:', error);
}
}
function loadUserSettings() {
try {
const storedSettings = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (storedSettings) state.userSettings = { ...DEFAULT_SETTINGS, ...storedSettings };
if (!QUALITIES[state.userSettings.targetResolution]) {
state.userSettings.targetResolution = DEFAULT_SETTINGS.targetResolution;
}
saveUserSettings();
} catch (error) {
console.error('Error loading settings:', error);
}
}
function startObserveSettingsPanel() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && node.classList.contains('ytp-panel')) {
if (node.querySelector('.ytp-menuitem[role="menuitemradio"]') && node.classList.contains('ytp-quality-menu')) {
setupQualityMenuNavigation(node);
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: 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);
}
// --- Initialization ---
function initialize() {
loadUserSettings();
addEventListeners();
startObserveSettingsPanel();
}
initialize();
})();