Greasy Fork

Greasy Fork is available in English.

Simple YouTube Age Restriction Bypass

Watch age restricted videos on YouTube without login and without age verification :)

当前为 2021-08-03 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            Simple YouTube Age Restriction Bypass
// @description     Watch age restricted videos on YouTube without login and without age verification :)
// @description:de  Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen :)
// @description:fr  Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge :)
// @description:it  Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età :)
// @version         2.0.6
// @author          Zerody (https://github.com/zerodytrash)
// @namespace       https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/
// @supportURL      https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues
// @license         MIT
// @match           https://www.youtube.com/*
// @match           https://m.youtube.com/*
// @grant           none
// @run-at          document-start
// @compatible      chrome Chrome + Tampermonkey or Violentmonkey
// @compatible      firefox Firefox + Tampermonkey or Violentmonkey
// @compatible      opera Opera + Tampermonkey or Violentmonkey
// @compatible      edge Edge + Tampermonkey or Violentmonkey
// @compatible      safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==

(function () {

    var nativeParse = window.JSON.parse; // Backup the original parse function
    var nativeDefineProperty = getNativeDefineProperty(); // Backup the original defineProperty function to intercept setter & getter on the ytInitialPlayerResponse
    var nativeXmlHttpOpen = XMLHttpRequest.prototype.open;
    var wrappedPlayerResponse = null;
    var unlockablePlayerStates = ["AGE_VERIFICATION_REQUIRED", "AGE_CHECK_REQUIRED", "LOGIN_REQUIRED"];
    var playerResponsePropertyAliases = ["ytInitialPlayerResponse", "playerResponse"];
    var lastProxiedGoogleVideoUrlParams = null;
    var responseCache = {};

    // Youtube API config (Innertube). 
    // The actual values will be determined later from the global ytcfg variable => setInnertubeConfigFromYtcfg()
    var innertubeConfig = {
        INNERTUBE_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
        INNERTUBE_CLIENT_VERSION: "2.20210721.00.00",
        STS: 18834 // signatureTimestamp (relevant for the cipher functions)
    };

    // The following proxies are currently used as fallback if the innertube age-gate bypass doesn't work...
    // You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
    var accountProxyServerHost = "https://youtube-proxy.zerody.one";
    var videoProxyServerHost = "https://phx.4everproxy.com";

    // UI related stuff (notifications, ...)
    var enableUnlockNotification = true;
    var playerCreationObserver = null;
    var notificationElement = null;
    var notificationTimeout = null;

    // Just for compatibility: Backup original getter/setter for 'ytInitialPlayerResponse', defined by other extensions like AdBlock
    var initialPlayerResponseDescriptor = window.Object.getOwnPropertyDescriptor(window, "ytInitialPlayerResponse");
    var chainedSetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.set : null;
    var chainedGetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.get : null;

    // Just for compatibility: Intercept (re-)definitions on Youtube's initial player response property to chain setter/getter from other extensions by hijacking the Object.defineProperty function
    window.Object.defineProperty = function (obj, prop, descriptor) {
        if (obj === window && playerResponsePropertyAliases.includes(prop)) {
            console.info("Another extension tries to re-define '" + prop + "' (probably an AdBlock extension). Chain it...");

            if (descriptor && descriptor.set) chainedSetter = descriptor.set;
            if (descriptor && descriptor.get) chainedGetter = descriptor.get;
        } else {
            nativeDefineProperty(obj, prop, descriptor);
        }
    }

    // Re-define 'ytInitialPlayerResponse' to inspect and modify the initial player response as soon as the variable is set on page load
    nativeDefineProperty(window, "ytInitialPlayerResponse", {
        set: function (playerResponse) {

            // prevent recursive setter calls by ignoring unchanged data (this fixes a problem caused by brave browser shield)
            if (playerResponse === wrappedPlayerResponse) return;

            wrappedPlayerResponse = inspectJsonData(playerResponse);
            if (typeof chainedSetter === "function") chainedSetter(wrappedPlayerResponse);
        },
        get: function () {
            if (typeof chainedGetter === "function") try { return chainedGetter() } catch (err) { };
            return wrappedPlayerResponse || {};
        },
        configurable: true
    });

    // Intercept XMLHttpRequest.open to rewrite video url's (sometimes required)
    XMLHttpRequest.prototype.open = function () {

        if (arguments.length > 1 && typeof arguments[1] === "string" && arguments[1].indexOf("https://") === 0) {
            var method = arguments[0];
            var url = new URL(arguments[1]);
            var urlParams = new URLSearchParams(url.search);

            // if the account proxy was used to retieve the video info, the following applies:
            // some video files (mostly music videos) can only be accessed from IPs in the same country as the innertube api request (/youtubei/v1/player) was made.
            // to get around this, the googlevideo url will be replaced with a web-proxy url in the same country (US).
            // this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url...

            function isGoogleVideo() {
                return method === "GET" && url.host.indexOf(".googlevideo.com") > 0;
            }

            function hasGcrFlag() {
                return urlParams.get("gcr") !== null;
            }

            function isUnlockedByAccountProxy() {
                return urlParams.get("id") !== null && lastProxiedGoogleVideoUrlParams && urlParams.get("id") === lastProxiedGoogleVideoUrlParams.get("id");
            }

            if (videoProxyServerHost && isGoogleVideo() && hasGcrFlag() && isUnlockedByAccountProxy()) {

                // rewrite request url
                arguments[1] = videoProxyServerHost + "/direct/" + btoa(arguments[1]);

                // solve CORS errors by preventing youtube from enabling the "withCredentials" option (not required for the proxy)
                nativeDefineProperty(this, "withCredentials", {
                    set: function () { },
                    get: function () {
                        return false;
                    }
                });
            }

        }

        return nativeXmlHttpOpen.apply(this, arguments);
    }

    // Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
    window.JSON.parse = function (text, reviver) {
        return inspectJsonData(nativeParse(text, reviver));
    }

    function inspectJsonData(parsedData) {

        // If youtube does JSON.parse(null) or similar weird things
        if (typeof parsedData !== "object" || parsedData === null) return parsedData;

        try {
            // Unlock #1: Array based in "&pbj=1" AJAX response on any navigation (does not seem to be used anymore)
            if (Array.isArray(parsedData)) {

                var playerResponseArrayItem = parsedData.find(e => typeof e.playerResponse === "object");
                var playerResponse = playerResponseArrayItem?.playerResponse;

                if (playerResponse && isAgeRestricted(playerResponse.playabilityStatus)) {
                    playerResponseArrayItem.playerResponse = unlockPlayerResponse(playerResponse);
                }
            }

            // Hide unlock notification on navigation (if still visible from the last unlock)
            if (parsedData.playerResponse || parsedData.playabilityStatus) hidePlayerNotification();

            // Unlock #2: Another JSON-Object containing the 'playerResponse'
            if (parsedData.playerResponse?.playabilityStatus && parsedData.playerResponse?.videoDetails && isAgeRestricted(parsedData.playerResponse.playabilityStatus)) {
                parsedData.playerResponse = unlockPlayerResponse(parsedData.playerResponse);
            }

            // Unlock #3: Initial page data structure and response from '/youtubei/v1/player' endpoint
            if (parsedData.playabilityStatus && parsedData.videoDetails && isAgeRestricted(parsedData.playabilityStatus)) {
                parsedData = unlockPlayerResponse(parsedData);
            }

        } catch (err) {
            console.error("Simple-YouTube-Age-Restriction-Bypass-Error:", err, "You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues");
        }

        return parsedData;
    }

    function isAgeRestricted(playabilityStatus) {
        if (!playabilityStatus || !playabilityStatus.status) return false;
        return typeof playabilityStatus.desktopLegacyAgeGateReason !== "undefined" || unlockablePlayerStates.includes(playabilityStatus.status);
    }

    function unlockPlayerResponse(playerResponse) {
        var videoId = playerResponse.videoDetails.videoId;
        var reason = playerResponse.playabilityStatus?.status;

        var unlockedPayerResponse = getUnlockedPlayerResponse(videoId, reason);

        // account proxy error?
        if (unlockedPayerResponse.errorMessage) {
            showPlayerNotification("#7b1e1e", "Unable to unlock this video :( Please look into the developer console for more details. (ProxyError)", 10);
            throw new Error(`Unlock Failed, errorMessage:${unlockedPayerResponse.errorMessage}; innertubeApiKey:${innertubeConfig.INNERTUBE_API_KEY}; innertubeClientVersion:${innertubeConfig.INNERTUBE_CLIENT_VERSION}`);
        }


        // check if the unlocked response isn't playable
        if (unlockedPayerResponse.playabilityStatus?.status !== "OK") {
            showPlayerNotification("#7b1e1e", `Unable to unlock this video :( Please look into the developer console for more details. (playabilityStatus: ${unlockedPayerResponse.playabilityStatus?.status})`, 10);
            throw new Error(`Unlock Failed, playabilityStatus:${unlockedPayerResponse.playabilityStatus?.status}; innertubeApiKey:${innertubeConfig.INNERTUBE_API_KEY}; innertubeClientVersion:${innertubeConfig.INNERTUBE_CLIENT_VERSION}`);
        }

        // if the video info was retrieved via proxy, store the url params from the url- or signatureCipher-attribute to detect later if the requested video files are from this unlock.
        // => see isUnlockedByAccountProxy()
        if (unlockedPayerResponse.proxied && unlockedPayerResponse.streamingData?.adaptiveFormats) {
            var videoUrl = unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.url)?.url;
            var cipherText = unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.signatureCipher)?.signatureCipher;

            if (cipherText) videoUrl = new URLSearchParams(cipherText).get("url");

            lastProxiedGoogleVideoUrlParams = videoUrl ? new URLSearchParams(new URL(videoUrl).search) : null;
        }

        showPlayerNotification("#005c04", "Age-restricted video successfully unlocked!", 4);

        return unlockedPayerResponse;
    }

    function getUnlockedPlayerResponse(videoId, reason) {

        // Check if response is cached
        if (responseCache.videoId === videoId) return responseCache.content;

        // to avoid version conflicts between client and server response, the current YouTube version config will be determined
        setInnertubeConfigFromYtcfg();

        var playerResponse = null;

        // Strategy 1: Retrieve the video info by using a age-gate bypass for the innertube api
        // Source: https://github.com/yt-dlp/yt-dlp/issues/574#issuecomment-887171136
        function useInnertubeEmbed() {
            console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Unlock Method #1 (Innertube Embed)");
            var payload = getInnertubeEmbedPlayerPayload(videoId);
            var xmlhttp = new XMLHttpRequest();
            xmlhttp.open("POST", `/youtubei/v1/player?key=${innertubeConfig.INNERTUBE_API_KEY}`, false); // Synchronous!!!
            xmlhttp.send(JSON.stringify(payload));
            playerResponse = nativeParse(xmlhttp.responseText);
        }

        // Strategy 2: Retrieve the video info from an account proxy server.
        // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
        function useProxy() {
            console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Unlock Method #2 (Account Proxy)");
            var xmlhttp = new XMLHttpRequest();
            xmlhttp.open("GET", accountProxyServerHost + `/getPlayer?videoId=${encodeURIComponent(videoId)}&reason=${encodeURIComponent(reason)}&clientVersion=${innertubeConfig.INNERTUBE_CLIENT_VERSION}&signatureTimestamp=${innertubeConfig.STS}`, false); // Synchronous!!!
            xmlhttp.send(null);
            playerResponse = nativeParse(xmlhttp.responseText);
            playerResponse.proxied = true;
        }

        if (playerResponse?.playabilityStatus?.status !== "OK") useInnertubeEmbed();
        if (playerResponse?.playabilityStatus?.status !== "OK") useProxy();

        // Cache response for 10 seconds
        responseCache = { videoId: videoId, content: playerResponse };
        setTimeout(function () { responseCache = {} }, 10000);

        return playerResponse;
    }

    function getInnertubeEmbedPlayerPayload(videoId) {
        return {
            "context": {
                "client": {
                    "clientName": "WEB",
                    "clientVersion": innertubeConfig.INNERTUBE_CLIENT_VERSION,
                    "clientScreen": "EMBED"
                },
                "thirdParty": {
                    "embedUrl": "https://www.youtube.com/"
                }
            },
            "playbackContext": {
                "contentPlaybackContext": {
                    "signatureTimestamp": innertubeConfig.STS
                }
            },
            "videoId": videoId
        }
    }

    function setInnertubeConfigFromYtcfg() {
        if (!window.ytcfg) {
            console.warn("Simple-YouTube-Age-Restriction-Bypass: Unable to retrieve global YouTube configuration (window.ytcfg). Using old values...");
            return;
        }

        for (const key in innertubeConfig) {
            var value = window.ytcfg.data_?.[key] ?? window.ytcfg.get?.(key);
            if (value) {
                innertubeConfig[key] = value;
            } else {
                console.warn(`Simple-YouTube-Age-Restriction-Bypass: Unable to retrieve global YouTube configuration variable '${key}'. Using old value...`);
            }
        }
    }

    function showPlayerNotification(color, message, displayDuration) {
        if (!enableUnlockNotification) return;
        if (typeof MutationObserver !== "function") return;

        try {
            // clear existing notifications
            disconnectPlayerCreationObserver();
            hidePlayerNotification();

            function getPlayerElement() {
                return document.querySelector("#primary > #primary-inner > #player") || document.querySelector("#player-container-id > #player");
            }

            function createNotifiction() {
                var playerElement = getPlayerElement();
                if (!playerElement) return;

                // first, remove existing notification
                hidePlayerNotification();

                // create new notification
                notificationElement = document.createElement("div");
                notificationElement.innerHTML = message;
                notificationElement.style = `width: 100%; text-align: center; background-color: ${color}; color: #ffffff; padding: 2px 0px 2px; font-size: 1.1em;`;
                notificationElement.id = "bypass-notification";

                // append below the player 
                playerElement.parentNode.insertBefore(notificationElement, playerElement.nextSibling);

                if (notificationTimeout) {
                    clearTimeout(notificationTimeout);
                    notificationTimeout = null;
                }

                notificationTimeout = setTimeout(hidePlayerNotification, displayDuration * 1000);
            }

            function disconnectPlayerCreationObserver() {
                if (playerCreationObserver) {
                    playerCreationObserver.disconnect();
                    playerCreationObserver = null;
                }
            }

            // Player already exists in DOM?
            if (getPlayerElement() !== null) {
                createNotifiction();
                return;
            }

            // waiting for creation of the player element...
            playerCreationObserver = new MutationObserver(function (mutations) {
                if (getPlayerElement() !== null) {
                    disconnectPlayerCreationObserver();
                    createNotifiction();
                }
            });

            playerCreationObserver.observe(document.body, { childList: true });

        } catch (err) { }
    }

    function hidePlayerNotification() {
        if (playerCreationObserver) {
            playerCreationObserver.disconnect();
            playerCreationObserver = null;
        }

        if (notificationElement) {
            notificationElement.remove();
            notificationElement = null;
        }
    }

    // Some extensions like AdBlock override the Object.defineProperty function to prevent a re-definition of the 'ytInitialPlayerResponse' variable by YouTube.
    // But we need to define a custom descriptor to that variable to intercept his value. This behavior causes a race condition depending on the execution order with this script :(
    // This function tries to restore the native Object.defineProperty function...
    function getNativeDefineProperty() {

        // Check if the Object.defineProperty function is native (original)
        if (window.Object.defineProperty && window.Object.defineProperty.toString().indexOf("[native code]") > -1) {
            return window.Object.defineProperty;
        }

        // if the Object.defineProperty function is already overidden, try to restore the native function from another window...
        try {
            if (!document.body) document.body = document.createElement("body");

            var tempFrame = document.createElement("iframe");
            tempFrame.style.display = "none";

            document.body.insertAdjacentElement("beforeend", tempFrame);
            var nativeDefineProperty = tempFrame.contentWindow.Object.defineProperty;
            tempFrame.remove();

            console.info("Simple-YouTube-Age-Restriction-Bypass: Overidden Object.defineProperty function successfully restored!");

            return nativeDefineProperty;
        } catch (err) {
            console.warn("Simple-YouTube-Age-Restriction-Bypass: Unable to restore the original Object.defineProperty function", err);
            return window.Object.defineProperty;
        }
    }

})();