Greasy Fork

Youtube HD premium fixed

Automcatically switches to your pre-selected resolution. Enables premium when possible. Tested on the newest beta premium-only UI.

目前为 2024-06-25 提交的版本。查看 最新版本

// ==UserScript==
// @name          Youtube HD premium fixed
// @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
// @description   Automcatically switches to your pre-selected resolution. Enables premium when possible. Tested on the newest beta premium-only UI.
// @version       2024.06.25
// @match         https://*.youtube.com/*
// @noframes
// @grant         GM.getValue
// @grant         GM.setValue
// @license       MIT

// ==/UserScript==

// The video will only resize when in theater mode on the main youtube website.
// By default only runs on youtube website, not players embedded on other websites, but there is experimental support for embeds.
// To enable experimental support for embedded players outside of YouTube website, do the following steps:
//   add " @include * " to the script metadata
//   remove " @noframes " from the script metadata

// 2024.06.25
// Fixed auto theater mode.🥳
// Removed HFR functions.
// Defaults to aways settings resolution early.
// Added support for 'auto' resolution since YouTube officially returns the value now.
// Simplified resolution calculation.
//    Function will also find the closet resolution not exceeding the target if the target resolution cannot be found.
// Removed calls to obsolete functions. (deprecated by YouTube)
// Removed options to manually switch between button and API mode. Will now default to API mode then fallback to button emulation when API calls aren't supported.
// Removed code that stops calculation of resolution when playing the same video back to which. Fixing the edge case of video having different resolution between playback, often caused by a video being too new.
// Removed fetching resolution data with video ID. Pretty sure YouTube have changed their loading order since this script was created so this is no longer necessary.
// Removed flushBuffer as it relied on loading video data with video ID.
// Removed enableErrorScreenWorkaround.
// Removed ability to set custom size video players. YouTube no longer provides direct API calls to resize the player.
//    Now 9 different divs are stsacked on top of each other for the player, there are just too many ways YouTube can accidentally break a CSS based approach.
//    Besides, resizing no longer makes much sense with the new AI, and assuming they will adopt the beta UI in the future, finding a solution to this problem seems like a huge waste of time and effort.
// Target resolution now defaults to 4k (hd2160) [added 2024.06.15]
// [NOTE*] There was a massive amount of change this update so please do give feedback if it is somehow catastrophically backwards uncompatible.

// 2024.06.15
// Fix issue that disabled the premium quality option on premium accounts.
// Fix conflict between api quality selector and button click fallback.
// Fix bug that caused the script to not recognize the quality selector button in the Youtube player UI when using button click fallback.
// Fix bug that caused the script to fail to fetch video quality data when switching between videos using button click fallback when not reloading.

// 2024.01.17
// Fix issue with user script managers that don't define GM

// 2024.01.14
// Partially fix auto theater mode again after more youtube changes
// Note that if you want to turn theater mode back off after using auto theater you have to click the button a few times. This is a known issue that hasn't been fixed yet.

