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

您需要先安装一个扩展,例如 篡改猴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.1.3
// @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 + Greasemonkey or Tampermonkey or Violentmonkey
// @compatible      opera Opera + Tampermonkey or Violentmonkey
// @compatible      edge Edge + Tampermonkey or Violentmonkey
// @compatible      safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==

const initUnlocker = () => {
    const UNLOCKABLE_PLAYER_STATES = ["AGE_VERIFICATION_REQUIRED", "AGE_CHECK_REQUIRED", "LOGIN_REQUIRED"];
    const PLAYER_RESPONSE_ALIASES = ["ytInitialPlayerResponse", "playerResponse"];

    // YouTube API config (Innertube).
    // The actual values will be determined later from the global ytcfg variable => setInnertubeConfigFromYtcfg()
    const INNERTUBE_CONFIG = {
        INNERTUBE_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
        INNERTUBE_CLIENT_NAME: "WEB",
        INNERTUBE_CLIENT_VERSION: "2.20210721.00.00",
        INNERTUBE_CONTEXT: {},
        STS: 18834, // signatureTimestamp (relevant for the cipher functions)
        LOGGED_IN: false,
    };

    // 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
    const ACCOUNT_PROXY_SERVER_HOST = "https://youtube-proxy.zerody.one";
    const VIDEO_PROXY_SERVER_HOST = "https://phx.4everproxy.com";

    const ENABLE_UNLOCK_NOTIFICATION = true;

    const nativeParse = window.JSON.parse; // Backup the original parse function
    const nativeDefineProperty = getNativeDefineProperty(); // Backup the original defineProperty function to intercept setter & getter on the ytInitialPlayerResponse
    const nativeXmlHttpOpen = XMLHttpRequest.prototype.open;

    // Just for compatibility: Backup original getter/setter for 'ytInitialPlayerResponse', defined by other extensions like AdBlock
    let { get: chainedPlayerGetter, set: chainedPlayerSetter } = Object.getOwnPropertyDescriptor(window, "ytInitialPlayerResponse") || {};

    let wrappedPlayerResponse;
    let wrappedNextResponse;
    let lastProxiedGoogleVideoUrlParams;
    let responseCache = {};

    // UI-related stuff (notifications, ...)
    let playerCreationObserver;
    let notificationElement;
    let notificationTimeout;

    // 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
    Object.defineProperty = (obj, prop, descriptor) => {
        if (obj === window && PLAYER_RESPONSE_ALIASES.includes(prop)) {
            console.info("Another extension tries to redefine '" + prop + "' (probably an AdBlock extension). Chain it...");

            if (descriptor?.set) chainedPlayerSetter = descriptor.set;
            if (descriptor?.get) chainedPlayerGetter = descriptor.get;
        } else {
            nativeDefineProperty(obj, prop, descriptor);
        }
    };

    // Redefine 'ytInitialPlayerResponse' to inspect and modify the initial player response as soon as the variable is set on page load
    nativeDefineProperty(window, "ytInitialPlayerResponse", {
        set: (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 chainedPlayerSetter === "function") chainedPlayerSetter(wrappedPlayerResponse);
        },
        get: () => {
            if (typeof chainedPlayerGetter === "function") try { return chainedPlayerGetter() } catch (err) { };
            return wrappedPlayerResponse || {};
        },
        configurable: true
    });

    // Also redefine 'ytInitialData' for the initial next/sidebar response
    nativeDefineProperty(window, "ytInitialData", {
        set: (nextResponse) => { wrappedNextResponse = inspectJsonData(nextResponse); },
        get: () => wrappedNextResponse,
        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) {
            const method = arguments[0];
            const url = new URL(arguments[1]);
            const 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 && urlParams.get("id") === lastProxiedGoogleVideoUrlParams?.get("id");
            }

            if (VIDEO_PROXY_SERVER_HOST && isGoogleVideo() && hasGcrFlag() && isUnlockedByAccountProxy()) {
                // rewrite request URL
                arguments[1] = VIDEO_PROXY_SERVER_HOST + "/direct/" + btoa(arguments[1]);

                // solve CORS errors by preventing YouTube from enabling the "withCredentials" option (not required for the proxy)
                nativeDefineProperty(this, "withCredentials", {
                    set: () => { },
                    get: () => 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 = (text, reviver) => 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)) {
                const { playerResponse } = parsedData.find(e => typeof e.playerResponse === "object") || {};

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

                    const { response: nextResponse } = parsedData.find(e => typeof e.response === "object") || {};

                    if (isWatchNextObject(nextResponse) && !isLoggedIn() && isWatchNextSidebarEmpty(nextResponse.contents)) {
                        nextResponseArrayItem.response = unlockNextResponse(nextResponse);
                    }
                }
            }

            // 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' (seems to be used by m.youtube.com with &pbj=1)
            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 the '/youtubei/v1/player' endpoint
            if (parsedData.playabilityStatus && parsedData.videoDetails && isAgeRestricted(parsedData.playabilityStatus)) {
                parsedData = unlockPlayerResponse(parsedData);
            }

            // Equivelant of unlock #2 for sidebar/next response
            if (isWatchNextObject(parsedData.response) && !isLoggedIn() && isWatchNextSidebarEmpty(parsedData.response.contents)) {
                parsedData.response = unlockNextResponse(parsedData.response);
            }

            // Equivelant of unlock #3 for sidebar/next response
            if (isWatchNextObject(parsedData) && !isLoggedIn() && isWatchNextSidebarEmpty(parsedData.contents)) {
                parsedData = unlockNextResponse(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?.status) return false;
        return !!playabilityStatus.desktopLegacyAgeGateReason || UNLOCKABLE_PLAYER_STATES.includes(playabilityStatus.status);
    }

    function isWatchNextObject(parsedData) {
        if (!parsedData?.contents || !parsedData?.currentVideoEndpoint?.watchEndpoint?.videoId) return false;
        return !!parsedData.contents.twoColumnWatchNextResults || !!parsedData.contents.singleColumnWatchNextResults;
    }

    function isWatchNextSidebarEmpty(contents) {
        const secondaryResults = contents.twoColumnWatchNextResults?.secondaryResults?.secondaryResults;
        if (secondaryResults?.results) return false;

        // MWEB response layout
        const singleColumnWatchNextContents = contents.singleColumnWatchNextResults?.results?.results?.contents;
        if (!singleColumnWatchNextContents) return true;

        const { itemSectionRenderer } = singleColumnWatchNextContents.find(e => e.itemSectionRenderer?.targetId === "watch-next-feed") || {};

        return !!itemSectionRenderer;
    }

    function isLoggedIn() {
        setInnertubeConfigFromYtcfg();
        return INNERTUBE_CONFIG.LOGGED_IN;
    }

    function unlockPlayerResponse(playerResponse) {
        const videoId = playerResponse.videoDetails.videoId;
        const reason = playerResponse.playabilityStatus?.status;
        const 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:${INNERTUBE_CONFIG.INNERTUBE_API_KEY}; innertubeClientName:${INNERTUBE_CONFIG.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${INNERTUBE_CONFIG.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:${INNERTUBE_CONFIG.INNERTUBE_API_KEY}; innertubeClientName:${INNERTUBE_CONFIG.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${INNERTUBE_CONFIG.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) {
            const cipherText = unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.signatureCipher)?.signatureCipher;
            const videoUrl = cipherText ? new URLSearchParams(cipherText).get("url") : unlockedPayerResponse.streamingData.adaptiveFormats.find(x => x.url)?.url;

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

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

        return unlockedPayerResponse;
    }

    function unlockNextResponse(nextResponse) {
        const { videoId, playlistId, index: playlistIndex } = nextResponse.currentVideoEndpoint.watchEndpoint;
        const unlockedNextResponse = getUnlockedNextResponse(videoId, playlistId, playlistIndex);

        // check if the unlocked response's sidebar is still empty
        if (isWatchNextSidebarEmpty(unlockedNextResponse.contents)) {
            throw new Error(`Sidebar Unlock Failed, innertubeApiKey:${INNERTUBE_CONFIG.INNERTUBE_API_KEY}; innertubeClientName:${INNERTUBE_CONFIG.INNERTUBE_CLIENT_NAME}; innertubeClientVersion:${INNERTUBE_CONFIG.INNERTUBE_CLIENT_VERSION}`);
        }

        // Transfer WatchNextResults to original response
        if (nextResponse.contents?.twoColumnWatchNextResults?.secondaryResults) {
            nextResponse.contents.twoColumnWatchNextResults.secondaryResults = unlockedNextResponse?.contents?.twoColumnWatchNextResults?.secondaryResults;
        }

        // Transfer mobile (MWEB) WatchNextResults to original response
        if (nextResponse.contents?.singleColumnWatchNextResults?.results?.results?.contents) {
            const unlockedWatchNextFeed = unlockedNextResponse?.contents?.singleColumnWatchNextResults?.results?.results?.contents
                ?.find(x => x.itemSectionRenderer?.targetId === "watch-next-feed");
            if (unlockedWatchNextFeed) nextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(unlockedWatchNextFeed);
        }

        // Transfer video description to original response
        const originalVideoSecondaryInfoRenderer = nextResponse.contents?.twoColumnWatchNextResults?.results?.results?.contents
            ?.find(x => x.videoSecondaryInfoRenderer)?.videoSecondaryInfoRenderer;
        const unlockedVideoSecondaryInfoRenderer = unlockedNextResponse.contents?.twoColumnWatchNextResults?.results?.results?.contents
            ?.find(x => x.videoSecondaryInfoRenderer)?.videoSecondaryInfoRenderer;

        if (originalVideoSecondaryInfoRenderer && unlockedVideoSecondaryInfoRenderer?.description)
            originalVideoSecondaryInfoRenderer.description = unlockedVideoSecondaryInfoRenderer.description;

        // Transfer mobile (MWEB) video description to original response
        const originalStructuredDescriptionContentRenderer = nextResponse.engagementPanels
            ?.find(x => x.engagementPanelSectionListRenderer)?.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer?.items
            ?.find(x => x.expandableVideoDescriptionBodyRenderer);
        const unlockedStructuredDescriptionContentRenderer = unlockedNextResponse.engagementPanels
            ?.find(x => x.engagementPanelSectionListRenderer)?.engagementPanelSectionListRenderer?.content?.structuredDescriptionContentRenderer?.items
            ?.find(x => x.expandableVideoDescriptionBodyRenderer);

        if (originalStructuredDescriptionContentRenderer && unlockedStructuredDescriptionContentRenderer?.expandableVideoDescriptionBodyRenderer)
            originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer = unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer;

        return nextResponse;
    }

    function getUnlockedPlayerResponse(videoId, reason) {
        // Check if response is cached
        if (responseCache.videoId === videoId) return responseCache.playerResponse;

        setInnertubeConfigFromYtcfg();

        let playerResponse = useInnertubeEmbed();

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

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

        return playerResponse;

        // 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)");
            const payload = getInnertubeEmbedPayload(videoId);
            const xmlhttp = new XMLHttpRequest();
            xmlhttp.open("POST", `/youtubei/v1/player?key=${INNERTUBE_CONFIG.INNERTUBE_API_KEY}`, false); // Synchronous!!!
            xmlhttp.send(JSON.stringify(payload));
            return 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)");
            const xmlhttp = new XMLHttpRequest();
            xmlhttp.open("GET", ACCOUNT_PROXY_SERVER_HOST + `/getPlayer?videoId=${encodeURIComponent(videoId)}&reason=${encodeURIComponent(reason)}&clientName=${INNERTUBE_CONFIG.INNERTUBE_CLIENT_NAME}&clientVersion=${INNERTUBE_CONFIG.INNERTUBE_CLIENT_VERSION}&signatureTimestamp=${INNERTUBE_CONFIG.STS}`, false); // Synchronous!!!
            xmlhttp.send(null);
            const playerResponse = nativeParse(xmlhttp.responseText);
            playerResponse.proxied = true;
            return playerResponse;
        }
    }

    function getUnlockedNextResponse(videoId, playlistId, playlistIndex) {
        setInnertubeConfigFromYtcfg();

        // Retrieve the video info by using a age-gate bypass for the innertube API
        // Source: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/16#issuecomment-889232425
        console.info("Simple-YouTube-Age-Restriction-Bypass: Trying Sidebar Unlock Method (Innertube Embed)");
        const payload = getInnertubeEmbedPayload(videoId, playlistId, playlistIndex);
        const xmlhttp = new XMLHttpRequest();
        xmlhttp.open("POST", `/youtubei/v1/next?key=${INNERTUBE_CONFIG.INNERTUBE_API_KEY}`, false); // Synchronous!!!
        xmlhttp.send(JSON.stringify(payload));
        return nativeParse(xmlhttp.responseText);
    }

    function getInnertubeEmbedPayload(videoId, playlistId, playlistIndex) {
        const data = {
            context: {
                client: {
                    clientName: INNERTUBE_CONFIG.INNERTUBE_CLIENT_NAME.replace('_EMBEDDED_PLAYER', ''),
                    clientVersion: INNERTUBE_CONFIG.INNERTUBE_CLIENT_VERSION,
                    clientScreen: "EMBED",
                },
                thirdParty: {
                    embedUrl: "https://www.youtube.com/",
                },
            },
            playbackContext: {
                contentPlaybackContext: {
                    signatureTimestamp: INNERTUBE_CONFIG.STS,
                },
            },
            videoId,
            playlistId,
            playlistIndex,
        };

        // Append client info from INNERTUBE_CONTEXT
        if (typeof INNERTUBE_CONFIG.INNERTUBE_CONTEXT?.client === "object") {
            data.context.client = { ...data.context.client, ...INNERTUBE_CONFIG.INNERTUBE_CONTEXT.client };
        }

        return data;
    }

    // to avoid version conflicts between client and server response, the current YouTube version config will be determined
    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 INNERTUBE_CONFIG) {
            const value = window.ytcfg.data_?.[key] ?? window.ytcfg.get?.(key);
            if (value !== undefined && value !== null) {
                INNERTUBE_CONFIG[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 (!ENABLE_UNLOCK_NOTIFICATION) return;

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

        // Does the player already exist in the DOM?
        if (getPlayerElement()) {
            createNotification();
            return;
        }

        // waiting for creation of the player element...
        playerCreationObserver = new MutationObserver(() => {
            if (getPlayerElement()) {
                disconnectPlayerCreationObserver();
                createNotification();
            }
        });

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

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

        function createNotification() {
            const playerElement = getPlayerElement();
            if (!playerElement) return;

            // first, remove existing notification
            hidePlayerNotification();

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

            // append below the player
            playerElement.nextSibling?.before(notificationElement);

            if (notificationTimeout) clearTimeout(notificationTimeout);

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

        function disconnectPlayerCreationObserver() {
            playerCreationObserver?.disconnect();
        }
    }

    function hidePlayerNotification() {
        playerCreationObserver?.disconnect();
        notificationElement?.remove();
    }

    // Some extensions like AdBlock override the Object.defineProperty function to prevent a redefinition of the 'ytInitialPlayerResponse' variable by YouTube.
    // But we need to define a custom descriptor to that variable to intercept its 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 (Object.defineProperty?.toString().indexOf("[native code]") > -1) {
            return Object.defineProperty;
        }

        // if the Object.defineProperty function is already overidden, try to restore the native function from another window...
        const tempFrame = createElement("iframe", { style: `display: none;` });
        document.documentElement.append(tempFrame);

        const nativeDefineProperty = tempFrame?.contentWindow?.Object?.defineProperty;

        tempFrame.remove();

        if (nativeDefineProperty) {
            console.info("Simple-YouTube-Age-Restriction-Bypass: Overidden Object.defineProperty function successfully restored!");
            return nativeDefineProperty;
        } else {
            console.warn("Simple-YouTube-Age-Restriction-Bypass: Unable to restore the original Object.defineProperty function");
            return Object.defineProperty;
        }
    }

    function createElement(tagName, options) {
        const node = document.createElement(tagName);
        options && Object.assign(node, options);
        return node;
    }
};

// Just a trick to get around the sandbox restrictions in Firefox / Greasemonkey
// Greasemonkey => Inject code into the main window
// Tampermonkey & Violentmonkey => Execute code directly
if (typeof GM_info === "object" && GM_info.scriptHandler === "Greasemonkey") {
    window.eval("(" + initUnlocker.toString() + ")();");
} else {
    initUnlocker();
}