// ==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.11.20.4 // @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/1698202/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 none // ==/UserScript== /*jshint esversion: 11 */ (function () { 'use strict'; const api = globalThis.youtubeHelperApi; if (!api) return console.error('Helper API not found. Likely incompatible script manager or extension settings.'); const STORAGE_KEY = 'YTHD_settings'; const DEFAULT_SETTINGS = { targetResolution: 'hd2160', }; const SVG_NS = 'http://www.w3.org/2000/svg'; const CSS_STYLES = ` #ythd-animation-wrapper { position: relative; overflow: hidden; transition: height 0.25s ease-in-out; } .ythd-slide-panel { position: absolute; width: 100%; top: 0; left: 0; transition: transform 0.25s ease-in-out; } #ythd-mobile-overlay { 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); } .ythd-mobile-menu-box { background: #282828; color: white; padding: 16px; border-radius: 12px; min-width: 280px; max-width: 90%; font-family: "Roboto", "Arial", sans-serif; } .ythd-mobile-title { font-size: 1.2em; margin-bottom: 16px; font-weight: 500; } .ythd-menu-item { padding: 14px; cursor: pointer; border-radius: 8px; } .ythd-menu-item.active { font-weight: bold; background: rgba(255, 255, 255, 0.1); } `; 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 }; function injectStyles() { const styleElement = document.createElement('style'); styleElement.textContent = CSS_STYLES; document.head.appendChild(styleElement); } function setResolution() { api.setPlaybackResolution(userSettings.targetResolution); } function handlePlayerStateChange(playerState) { const playerElement = api.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 = api.player.playerObject; if (playerContainer && !playerContainer.hasAttribute('YTHD-listener-added')) { playerContainer.addEventListener('onStateChange', handlePlayerStateChange); playerContainer.setAttribute('YTHD-listener-added', 'true'); } } 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 = `${api.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 nativeTitleText = nativeHeader?.querySelector('.ytp-panel-title')?.textContent.trim() || 'Quality'; if (!nativeHeader) return; const ythdHeaderTrigger = createYTHDHeaderTrigger(nativeTitleText); nativeHeader.after(ythdHeaderTrigger); const animationWrapper = document.createElement('div'); animationWrapper.id = 'ythd-animation-wrapper'; const nativePanel = document.createElement('div'); nativePanel.className = 'ythd-slide-panel'; const customPanel = document.createElement('div'); customPanel.className = 'ythd-slide-panel'; customPanel.style.transform = 'translateX(100%)'; Array.from(qualityPanel.children).forEach((child) => { nativePanel.appendChild(child); }); animationWrapper.appendChild(nativePanel); animationWrapper.appendChild(customPanel); qualityPanel.appendChild(animationWrapper); const performSlideAnimation = (direction) => { const isForward = direction === 'forward'; const currentPanel = isForward ? nativePanel : customPanel; const nextPanel = isForward ? customPanel : nativePanel; nextPanel.style.transition = 'none'; nextPanel.style.transform = isForward ? 'translateX(100%)' : 'translateX(-100%)'; void nextPanel.offsetHeight; animationWrapper.style.height = `${nextPanel.scrollHeight}px`; nextPanel.style.transition = 'transform 0.25s ease-in-out'; currentPanel.style.transform = isForward ? 'translateX(-100%)' : 'translateX(100%)'; nextPanel.style.transform = 'translateX(0)'; }; const switchToNativeMenu = () => { const restoredTriggerLabel = document.getElementById('ythd-header-label'); if (restoredTriggerLabel) { restoredTriggerLabel.textContent = `${api.POSSIBLE_RESOLUTIONS[userSettings.targetResolution].label} >`; } performSlideAnimation('back'); }; const switchToYTHDMenu = () => { customPanel.replaceChildren(); const backButton = document.createElement('button'); backButton.className = 'ytp-panel-back-button ytp-button'; backButton.style.padding = '0'; 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 header = document.createElement('div'); header.className = 'ytp-panel-header'; header.append(backButton, title); const menu = document.createElement('div'); menu.className = 'ytp-panel-menu'; Object.entries(api.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); menuItem.addEventListener('click', async (event) => { event.stopPropagation(); const newResolution = menuItem.dataset.resolutionKey; if (userSettings.targetResolution === newResolution) return; userSettings.targetResolution = newResolution; await api.saveToStorage(STORAGE_KEY, userSettings); setResolution(); switchToNativeMenu(); }); menu.appendChild(menuItem); }); customPanel.append(header, menu); backButton.addEventListener('click', (event) => { event.stopPropagation(); switchToNativeMenu(); }); performSlideAnimation('forward'); }; animationWrapper.style.height = `${nativePanel.scrollHeight}px`; ythdHeaderTrigger.addEventListener('click', (event) => { event.stopPropagation(); switchToYTHDMenu(); }); } function startDesktopObserver() { const observer = new MutationObserver((mutations) => { let nodesAdded = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { nodesAdded = true; break; } } if (!nodesAdded) return; 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 showMobileQualityOverlay(closeNativeMenuCallback) { if (document.getElementById('ythd-mobile-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'ythd-mobile-overlay'; const menuBox = document.createElement('div'); menuBox.className = 'ythd-mobile-menu-box'; const title = document.createElement('div'); title.className = 'ythd-mobile-title'; title.textContent = 'Set Quality Preference'; menuBox.appendChild(title); Object.entries(api.POSSIBLE_RESOLUTIONS).forEach(([key, value]) => { const menuItem = document.createElement('div'); menuItem.className = 'ythd-menu-item'; if (userSettings.targetResolution === key) { menuItem.classList.add('active'); } menuItem.textContent = `${value.p}p ${value.label.includes('K') ? `(${value.label})` : ''}`.trim(); menuItem.onclick = async () => { userSettings.targetResolution = key; await api.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() { document.addEventListener( 'click', (event) => { const settingsButton = event.target.closest( '.player-settings-icon, [aria-label*="Settings"], .slim-video-metadata-actions button', ); if (settingsButton) { const observer = new MutationObserver((mutations, obs) => { const bottomSheet = document.querySelector('bottom-sheet-container:not(:empty)'); if (bottomSheet) { injectMobileTrigger(bottomSheet); obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); } }, true, ); } async function initialize() { injectStyles(); userSettings = await api.loadAndCleanFromStorage(STORAGE_KEY, DEFAULT_SETTINGS); await api.saveToStorage(STORAGE_KEY, userSettings); api.page.isMobile ? initializeMobileEventListeners() : startDesktopObserver(); api.eventTarget.addEventListener('yt-helper-api-ready', processVideoLoad); } initialize(); })();