Youtube HD Premium

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

目前為 2024-07-06 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.07.07.1
// @match         *://www.youtube.com/*
// @exclude       *://www.youtube.com/live_chat*
// @grant         GM.getValue
// @grant         GM.setValue
// @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のビットレートを優先的に選択します。

// ==/UserScript==

/*jshint esversion: 11 */

(function() {
    "use strict";

    const DEBUG = false;

    const DEFAULT_SETTINGS = {
        targetResolution: "hd2160"
    };

    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 menuCommandIds = [];

    let doc = document, win = window;
    let vidId = null;

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

    function debugLog(message, shouldShow = true) {
        if (DEBUG && shouldShow) {
            console.log("YTHD DEBUG | " + message);
        }
    }

    // --------------------
    // Attempt to set the video resolution to target quality or the next best quality
    function setResolution(target, force = false) {
        if (target == 'auto') return;

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

        if (!isValidVideo(ytPlayer, force)) return;

        vidId = ytPlayer.getVideoData().video_id;

        let localItem = null;
        try {
            localStorage.getItem("yt-player-quality");
        } catch {
            debugLog("Fetching last used quality failed catastrophically. Likely the website is not YouTube. If website is YouTube then YouTube changed something.");
        }

        let limitList = ytPlayer.getAvailableQualityLevels();
        let limit = limitList[0];
        if (quality[target] > quality[limit]) {
            target = limit;
        }

        let premiumIndicator = "Premium";
        let premiumData = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes(premiumIndicator) && q.isPlayable);
        ytPlayer.setPlaybackQualityRange(target, target, premiumData?.formatId);
        debugLog("Set quality to: " + target + (premiumData ? " Premium" : ""));

        if (localItem){
            localStorage.setItem("yt-player-quality",localItem);
        }
        else {
            localStorage.removeItem("yt-player-quality");
        }
    }

    function isValidVideo(ytPlayer, force) {

        if (!ytPlayer?.getAvailableQualityLabels()[0]) {
            debugLog("Video data missing");
            vidId = null;
            return false;
        }

        if (window.location.href.startsWith("https://www.youtube.com/shorts/")) {
            debugLog("Skipping Youtube Shorts");
            vidId = null;
            return false;
        }

        if (vidId == ytPlayer.getVideoData().video_id && !force) {
            debugLog("Duplicate load");
            return false;
        }

        return true;
    }

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

    function createQualityMenu() {
        const menu_command_id_0 = GM_registerMenuCommand("Set Preferred Quality (show/hide)", function(MouseEvent) {
            menuCommandIds[0] ? removeQualityMenuItems() : showQualityMenuItems();
        }, {
            autoClose: false
        });
    }

    function showQualityMenuItems() {
        removeQualityMenuItems();
        resolutions.forEach((resolution) => {
            let qualityText = (resolution === 'auto') ? 'auto' : quality[resolution] + "p";
            if (resolution === userSettings.targetResolution) {
                qualityText += " (selected)";
            }
            let menuCommandId = GM_registerMenuCommand(qualityText, function() {
                setSelectedResolution(resolution);
            }, {
                autoClose: false,
            });
            menuCommandIds.push(menuCommandId);
        });
    }

    function removeQualityMenuItems() {
        menuCommandIds.forEach((menuCommandId) => {
            GM_unregisterMenuCommand(menuCommandId);
        });
        menuCommandIds = [];
    }

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

    // --------------------
    // Sync settings with locally storaged values
    async function applySettings() {
        try {
            if (typeof GM != 'undefined' && GM.getValue && GM.setValue && GM.deleteValue && GM.listValues) {
                // Get all keys from GM
                const gmKeys = await GM.listValues();


                // Write any missing key-value pairs from DEFAULT_SETTINGS to GM
                await Promise.all(Object.entries(DEFAULT_SETTINGS).map(async ([k, v]) => {
                    if (!gmKeys.includes(k)) {
                        await GM.setValue(k, v);
                    }
                }));

                // Delete any extra keys in GM that are not in DEFAULT_SETTINGS
                await Promise.all(gmKeys.map(async key => {
                    if (!(key in DEFAULT_SETTINGS)) {
                        await GM.deleteValue(key);
                    }
                }));

                // Retrieve and update user settings from GM
                await Promise.all(
                    gmKeys.map(k => GM.getValue(k).then(v => [k, v]))
                ).then(c => c.forEach(([nk, nv]) => {
                    userSettings[nk] = nv;
                }));

                debugLog(Object.entries(userSettings).map(([k, v]) => k + ": " + v).join(", "));
            }
        } catch (error) {
            debugLog("Error in applySettings: " + error.message);
        }
    }

    // --------------------
    // Main function
    function main() {
       if (win.self == win.top) { createQualityMenu(); }
        setResolution(userSettings.targetResolution);
        win.addEventListener("loadstart", () => { setResolution(userSettings.targetResolution); }, true);
    }

    // --------------------
    // Entry Point
    applySettings().then(main);
})();