// ==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.30.1 // @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* // @require https://update.greasyfork.icu/scripts/549881/1669057/YouTube%20Helper%20API.js // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @run-at document-idle // @inject-into page // @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/scripts/498145-youtube-hd-premium // @downloadURL https://update.greasyfork.icu/scripts/498145/YouTube%20HD%20Premium.user.js // @updateURL https://update.greasyfork.icu/scripts/498145/YouTube%20HD%20Premium.meta.js // ==/UserScript== /*jshint esversion: 11 */ (function () { 'use strict'; const STORAGE_KEY = 'YTHD_settings'; const DEFAULT_SETTINGS = { targetResolution: 'hd2160', }; 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; }, }; let userSettings = { ...DEFAULT_SETTINGS }; // --- Core Logic --- function setResolution() { const targetResolution = userSettings.targetResolution; window.youtubeHelperApi.setPlaybackResolution(targetResolution); } function handlePlayerStateChange(playerState) { const playerElement = window.youtubeHelperApi.player.playerObject; 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() { setResolution(); const playerContainer = window.youtubeHelperApi.player.playerObject; if (playerContainer && !playerContainer.hasAttribute('YTHD-listener-added')) { playerContainer.addEventListener('onStateChange', handlePlayerStateChange); playerContainer.setAttribute('YTHD-listener-added', 'true'); } } // --- Desktop 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 = `${window.youtubeHelperApi.POSSIBLE_RESOLUTIONS[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 = `${ window.youtubeHelperApi.POSSIBLE_RESOLUTIONS[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(window.youtubeHelperApi.POSSIBLE_RESOLUTIONS).forEach(([key, value]) => { const menuItem = document.createElement('div'); menuItem.className = 'ytp-menuitem'; menuItem.setAttribute('role', 'menuitemradio'); menuItem.setAttribute('aria-checked', (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', async (event) => { event.stopPropagation(); const newResolution = item.dataset.resolutionKey; if (userSettings.targetResolution === newResolution) return; ythdMenu.querySelector('[aria-checked="true"]')?.setAttribute('aria-checked', 'false'); item.setAttribute('aria-checked', 'true'); userSettings.targetResolution = newResolution; await window.youtubeHelperApi.saveToStorage(STORAGE_KEY, userSettings); setResolution(); document.getElementById( 'ythd-header-label', ).textContent = `${window.youtubeHelperApi.POSSIBLE_RESOLUTIONS[newResolution].label} >`; switchToNativeMenu(); }); }); }, true); }; animationWrapper.querySelector('#ythd-header-trigger').addEventListener('click', (event) => { event.stopPropagation(); switchToYTHDMenu(); }); } function startDesktopObserver() { 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 }); } // --- Mobile UI Logic --- function showMobileQualityOverlay(closeNativeMenuCallback) { if (document.getElementById('ythd-mobile-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'ythd-mobile-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 9999; backdrop-filter: blur(4px); `; const menuBox = document.createElement('div'); menuBox.style.cssText = ` background: #282828; color: white; padding: 16px; border-radius: 12px; min-width: 280px; max-width: 90%; font-family: "Roboto", "Arial", sans-serif; `; const title = document.createElement('div'); title.textContent = 'Set Quality Preference'; title.style.cssText = 'font-size: 1.2em; margin-bottom: 16px; font-weight: 500;'; menuBox.appendChild(title); Object.entries(window.youtubeHelperApi.POSSIBLE_RESOLUTIONS).forEach(([key, value]) => { const menuItem = document.createElement('div'); menuItem.textContent = `${value.p}p ${value.label.includes('K') ? `(${value.label})` : ''}`.trim(); menuItem.style.cssText = 'padding: 14px; cursor: pointer; border-radius: 8px;'; if (userSettings.targetResolution === key) { menuItem.style.fontWeight = 'bold'; menuItem.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; } menuItem.onclick = async () => { userSettings.targetResolution = key; await window.youtubeHelperApi.saveToStorage(STORAGE_KEY, userSettings); setResolution(); document.body.removeChild(overlay); if (closeNativeMenuCallback) closeNativeMenuCallback(); }; menuBox.appendChild(menuItem); }); overlay.onclick = (event) => { if (event.target === overlay) { document.body.removeChild(overlay); } }; overlay.appendChild(menuBox); document.body.appendChild(overlay); } function injectMobileTrigger(bottomSheetNode) { if (bottomSheetNode.querySelector('.ythd-mobile-trigger')) return; const templateItem = bottomSheetNode.querySelector('[role=menuitem], ytm-menu-service-item-renderer'); if (!templateItem) return; const triggerItem = templateItem.cloneNode(true); triggerItem.classList.add('ythd-mobile-trigger'); triggerItem.removeAttribute('aria-disabled'); const icon = triggerItem.querySelector('yt-icon, c3-icon'); const label = triggerItem.querySelector('.yt-formatted-string, .menu-service-item-text'); if (icon) { icon.replaceChildren(ICONS.createPinIcon()); } if (label) { label.textContent = 'Set Quality Preference'; } triggerItem.onclick = (event) => { event.preventDefault(); event.stopPropagation(); const closeNativeMenu = () => document.querySelector('ytw-scrim')?.click(); showMobileQualityOverlay(closeNativeMenu); }; const parent = templateItem.parentElement; parent.insertBefore(triggerItem, parent.firstChild); } function initializeMobileEventListeners() { let settingsClicked = false; const observer = new MutationObserver((mutations, obs) => { if (!settingsClicked) return; const bottomSheet = document.querySelector('bottom-sheet-container:not(:empty)'); if (bottomSheet) { injectMobileTrigger(bottomSheet); settingsClicked = false; obs.disconnect(); } }); document.addEventListener( 'click', (event) => { const settingsButton = event.target.closest( '.player-settings-icon, [aria-label*="Settings"], .slim-video-metadata-actions button', ); if (settingsButton) { settingsClicked = true; observer.observe(document.body, { childList: true, subtree: true }); } }, true, ); } // --- Initialization --- async function initialize() { userSettings = await window.youtubeHelperApi.loadAndCleanFromStorage(STORAGE_KEY, DEFAULT_SETTINGS); await window.youtubeHelperApi.saveToStorage(STORAGE_KEY, userSettings); window.youtubeHelperApi.page.isMobile ? initializeMobileEventListeners() : startDesktopObserver(); window.youtubeHelperApi.eventTarget.addEventListener('yt-helper-api-ready', processVideoLoad); } initialize(); })();