Youtube HD Premium

自動切換到你預先設定的畫質。會優先使用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.08.02
// @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               GM.getValue
// @grant               GM.setValue
// @grant               GM.deleteValue
// @grant               GM.listValues
// @grant               GM.registerMenuCommand
// @grant               GM.unregisterMenuCommand
// @grant               GM.notification
// @grant               GM.addValueChangeListener
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @grant               GM_notification
// @grant               GM_addValueChangeListener
// @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 DEFAULT_SETTINGS = {
        targetResolution: 'hd2160',
        expandMenu: false,
    };

    const QUALITIES = {
        highres: 4320,
        hd2160: 2160,
        hd1440: 1440,
        hd1080: 1080,
        hd720: 720,
        large: 480,
        medium: 360,
        small: 240,
        tiny: 144,
    };

    const PREMIUM_INDICATOR = 'Premium';
    const REQUIRED_TAMPERMONKEY_VERSION = '5.4.624';

    const TRANSLATIONS = {
        'en-US': {
            tampermonkeyOutdatedAlertMessage: `It looks like you're using an older version of Tampermonkey that might cause menu issues. For the best experience, please update to version ${REQUIRED_TAMPERMONKEY_VERSION} or later.`,
            qualityMenu: 'Quality Menu',
        },
        'zh-TW': {
            tampermonkeyOutdatedAlertMessage: `看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 ${REQUIRED_TAMPERMONKEY_VERSION} 或更高版本。`,
            qualityMenu: '畫質選單',
        },
        'zh-CN': {
            tampermonkeyOutdatedAlertMessage: `看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 ${REQUIRED_TAMPERMONKEY_VERSION} 或更高版本。`,
            qualityMenu: '画质菜单',
        },
        ja: {
            tampermonkeyOutdatedAlertMessage: `ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン${REQUIRED_TAMPERMONKEY_VERSION}以上に更新してください。`,
            qualityMenu: '画質メニュー',
        },
    };

    const state = {
        userSettings: { ...DEFAULT_SETTINGS },
        isIframe: window.self !== window.top,
        isOldTampermonkey: false,
        useCompatibilityMode: typeof GM === 'undefined' && typeof GM_info !== 'undefined',
        moviePlayer: null,
        registeredMenuIds: [],
    };

    const GM_API = {
        registerMenuCommand: state.useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand,
        unregisterMenuCommand: state.useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand,
        getValue: state.useCompatibilityMode ? GM_getValue : GM.getValue,
        setValue: state.useCompatibilityMode ? GM_setValue : GM.setValue,
        deleteValue: state.useCompatibilityMode ? GM_deleteValue : GM.deleteValue,
        listValues: state.useCompatibilityMode ? GM_listValues : GM.listValues,
        notification: state.useCompatibilityMode ? GM_notification : GM.notification,
        addValueChangeListener: state.useCompatibilityMode ? GM_addValueChangeListener : GM.addValueChangeListener,
    };

    const getLocalizedText = () => {
        const lang = navigator.language || navigator.userLanguage;
        const preferredLang = lang.startsWith('zh') && lang !== 'zh-TW' ? 'zh-CN' : lang;
        return TRANSLATIONS[preferredLang] || TRANSLATIONS['en-US'];
    };

    function resolveOptimalQuality(videoQualityData, targetResolutionString) {
        const availableQualities = [...new Set(videoQualityData.map((q) => q.quality))];
        const targetValue = QUALITIES[targetResolutionString];

        const bestQualityString = availableQualities
            .filter((q) => QUALITIES[q] <= targetValue)
            .sort((a, b) => QUALITIES[b] - QUALITIES[a])[0];

        if (!bestQualityString) return null;

        let normalCandidate = null;
        let 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) throw new Error('Movie player not found.');

            const videoQualityData = state.moviePlayer.getAvailableQualityData();

            if (!videoQualityData.length) {
                // Fallback for non-auto-playing videos that have preloading disabled
                try {
                    const videoElement = state.moviePlayer.querySelector('video');
                    state.moviePlayer.setPlaybackQualityRange(state.userSettings.targetResolution); // Instant, but may break UI temporarily
                    videoElement.addEventListener('play', setResolution, { once: true }); // Slower, but guarantees correct quality and fixes UI
                } catch {
                    console.error('Failed to set resolution using fallback method.');
                }
                return;
            }

            const optimalQuality = resolveOptimalQuality(videoQualityData, state.userSettings.targetResolution);
            if (optimalQuality) {
                state.moviePlayer.setPlaybackQualityRange(optimalQuality.quality, optimalQuality.quality, optimalQuality.formatId);
            }
        } catch (error) {
            console.error('Did not set resolution.', error);
        }
    }

    function handlePlayerStateChange(playerState) {
        const playerElement = document.getElementById('movie_player');
        if (!playerElement) return;
        if (playerState === -1 && playerElement.hasAttribute('resolution-set')) {
            playerElement.removeAttribute('resolution-set');
        }
        if (playerState === 1 && !playerElement.hasAttribute('resolution-set')) {
            playerElement.setAttribute('resolution-set', '');
            setResolution();
        }
    }

    function processVideoLoad(event = null) {
        state.moviePlayer = event?.target?.player_ ?? document.querySelector('#movie_player');
        setResolution();

        const playerElement = document.getElementById('movie_player');
        if (playerElement && !playerElement.hasAttribute('state-change-listener-added')) {
            playerElement.addEventListener('onStateChange', handlePlayerStateChange);
            playerElement.setAttribute('state-change-listener-added', '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);

        // Sync settings across tabs
        GM_API.addValueChangeListener('settings', (key, oldValue, newValue, remote) => {
            if (!remote) return;

            state.userSettings = newValue;
            showMenuOptions();

            const applyResolutionChange = () => {
                if (!document.hidden) setResolution();
            };

            if (document.hidden) {
                window.addEventListener('visibilitychange', applyResolutionChange, { once: true });
            } else {
                applyResolutionChange();
            }
        });
    }

    function removeMenuOptions() {
        while (state.registeredMenuIds.length) {
            GM_API.unregisterMenuCommand(state.registeredMenuIds.pop());
        }
    }

    function showMenuOptions() {
        removeMenuOptions();
        const shouldUseFallback = state.isOldTampermonkey || state.isIframe;
        const localizedText = getLocalizedText();

        const menuItems = [
            !shouldUseFallback && {
                label: () => `${localizedText.qualityMenu} ${state.userSettings.expandMenu ? '🔼' : '🔽'}`,
                menuId: 'menuExpandBtn',
                alwaysShow: true,
                async handleClick() {
                    state.userSettings.expandMenu = !state.userSettings.expandMenu;
                    await updateSetting('expandMenu', state.userSettings.expandMenu);
                    showMenuOptions();
                },
            },
            ...Object.entries(QUALITIES).map(([label, resolution]) => ({
                label: () => `${resolution}p ${label === state.userSettings.targetResolution ? '✅' : ''}`,
                menuId: label,
                alwaysShow: false,
                async handleClick() {
                    if (state.userSettings.targetResolution === label) return;
                    state.userSettings.targetResolution = label;
                    await updateSetting('targetResolution', label);
                    setResolution();
                    showMenuOptions();
                },
            })),
        ];

        menuItems.forEach((item) => {
            if (!item) return; // Skip invalid items
            if (item.alwaysShow || state.userSettings.expandMenu || shouldUseFallback) {
                GM_API.registerMenuCommand(item.label(), item.handleClick, {
                    id: item.menuId,
                    autoClose: shouldUseFallback,
                });
                state.registeredMenuIds.push(item.menuId);
            }
        });
    }

    async function updateSetting(key, value) {
        try {
            const currentSettings = (await GM_API.getValue('settings')) || {};
            currentSettings[key] = value;
            await GM_API.setValue('settings', currentSettings);
        } catch (error) {
            console.error('Error updating setting:', error);
        }
    }

    async function loadUserSettings() {
        const storedSettings = (await GM_API.getValue('settings')) || {};
        state.userSettings = { ...DEFAULT_SETTINGS, ...storedSettings };

        if (!QUALITIES[state.userSettings.targetResolution]) {
            state.userSettings.targetResolution = DEFAULT_SETTINGS.targetResolution;
        }
        await GM_API.setValue('settings', state.userSettings);
    }

    async function cleanupOldStorage() {
        try {
            const allowedKeys = ['settings', 'versionWarningShown'];
            const keys = await GM_API.listValues();
            for (const key of keys) {
                if (!allowedKeys.includes(key)) {
                    await GM_API.deleteValue(key);
                }
            }
        } catch (error) {
            console.error('Error cleaning up old storage keys:', error);
        }
    }

    function compareVersions(v1, v2) {
        const parts1 = v1.split('.').map(Number);
        const parts2 = v2.split('.').map(Number);
        const len = Math.max(parts1.length, parts2.length);
        for (let i = 0; i < len; i++) {
            const num1 = parts1[i] || 0;
            const num2 = parts2[i] || 0;
            if (num1 > num2) return 1;
            if (num1 < num2) return -1;
        }
        return 0;
    }

    function checkTampermonkeyVersion() {
        if (state.useCompatibilityMode || GM_info.scriptHandler !== 'Tampermonkey') return;
        if (compareVersions(GM_info.version, REQUIRED_TAMPERMONKEY_VERSION) < 0) {
            state.isOldTampermonkey = true;
            const versionWarningShown = GM_API.getValue('versionWarningShown', false);
            if (!versionWarningShown) {
                GM_API.setValue('versionWarningShown', true);
                GM_API.notification({
                    text: getLocalizedText().tampermonkeyOutdatedAlertMessage,
                    timeout: 15000,
                });
            }
        }
    }

    async function initialize() {
        if (state.useCompatibilityMode) console.warn('Running in Greasemonkey compatibility mode. Some features might be limited.');
        try {
            await cleanupOldStorage();
            await loadUserSettings();
            checkTampermonkeyVersion();
        } catch (error) {
            console.error(`Error during initialization: ${error}. Loading with default settings.`);
        }

        addEventListeners();
        showMenuOptions();
    }

    initialize();
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。