YouTube HD Premium

自動切換到你預先設定的畫質。會優先使用Premium位元率。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.org/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
// ==/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();
})();