您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
自动切换到你预先设定的画質。会优先使用Premium比特率。
// ==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.17 // @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 http://greasyfork.icu/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.startsWith('/shorts')) { return document.querySelector('#shorts-player'); } else if (window.location.pathname.startsWith('/watch')) { return document.querySelector('#movie_player'); } else { return document.querySelector('.inline-preview-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); window.addEventListener('pageshow', processVideoLoad); } // --- Initialization --- function initialize() { loadUserSettings(); addEventListeners(); startObserveSettingsPanel(); } initialize(); })();