Greasy Fork

Greasy Fork is available in English.

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 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.1
// @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: false
    };

    // --------------------
    // --- 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);
    });
})();