Youtube HD Premium

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

Install this script?
Author's suggested script

You may also like Youtube Remember Speed.

Install this script
// ==UserScript==
// @name                Youtube HD Premium
// @icon                data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAqhJREFUaEPtmc9rE0EUxz+DjSh6UAQRxP4F9uhBRKjipef+FwqtoZdYEk3U4jGn0FJ6KrQnj6X0EKVKKIi9tAotPZSCYilFoq0/sK1Z92V329XGENiZSRZ2LtllZ9+8z/e9ncy8UcS8qZj7TwLQ7ggmEUgiEFGB/6aQAxeBq8Al4GxonDPAydD9+dB1qkFfefy9iZ9fgRrwC/jh96v6vz+Bj8B7BduNbBwDcOA6UABuAyciCqTr9d/ACxf0oYI3YaOHAA71KfWpq8QDF6BTP27H9/GRArk+ctSBZ0BGl2SG7YwoyB4COF66lDtY+X/1EPVvKXhVTxUHKsANw6rpNl9RcFM50A1sxEj9QAiJQrcA9LvT5XPd8liy1y8Ad4GSpQF1D3NPAO4DRd2WLdlL6wUYH4dKBSYnLfmPZoDZWejrg/l5GByE5WXTIIYAxO1aDaamYGgIthsuY3TAGQQI3KtWoVCAUgkODnQ4HbZhASAYbnUV0mmYm9MJYREgcHtmxvs+1td1gLQBQNze24OxMchmYXc3CkibAOQDl6k2k4GtrZgBLC56KbSwEMXx4F2LEdjchHweJia8KVZPswCwvw+jo5DLwc6OHrePrBgGKJdhYABWVnQ7bjiF1ta8OV+WFmab5ghMT8PSEhSL3lRpvmkGSKVAct5eqwPEfkMT+y3lZeBDbDf1kq6xLqv4AL3AyxhFQUoqvQpeh2ujI+46cdjeBBJppL9Li34UBCYP5Do4ErKIeiLV82PF3UAPB64Bj4E7biW4K5JO+l6WvajUbqW8/jZsttkBxwWgB7gCnPZfCg4z5P6UH6lzTfyUgxGp7ctBRdBkBxNsjiWXv4Seyd93+DDkG/AJeKfgc6NxOvUcoOXYJQAtS2WoYxIBQ8K2bDaJQMtSGer4B8aT1sve/dr7AAAAAElFTkSuQmCC
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_hd_namespace
// @version             2024.10.14
// 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/*
// @exclude             *://www.youtube.com/live_chat*
// @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 resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto'];
    const quality = {
        highres: 4320,
        hd2880:  2880,
        hd2160:  2160,
        hd1440:  1440,
        hd1080:  1080,
        hd720:   720,
        large:   480,
        medium:  360,
        small:   240,
        tiny:    144,
        auto:    0
    };

    const qualityLevels = Object.fromEntries(
        Object.entries(quality).map(([key, value]) => [value, key])
    );

    let userSettings = { ...DEFAULT_SETTINGS };
    let menuItems = [];

    let videoId = '';
    let resolvedTarget = '';
    let targetLabel = '';

    let ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0];
    let video = document.querySelector('video');

    const premiumIndicator = "Premium";

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

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

    // --- 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 {
            checkVideoValid(force);
            resolvedTarget = userSettings.targetResolution;
            let premiumData = null;
            if (resolvedTarget != 'auto') {
                const availableQualities = ytPlayer.getAvailableQualityLevels();
                resolvedTarget = findNextAvailableQuality(resolvedTarget, availableQualities);
                debugLog("Resolved target resolution: " + resolvedTarget);
                premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality === resolvedTarget && q.qualityLabel.trim().endsWith(premiumIndicator) && q.isPlayable);
            }
            ytPlayer.setPlaybackQualityRange(resolvedTarget, resolvedTarget, premiumData?.formatId);
            targetLabel = quality[resolvedTarget] + "p" + (premiumData ? " " + premiumIndicator : "");
            videoId = ytPlayer.getVideoData().video_id;
            debugLog(`Setting quality to: ${resolvedTarget == "auto" ? "auto" : targetLabel}`);
        } catch (error) {
            debugLog("Did not set resolution. " + error);
        }
    }

    function checkVideoValid(force) {
        //debugLog(JSON.stringify(ytPlayer?.getVideoData(), null, 2));
        //debugLog(JSON.stringify(ytPlayer?.getAvailableQualityLevels(), null, 2));
        try {
            if (/^https?:\/\/(www\.)?youtube\.com\/shorts\//.test(window.location.href)) {
                throw new AllowedExceptionError("Skipping Youtube Shorts.");
            }
            if (!ytPlayer?.getAvailableQualityLabels().length) {
                throw "Video data missing.";
            }
            if (videoId == ytPlayer.getVideoData().video_id && !force) {
                throw new AllowedExceptionError("Duplicate Load. Skipping.");
            }
        } catch (error) {
            throw error;
        }
    }

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

    function compareQualityLabelButIgnoreFrameRate(label1, label2) {
        const normalize = label => label.replace(new RegExp(`(\\d+p)\\d*( ${premiumIndicator})?$`), '$1').trim();
        return normalize(label1) === normalize(label2);
    }

    // Tries to validate setting changes. If this fails to detect a successful update it will call setResolution to attempt to force the playback quality to update again.
    function verifyChange(retries = 10) {
        const retryDelay = 500;
        try {
            checkVideoValid(true);
            let success = ytPlayer.getPreferredQuality() !== "auto" || userSettings.targetResolution === "auto";
            let currentQualityLabel = ytPlayer.getPlaybackQualityLabel();
            let isManualChange = !compareQualityLabelButIgnoreFrameRate(currentQualityLabel, targetLabel);

            if (success) {
                debugLog(`Quality is ${currentQualityLabel}. ${isManualChange ? "Manual change detected!" : "Change verified!"}`);
            } else {
                throw "Unable to verify change";
            }
        } catch (error) {
            if (error instanceof AllowedExceptionError) {
                debugLog(error);
            } else if (retries) {
                debugLog(`Error when verifying quality change. [${error}] Attempting quality change and reverifying after ${retryDelay}ms...`);
                setResolution(true);
                setTimeout(() => verifyChange(retries - 1), retryDelay);
            } else {
                debugLog(`Cannot verify quality change after retrying.`);
            }
        }
    }

    function processNewVideo() {
        try {
            video?.removeEventListener('playing', verifyChange, true);
            videoId = '';
            ytPlayer = document.getElementById("movie_player") || document.getElementsByClassName("html5-video-player")[0] || ytPlayer;
            video = ytPlayer?.querySelector('video') || document.querySelector('video');
            if (!video) throw "Video element not found.";
            video.addEventListener('playing', verifyChange, true);
            setResolution();
        } catch (error) {
            debugLog("Load error: " + error);
        }
    }

    // ----------------------------------------
    // Functions for the quality selection menu

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

    function showMenuOptions() {
        removeMenuOptions();
        const isExpandMenu = userSettings.expandMenu;
        const menuButton = GM_registerMenuCommand(`Quality Settings [${ isExpandMenu ? "HIDE" : "SHOW" }]`, () => {
            userSettings.expandMenu = !isExpandMenu;
            GM.setValue('expandMenu', userSettings.expandMenu);
            showMenuOptions();
        }, {
            autoClose: false
        });
        menuItems.push(menuButton);

        if (!isExpandMenu) return;

        resolutions.forEach((resolution) => {
            let qualityText = (resolution === 'auto') ? 'auto' : quality[resolution] + "p";
            if (resolution === userSettings.targetResolution) {
                qualityText += " ✅";
            }
            const qualityOption = GM_registerMenuCommand(qualityText, () => {
                setSelectedResolution(resolution);
            }, {
                autoClose: false,
            });
            menuItems.push(qualityOption);
        });

        const showDebug = userSettings.debug;
        const debugButton = GM_registerMenuCommand(`DEBUG [${showDebug ? "ON" : "OFF"}]`, () => {
            userSettings.debug = !showDebug;
            GM.setValue('debug', userSettings.debug);
            showMenuOptions();
        }, {
            autoClose: false
        });
        menuItems.push(debugButton);
    }

    function setSelectedResolution(resolution) {
        if (userSettings.targetResolution == resolution) return;
        userSettings.targetResolution = resolution;
        GM.setValue('targetResolution', resolution);
        showMenuOptions();
        setResolution(true);
    }

    // -----------------------------------------------
    // User setting handling

    async function loadUserSettings() {
        // Get all keys from GM
        const storedValues = await GM.listValues();

        // 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 GM.setValue(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 GM.deleteValue(key);
            }
        }

        // Retrieve and update user settings from GM
        const keyValuePairs = await Promise.all(storedValues.map(async key => [key, await GM.getValue(key)]));
        keyValuePairs.forEach(([newKey, newValue]) => {
            userSettings[newKey] = newValue;
        });

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

    // Main function
    async function initialize() {
        try {
            await loadUserSettings();
        } catch (error) {
            return debugLog(`Error loading user settings: ${error}. Exiting script.`);
        }
        window.addEventListener('popstate', () => { videoId = ''; });
        if (window.self == window.top) {
            // handle youtube website
            window.addEventListener("yt-navigate-finish", processNewVideo, true );
            showMenuOptions();
        } else {
            // handle iframes
            window.addEventListener('load', processNewVideo, true );
        }
    }

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