(function() {
    "use strict";

    // --- SETTINGS -------

    // PLEASE NOTE:
    // Settings will be saved the first time the script is loaded so that your changes aren't undone by an update.
    // If you want to make adjustments, please set "overwriteStoredSettings" to true.
    // Otherwise, your settings changes will NOT have an effect because it will use the saved settings.
    // After the script has next been run by loading a video with "overwriteStoredSettings" as true, your settings will be updated.
    // Then after that, you can set it to false again to prevent your settings from being changed by an update.

    let settings = {
        // Target Resolution to always set to. If not available, the next best resolution will be used.
        changeResolution: true,
        preferPremium: true,
        targetRes: "hd2160",
        // Choices for targetRes are currently:
        //   "highres" >= ( 8K / 4320p / QUHD  )
        //   "hd2880"   = ( 5K / 2880p /  UHD+ )
        //   "hd2160"   = ( 4K / 2160p /  UHD  )
        //   "hd1440"   = (      1440p /  QHD  )
        //   "hd1080"   = (      1080p /  FHD  )
        //   "hd720"    = (       720p /   HD  )
        //   "large"    = (       480p         )
        //   "medium"   = (       360p         )
        //   "small"    = (       240p         )
        //   "tiny"     = (       144p         )
        //   "auto"     = (       auto         )

        // If autoTheater is true, each video page opened will default to theater mode.
        autoTheater: false,

        // Enabling cookies makes theater mode load faster.This is off by default
        allowCookies: false,

        // This make it so the scripts sses the settings coded here instead of a seperate save file.
        overwriteStoredSettings: true
    };

    // --------------------
    // --- GLOBALS --------
    // --------------------

    const DEBUG = false;
    // Possible resolution choices (in decreasing order, i.e. highres is the largest):
    const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto'];
    // YouTube has to be at least 480x270 for the player UI
    const ranks = {
        highres: "10",
        hd2880:  "9",
        hd2160:  "8",
        hd1440:  "7",
        hd1080:  "6",
        hd720:   "5",
        large:   "4",
        medium:  "3",
        small:   "2",
        tiny:    "1",
        auto:    "0"
    };

    let doc = document, win = window;

    // --------------------

    function debugLog(message) {
        if (DEBUG) {
            console.log("YTHD | " + message);
        }
    }

    // --------------------

    // Used only for compatibility with web extensions version of Greasemonkey
    function unwrapElement(el) {
        if (el && el.wrappedJSObject) {
            return el.wrappedJSObject;
        }
        return el;
    }

    // --------------------

    // Attempt to set the video resolution to desired quality or the next best quality
    function setResolution(ytPlayer, resolutionList) {

        // No idea why anyone would use auto when using this script. But this handles that edge case.
        if (settings.targetRes.toLowerCase() == "auto") {
            debugLog("Using auto resolution. Skipping calculations.");
            ytPlayer.setPlaybackQuality("auto");
            return;
        }

        let target = settings.targetRes;
        let limitList = ytPlayer.getAvailableQualityLevels();
        let limit = limitList[0];

        if (ranks[target] > ranks[limit]) {
            target = limit;
        }

        if (!limitList.includes(target)) {
            for (let L of limitList) {
                if (ranks[L] < ranks[target]) {
                    target = L;
                    break;
                }
            }
        }

        let shouldPremium = settings.preferPremium && ytPlayer.getAvailableQualityData().some(q => q.quality == target && q.qualityLabel.includes("Premium") && q.isPlayable);

        debugLog(settings.preferPremium)
        debugLog(ytPlayer.getAvailableQualityData().some(q => q.quality == target && q.qualityLabel.includes("Premium") && q.isPlayable));

        // Premium quality does not have an direct API call so using emulated clicks instead
        if (shouldPremium) {
            debugLog("Premium quality available. Attempting to enable...")

            let resLabel = ytPlayer.getAvailableQualityData().find(q => q.quality == target && q.qualityLabel.includes("Premium")).qualityLabel;

            let settingsButton = doc.querySelector(".ytp-settings-button:not(#ScaleBtn)");
            unwrapElement(settingsButton).click();

            let qualityMenuButton = document.evaluate('.//*[contains(text(),"Quality")]/ancestor-or-self::*[@class="ytp-menuitem-label"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            unwrapElement(qualityMenuButton).click();

            let qualityButton = document.evaluate('.//*[contains(text(),"' + resLabel + '") and not(@class)]/ancestor::*[@class="ytp-menuitem"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            unwrapElement(qualityButton).click();

            debugLog("Resolution Set To: " + target + (shouldPremium ? " Premium" : ""));
            return;

        } else {
            debugLog("Premium quality not available.")
        }

        ytPlayer.setPlaybackQualityRange(target);
        debugLog("Resolution Set To: " + target);
    }

    // --------------------

    // Sets resolution when API is ready
    function setResOnReady(ytPlayer, resolutionList) {
        if (ytPlayer.getPlaybackQuality === undefined) {
            win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
        } else {
            setResolution(ytPlayer, resolutionList);
        }
    }

    // --------------------

    function setTheaterMode() {
        debugLog("Setting Theater Mode");

        if (win.location.href.indexOf("/watch") !== -1) {
            let pageManager = unwrapElement(doc.getElementsByClassName("ytd-page-manager")[0]);

            // directly modifies the player to use theater mode if not already
            if (pageManager && !pageManager.hasAttribute("theater")) {
                pageManager.getState().watch.isTheaterMode = true;
            }
        }
    }


    // --- MAIN -----------

    function main() {
        if (!settings.changeResolution && !settings.autoTheater) {
            return;
        }

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

        if (ytPlayerUnwrapped) {
            if (settings.autoTheater) {
                if (settings.allowCookies && doc.cookie.indexOf("wide=1") === -1) {
                    doc.cookie = "wide=1; domain=.youtube.com";
                }

                setTheaterMode(ytPlayerUnwrapped);
            }

            if (settings.changeResolution) {
                setResOnReady(ytPlayerUnwrapped, resolutions);
            }
        }

        win.addEventListener("loadstart", function(e) {
            if (!(e.target instanceof win.HTMLMediaElement)) {
                return;
            }

            ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
            ytPlayerUnwrapped = unwrapElement(ytPlayer);
            if (ytPlayerUnwrapped) {
                debugLog("Loaded new video");
                if (settings.changeResolution) {
                    setResOnReady(ytPlayerUnwrapped, resolutions);
                }
                if (settings.autoTheater) {
                    setTheaterMode(ytPlayerUnwrapped);
                }
            }
        }, true);

        // This will eventually be changed to use the "once" option, but I want to keep a large range of browser support. --adisib
        win.removeEventListener("yt-navigate-finish", main, true);
    }

    async function applySettings() {
        if (typeof GM != 'undefined' && GM.getValue && GM.setValue) {
            let settingsSaved = await GM.getValue("SettingsSaved");

            if (settings.overwriteStoredSettings || !settingsSaved) {
                Object.entries(settings).forEach(([k, v]) => GM.setValue(k, v));

                await GM.setValue("SettingsSaved", true);
            } else {
                await Promise.all(
                    Object.keys(settings).map(k => { let newval = GM.getValue(k); return newval.then(v => [k, v]); })
                ).then((c) => c.forEach(([nk, nv]) => {
                    if (settings[nk] !== null && nk !== "overwriteStoredSettings") {
                        settings[nk] = nv;
                    }
                }));
            }

            debugLog(Object.entries(settings).map(([k, v]) => k + " | " + v).join(", "));
        }
    }

    applySettings().then(() => {
        main();
        // YouTube doesn't load the page immediately in the new version so you can watch before waiting for page load
        // But we can only set the resolution until the page finishes loading
        win.addEventListener("yt-navigate-finish", main, true);
    });
})();