Youtube HD Premium

Automcatically switches to your pre-selected resolution. Enables premium when possible.

ติดตั้งสคริปต์นี้?
สคริปต์ที่แนะนำของผู้เขียน

คุณอาจชื่นชอบ Better Theater Mode for YouTube

ติดตั้งสคริปต์นี้
// ==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.youtube.com/img/favicon_48.png
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_hd_namespace
// @version             2025.01.13
// 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_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @license             MIT
// @description         Automcatically 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/en/scripts/498145-youtube-hd-premium
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    "use strict";

    const DEFAULT_SETTINGS = {
        targetResolution: "hd2160",
        expandMenu: false,
        debug: false
    };

    const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
    const GET_PREFERRED_LANGUAGE = () => {
        if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
            return 'zh-CN';
        } else {
            return BROWSER_LANGUAGE;
        }
    };

    const TRANSLATIONS = {
        'en-US': {
            qualityMenu: 'Quality Menu',
            debug: 'DEBUG'
        },
        'zh-TW': {
            qualityMenu: '畫質選單',
            debug: '偵錯'
        },
        'zh-CN': {
            qualityMenu: '画质菜单',
            debug: '排错'
        },
        'ja': {
            qualityMenu: '画質メニュー',
            debug: 'デバッグ'
        }
    };

    const GET_LOCALIZED_TEXT = () => {
        const language = GET_PREFERRED_LANGUAGE();
        return TRANSLATIONS[language] || TRANSLATIONS['en-US'];
    };

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

    const PREMIUM_INDICATOR_LABEL = "Premium";

    let userSettings = { ...DEFAULT_SETTINGS };
    let useCompatibilityMode = false;
    let isBrokenOrMissingGMAPI = false;
    let menuItems = [];
    let moviePlayer = null;

    // --- CLASS DEFINITIONS -----------

    class AllowedExceptionError extends Error {
        constructor(message) {
            super(message);
            this.name = "Allowed Exception";
        }
    }

    // --- GM FUNCTION OVERRIDES ------

    const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand;
    const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
    const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
    const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
    const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
    const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;

    // --- FUNCTIONS ------

    function debugLog(message) {
        if (!userSettings.debug) return;
        const stack = new Error().stack;
        const stackLines = stack.split("\n");
        const callerLine = stackLines[2] ? stackLines[2].trim() : "Line not found";
        message += "";
        if (!message.endsWith(".")) {
            message += ".";
        }
        console.log(`[YTHD DEBUG] ${message} Function called ${callerLine}`);
    }

    // Attempt to set the video resolution to target quality or the next best quality
    function setResolution(force = false) {
        try {
            if (!moviePlayer?.getAvailableQualityData().length) throw "Quality options missing.";
            let resolvedTarget = findNextAvailableQuality(userSettings.targetResolution, moviePlayer.getAvailableQualityLevels());
            const premiumData = moviePlayer.getAvailableQualityData().find(q =>
                q.quality === resolvedTarget &&
                q.qualityLabel.trim().endsWith(PREMIUM_INDICATOR_LABEL) &&
                q.isPlayable
            );
            moviePlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
            debugLog(`Setting quality to: ${resolvedTarget}${premiumData ? " Premium" : ""}`);
        } catch (error) {
            debugLog("Did not set resolution. " + error);
        }
    }

    function findNextAvailableQuality(target, availableQualities) {
        const targetValue = QUALITIES[target];
        return availableQualities
            .map(q => ({ quality: q, value: QUALITIES[q] }))
            .find(q => q.value <= targetValue)?.quality;
    }

    function processNewPage() {
        debugLog('Processing new page...');
        moviePlayer = document.querySelector('#movie_player');
        setResolution();
    }

    // ----------------------------------------
    // Functions for the quality selection menu
    function processMenuOptions(options, callback) {
        Object.values(options).forEach(option => {
            if (!option.alwaysShow && !userSettings.expandMenu) return;
            if (option.items) {
                option.items.forEach(item => callback(item));
            } else {
                callback(option);
            }
        });
    }

    function showMenuOptions() {
        removeMenuOptions();
        const menuOptions = {
            expandMenu: {
                alwaysShow: true,
                label: () => `${GET_LOCALIZED_TEXT().qualityMenu} ${userSettings.expandMenu ? "🔼" : "🔽"}`,
                menuId: "menuExpandBtn",
                handleClick: function () {
                    userSettings.expandMenu = !userSettings.expandMenu;
                    GMCustomSetValue('expandMenu', userSettings.expandMenu);
                    showMenuOptions();
                },
            },
            qualities: {
                items: Object.entries(QUALITIES).map(([label, resolution]) => ({
                    label: () => `${resolution}p ${label === userSettings.targetResolution ? "✅" : ""}`,
                    menuId: label,
                    handleClick: function () {
                        if (userSettings.targetResolution === label) return;
                        userSettings.targetResolution = label;
                        GMCustomSetValue('targetResolution', label);
                        setResolution();
                        showMenuOptions();
                    },
                })),
            },
            debug: {
                label: () => `${GET_LOCALIZED_TEXT().debug} ${userSettings.debug ? "✅" : ""}`,
                menuId: "debugBtn",
                handleClick: function () {
                    userSettings.debug = !userSettings.debug;
                    GMCustomSetValue('debug', userSettings.debug);
                    showMenuOptions();
                },
            },
        };

        processMenuOptions(menuOptions, (item) => {
            GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
                id: item.menuId,
                autoClose: false,
            });
            menuItems.push(item.menuId);
        });
    }

    function removeMenuOptions() {
        while (menuItems.length) {
            GMCustomUnregisterMenuCommand(menuItems.pop());
        }
    }

    // -----------------------------------------------
    // Verify Grease Monkey API exists and is working.
    function hasGreasyMonkeyAPI() {
        if (typeof GM != 'undefined') return true;
        if (typeof GM_info != 'undefined') {
            useCompatibilityMode = true;
            debugLog("Running in compatibility mode.");
            return true;
        }
        return false;
    }

    // -----------------------------------------------
    // User setting handling
    async function loadUserSettings() {
        try {
            // Get all keys from GM
            const storedValues = await GMCustomListValues();
            // Write any missing key-value pairs from DEFAULT_SETTINGS to GM
            for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
                if (!storedValues.includes(key)) {
                    await GMCustomSetValue(key, value);
                }
            }
            // Delete any extra keys in GM that are not in DEFAULT_SETTINGS
            for (const key of storedValues) {
                if (!(key in DEFAULT_SETTINGS)) {
                    await GMCustomDeleteValue(key);
                }
            }
            // Retrieve and update user settings from GM
            const keyValuePairs = await Promise.all(
                storedValues.map(async key => [key, await GMCustomGetValue(key)])
            );

            keyValuePairs.forEach(([newKey, newValue]) => {
                userSettings[newKey] = newValue;
            });

            debugLog(`Loaded user settings: [${Object.entries(userSettings).map(([key, value]) => `${key}: ${value}`).join(", ")}].`);
        } catch (error) {
            throw error;
        }
    }

    // ----------------
    // Main function
    async function initialize() {
        try {
            if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
            await loadUserSettings();
        } catch (error) {
            debugLog(`Error loading user settings: ${error}. Loading with default settings.`);
        }
        if (window.self == window.top) {
            processNewPage(); // event listeners fire too late on first page load if premium bitrate is available and selected
            window.addEventListener('yt-player-updated', processNewPage, true); //handle desktop site
            window.addEventListener('yt-page-data-updated', processNewPage, true); //handle desktop site lazy reload
            window.addEventListener('state-navigateend', processNewPage, true); //handle mobile site
            showMenuOptions();
        } else {
            window.addEventListener('loadstart', processNewPage, true);
        }
    }

    // Entry Point
    initialize();
})();