Simple YouTube Age Restriction Bypass

Watch age restricted videos on YouTube without login and without age verification 😎

// ==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à 😎
// @icon            https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/v2.5.4/src/extension/icon/icon_64.png
// @version         2.5.10
// @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://www.youtube-nocookie.com/*
// @match           https://m.youtube.com/*
// @match           https://music.youtube.com/*
// @grant           none
// @run-at          document-start
// @compatible      chrome
// @compatible      firefox
// @compatible      opera
// @compatible      edge
// @compatible      safari
// ==/UserScript==

/*
    This is a transpiled version to achieve a clean code base and better browser compatibility.
    You can find the nicely readable source code at https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
*/

(function iife(ranOnce) {
    // Trick to get around the sandbox restrictions in Greasemonkey (Firefox)
    // Inject code into the main window if criteria match
    if (this !== window && !ranOnce) {
        window.eval('(' + iife.toString() + ')(true);');
        return;
    }

    // Script configuration variables
    const UNLOCKABLE_PLAYABILITY_STATUSES = ['AGE_VERIFICATION_REQUIRED', 'AGE_CHECK_REQUIRED', 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
    const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];

    // These are the proxy servers that are sometimes required to unlock videos with age restrictions.
    // You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
    // To learn what information is transferred, please read: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#privacy
    const ACCOUNT_PROXY_SERVER_HOST = 'https://youtube-proxy.zerody.one';
    const VIDEO_PROXY_SERVER_HOST = 'https://ny.4everproxy.com';

    // User needs to confirm the unlock process on embedded player?
    let ENABLE_UNLOCK_CONFIRMATION_EMBED = true;

    // Show notification?
    let ENABLE_UNLOCK_NOTIFICATION = true;

    // Disable content warnings?
    let SKIP_CONTENT_WARNINGS = true;

    // Some Innertube bypass methods require the following authentication headers of the currently logged in user.
    const GOOGLE_AUTH_HEADER_NAMES = ['Authorization', 'X-Goog-AuthUser', 'X-Origin'];

    /**
     * The SQP parameter length is different for blurred thumbnails.
     * They contain much less information, than normal thumbnails.
     * The thumbnail SQPs tend to have a long and a short version.
     */
    const BLURRED_THUMBNAIL_SQP_LENGTHS = [
        32, // Mobile (SHORT)
        48, // Desktop Playlist (SHORT)
        56, // Desktop (SHORT)
        68, // Mobile (LONG)
        72, // Mobile Shorts
        84, // Desktop Playlist (LONG)
        88, // Desktop (LONG)
    ];

    // small hack to prevent tree shaking on these exports
    var Config = window[Symbol()] = {
        UNLOCKABLE_PLAYABILITY_STATUSES,
        VALID_PLAYABILITY_STATUSES,
        ACCOUNT_PROXY_SERVER_HOST,
        VIDEO_PROXY_SERVER_HOST,
        ENABLE_UNLOCK_CONFIRMATION_EMBED,
        ENABLE_UNLOCK_NOTIFICATION,
        SKIP_CONTENT_WARNINGS,
        GOOGLE_AUTH_HEADER_NAMES,
        BLURRED_THUMBNAIL_SQP_LENGTHS,
    };

    function isGoogleVideoUrl(url) {
        return url.host.includes('.googlevideo.com');
    }

    function isGoogleVideoUnlockRequired(googleVideoUrl, lastProxiedGoogleVideoId) {
        const urlParams = new URLSearchParams(googleVideoUrl.search);
        const hasGcrFlag = urlParams.get('gcr');
        const wasUnlockedByAccountProxy = urlParams.get('id') === lastProxiedGoogleVideoId;

        return hasGcrFlag && wasUnlockedByAccountProxy;
    }

    const nativeJSONParse = window.JSON.parse;
    const nativeXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;

    const isDesktop = window.location.host !== 'm.youtube.com';
    const isMusic = window.location.host === 'music.youtube.com';
    const isEmbed = window.location.pathname.indexOf('/embed/') === 0;
    const isConfirmed = window.location.search.includes('unlock_confirmed');

    class Deferred {
        constructor() {
            return Object.assign(
                new Promise((resolve, reject) => {
                    this.resolve = resolve;
                    this.reject = reject;
                }),
                this,
            );
        }
    }

    // WORKAROUND: TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
    if (window.trustedTypes && trustedTypes.createPolicy) {
        if (!trustedTypes.defaultPolicy) {
            const passThroughFn = (x) => x;
            trustedTypes.createPolicy('default', {
                createHTML: passThroughFn,
                createScriptURL: passThroughFn,
                createScript: passThroughFn,
            });
        }
    }

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

    function isObject(obj) {
        return obj !== null && typeof obj === 'object';
    }

    function findNestedObjectsByAttributeNames(object, attributeNames) {
        var results = [];

        // Does the current object match the attribute conditions?
        if (attributeNames.every((key) => typeof object[key] !== 'undefined')) {
            results.push(object);
        }

        // Diggin' deeper for each nested object (recursive)
        Object.keys(object).forEach((key) => {
            if (object[key] && typeof object[key] === 'object') {
                results.push(...findNestedObjectsByAttributeNames(object[key], attributeNames));
            }
        });

        return results;
    }

    function pageLoaded() {
        if (document.readyState === 'complete') return Promise.resolve();

        const deferred = new Deferred();

        window.addEventListener('load', deferred.resolve, { once: true });

        return deferred;
    }

    function createDeepCopy(obj) {
        return nativeJSONParse(JSON.stringify(obj));
    }

    function getYtcfgValue(name) {
        var _window$ytcfg;
        return (_window$ytcfg = window.ytcfg) === null || _window$ytcfg === void 0 ? void 0 : _window$ytcfg.get(name);
    }

    function getSignatureTimestamp() {
        return (
            getYtcfgValue('STS')
            || ((_document$querySelect) => {
                // STS is missing on embedded player. Retrieve from player base script as fallback...
                const playerBaseJsPath = (_document$querySelect = document.querySelector('script[src*="/base.js"]')) === null || _document$querySelect === void 0
                    ? void 0
                    : _document$querySelect.src;

                if (!playerBaseJsPath) return;

                const xmlhttp = new XMLHttpRequest();
                xmlhttp.open('GET', playerBaseJsPath, false);
                xmlhttp.send(null);

                return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
            })()
        );
    }

    function isUserLoggedIn() {
        // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
        if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
        if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
        if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;

        return false;
    }

    function getCurrentVideoStartTime(currentVideoId) {
        // Check if the URL corresponds to the requested video
        // This is not the case when the player gets preloaded for the next video in a playlist.
        if (window.location.href.includes(currentVideoId)) {
            var _ref;
            // "t"-param on youtu.be urls
            // "start"-param on embed player
            // "time_continue" when clicking "watch on youtube" on embedded player
            const urlParams = new URLSearchParams(window.location.search);
            const startTimeString = (_ref = urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue')) === null || _ref === void 0
                ? void 0
                : _ref.replace('s', '');

            if (startTimeString && !isNaN(startTimeString)) {
                return parseInt(startTimeString);
            }
        }

        return 0;
    }

    function setUrlParams(params) {
        const urlParams = new URLSearchParams(window.location.search);
        for (const paramName in params) {
            urlParams.set(paramName, params[paramName]);
        }
        window.location.search = urlParams;
    }

    function waitForElement(elementSelector, timeout) {
        const deferred = new Deferred();

        const checkDomInterval = setInterval(() => {
            const elem = document.querySelector(elementSelector);
            if (elem) {
                clearInterval(checkDomInterval);
                deferred.resolve(elem);
            }
        }, 100);

        {
            setTimeout(() => {
                clearInterval(checkDomInterval);
                deferred.reject();
            }, timeout);
        }

        return deferred;
    }

    function isWatchNextObject(parsedData) {
        var _parsedData$currentVi;
        if (
            !(parsedData !== null && parsedData !== void 0 && parsedData.contents)
            || !(parsedData !== null && parsedData !== void 0 && (_parsedData$currentVi = parsedData.currentVideoEndpoint) !== null && _parsedData$currentVi !== void 0
                && (_parsedData$currentVi = _parsedData$currentVi.watchEndpoint) !== null && _parsedData$currentVi !== void 0 && _parsedData$currentVi.videoId)
        ) return false;
        return !!parsedData.contents.twoColumnWatchNextResults || !!parsedData.contents.singleColumnWatchNextResults;
    }

    function isWatchNextSidebarEmpty(parsedData) {
        var _parsedData$contents2, _content$find;
        if (isDesktop) {
            var _parsedData$contents;
            // WEB response layout
            const result = (_parsedData$contents = parsedData.contents) === null || _parsedData$contents === void 0
                    || (_parsedData$contents = _parsedData$contents.twoColumnWatchNextResults) === null || _parsedData$contents === void 0
                    || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
                    || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
                ? void 0
                : _parsedData$contents.results;
            return !result;
        }

        // MWEB response layout
        const content = (_parsedData$contents2 = parsedData.contents) === null || _parsedData$contents2 === void 0
                || (_parsedData$contents2 = _parsedData$contents2.singleColumnWatchNextResults) === null || _parsedData$contents2 === void 0
                || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
                || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
            ? void 0
            : _parsedData$contents2.contents;
        const result = content === null || content === void 0 || (_content$find = content.find((e) => {
                    var _e$itemSectionRendere;
                    return ((_e$itemSectionRendere = e.itemSectionRenderer) === null || _e$itemSectionRendere === void 0 ? void 0 : _e$itemSectionRendere.targetId)
                        === 'watch-next-feed';
                })) === null
                || _content$find === void 0
            ? void 0
            : _content$find.itemSectionRenderer;
        return typeof result !== 'object';
    }

    function isPlayerObject(parsedData) {
        return (parsedData === null || parsedData === void 0 ? void 0 : parsedData.videoDetails)
            && (parsedData === null || parsedData === void 0 ? void 0 : parsedData.playabilityStatus);
    }

    function isEmbeddedPlayerObject(parsedData) {
        return typeof (parsedData === null || parsedData === void 0 ? void 0 : parsedData.previewPlayabilityStatus) === 'object';
    }

    function isAgeRestricted(playabilityStatus) {
        var _playabilityStatus$er;
        if (!(playabilityStatus !== null && playabilityStatus !== void 0 && playabilityStatus.status)) return false;
        if (playabilityStatus.desktopLegacyAgeGateReason) return true;
        if (Config.UNLOCKABLE_PLAYABILITY_STATUSES.includes(playabilityStatus.status)) return true;

        // Fix to detect age restrictions on embed player
        // see https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/85#issuecomment-946853553
        return (
            isEmbed
            && ((_playabilityStatus$er = playabilityStatus.errorScreen) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.playerErrorMessageRenderer) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.reason) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.runs) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.find((x) => x.navigationEndpoint)) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.navigationEndpoint) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.urlEndpoint) === null || _playabilityStatus$er === void 0
                    || (_playabilityStatus$er = _playabilityStatus$er.url) === null || _playabilityStatus$er === void 0
                ? void 0
                : _playabilityStatus$er.includes('/2802167'))
        );
    }

    function isSearchResult(parsedData) {
        var _parsedData$contents3, _parsedData$contents4, _parsedData$onRespons;
        return (
            typeof (parsedData === null || parsedData === void 0 || (_parsedData$contents3 = parsedData.contents) === null || _parsedData$contents3 === void 0
                    ? void 0
                    : _parsedData$contents3.twoColumnSearchResultsRenderer) === 'object' // Desktop initial results
            || (parsedData === null || parsedData === void 0 || (_parsedData$contents4 = parsedData.contents) === null || _parsedData$contents4 === void 0
                        || (_parsedData$contents4 = _parsedData$contents4.sectionListRenderer) === null || _parsedData$contents4 === void 0
                    ? void 0
                    : _parsedData$contents4.targetId) === 'search-feed' // Mobile initial results
            || (parsedData === null || parsedData === void 0 || (_parsedData$onRespons = parsedData.onResponseReceivedCommands) === null || _parsedData$onRespons === void 0
                        || (_parsedData$onRespons = _parsedData$onRespons.find((x) => x.appendContinuationItemsAction)) === null || _parsedData$onRespons === void 0
                        || (_parsedData$onRespons = _parsedData$onRespons.appendContinuationItemsAction) === null || _parsedData$onRespons === void 0
                    ? void 0
                    : _parsedData$onRespons.targetId) === 'search-feed' // Desktop & Mobile scroll continuation
        );
    }

    function attach$4(obj, prop, onCall) {
        if (!obj || typeof obj[prop] !== 'function') {
            return;
        }

        let original = obj[prop];

        obj[prop] = function() {
            try {
                onCall(arguments);
            } catch {}
            original.apply(this, arguments);
        };
    }

    const logPrefix = '%cSimple-YouTube-Age-Restriction-Bypass:';
    const logPrefixStyle = 'background-color: #1e5c85; color: #fff; font-size: 1.2em;';
    const logSuffix = '\uD83D\uDC1E You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues';

    function error(err, msg) {
        console.error(logPrefix, logPrefixStyle, msg, err, getYtcfgDebugString(), '\n\n', logSuffix);
        if (window.SYARB_CONFIG) {
            window.dispatchEvent(
                new CustomEvent('SYARB_LOG_ERROR', {
                    detail: {
                        message: (msg ? msg + '; ' : '') + (err && err.message ? err.message : ''),
                        stack: err && err.stack ? err.stack : null,
                    },
                }),
            );
        }
    }

    function info(msg) {
        console.info(logPrefix, logPrefixStyle, msg);
        if (window.SYARB_CONFIG) {
            window.dispatchEvent(
                new CustomEvent('SYARB_LOG_INFO', {
                    detail: {
                        message: msg,
                    },
                }),
            );
        }
    }

    function getYtcfgDebugString() {
        try {
            return (
                `InnertubeConfig: `
                + `innertubeApiKey: ${getYtcfgValue('INNERTUBE_API_KEY')} `
                + `innertubeClientName: ${getYtcfgValue('INNERTUBE_CLIENT_NAME')} `
                + `innertubeClientVersion: ${getYtcfgValue('INNERTUBE_CLIENT_VERSION')} `
                + `loggedIn: ${getYtcfgValue('LOGGED_IN')} `
            );
        } catch (err) {
            return `Failed to access config: ${err}`;
        }
    }

    /**
     * And here we deal with YouTube's crappy initial data (present in page source) and the problems that occur when intercepting that data.
     * YouTube has some protections in place that make it difficult to intercept and modify the global ytInitialPlayerResponse variable.
     * The easiest way would be to set a descriptor on that variable to change the value directly on declaration.
     * But some adblockers define their own descriptors on the ytInitialPlayerResponse variable, which makes it hard to register another descriptor on it.
     * As a workaround only the relevant playerResponse property of the ytInitialPlayerResponse variable will be intercepted.
     * This is achieved by defining a descriptor on the object prototype for that property, which affects any object with a `playerResponse` property.
     */
    function attach$3(onInitialData) {
        interceptObjectProperty('playerResponse', (obj, playerResponse) => {
            info(`playerResponse property set, contains sidebar: ${!!obj.response}`);

            // The same object also contains the sidebar data and video description
            if (isObject(obj.response)) onInitialData(obj.response);

            // If the script is executed too late and the bootstrap data has already been processed,
            // a reload of the player can be forced by creating a deep copy of the object.
            // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
            playerResponse.unlocked = false;
            onInitialData(playerResponse);
            return playerResponse.unlocked ? createDeepCopy(playerResponse) : playerResponse;
        });

        // The global `ytInitialData` variable can be modified on the fly.
        // It contains search results, sidebar data and meta information
        // Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
        window.addEventListener('DOMContentLoaded', () => {
            if (isObject(window.ytInitialData)) {
                onInitialData(window.ytInitialData);
            }
        });
    }

    function interceptObjectProperty(prop, onSet) {
        var _Object$getOwnPropert;
        // Allow other userscripts to decorate this descriptor, if they do something similar
        const dataKey = '__SYARB_' + prop;
        const { get: getter, set: setter } = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(Object.prototype, prop)) !== null && _Object$getOwnPropert !== void 0
            ? _Object$getOwnPropert
            : {
                set(value) {
                    this[dataKey] = value;
                },
                get() {
                    return this[dataKey];
                },
            };

        // Intercept the given property on any object
        // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
        Object.defineProperty(Object.prototype, prop, {
            set(value) {
                setter.call(this, isObject(value) ? onSet(this, value) : value);
            },
            get() {
                return getter.call(this);
            },
            configurable: true,
        });
    }

    // Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
    function attach$2(onJsonDataReceived) {
        window.JSON.parse = function() {
            const data = nativeJSONParse.apply(this, arguments);
            return isObject(data) ? onJsonDataReceived(data) : data;
        };
    }

    function attach$1(onRequestCreate) {
        if (typeof window.Request !== 'function') {
            return;
        }

        window.Request = new Proxy(window.Request, {
            construct(target, args) {
                let [url, options] = args;
                try {
                    if (typeof url === 'string') {
                        if (url.indexOf('/') === 0) {
                            url = window.location.origin + url;
                        }

                        if (url.indexOf('https://') !== -1) {
                            const modifiedUrl = onRequestCreate(url, options);

                            if (modifiedUrl) {
                                args[0] = modifiedUrl;
                            }
                        }
                    }
                } catch (err) {
                    error(err, `Failed to intercept Request()`);
                }

                return Reflect.construct(target, args);
            },
        });
    }

    function attach(onXhrOpenCalled) {
        XMLHttpRequest.prototype.open = function(...args) {
            let [method, url] = args;
            try {
                if (typeof url === 'string') {
                    if (url.indexOf('/') === 0) {
                        url = window.location.origin + url;
                    }

                    if (url.indexOf('https://') !== -1) {
                        const modifiedUrl = onXhrOpenCalled(method, url, this);

                        if (modifiedUrl) {
                            args[1] = modifiedUrl;
                        }
                    }
                }
            } catch (err) {
                error(err, `Failed to intercept XMLHttpRequest.open()`);
            }

            nativeXMLHttpRequestOpen.apply(this, args);
        };
    }

    const localStoragePrefix = 'SYARB_';

    function set(key, value) {
        localStorage.setItem(localStoragePrefix + key, JSON.stringify(value));
    }

    function get(key) {
        try {
            return JSON.parse(localStorage.getItem(localStoragePrefix + key));
        } catch {
            return null;
        }
    }

    function getPlayer$1(payload, useAuth) {
        return sendInnertubeRequest('v1/player', payload, useAuth);
    }

    function getNext$1(payload, useAuth) {
        return sendInnertubeRequest('v1/next', payload, useAuth);
    }

    function sendInnertubeRequest(endpoint, payload, useAuth) {
        const xmlhttp = new XMLHttpRequest();
        xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);

        if (useAuth && isUserLoggedIn()) {
            xmlhttp.withCredentials = true;
            Config.GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
                xmlhttp.setRequestHeader(headerName, get(headerName));
            });
        }

        xmlhttp.send(JSON.stringify(payload));
        return nativeJSONParse(xmlhttp.responseText);
    }

    var innertube = {
        getPlayer: getPlayer$1,
        getNext: getNext$1,
    };

    let nextResponseCache = {};

    function getGoogleVideoUrl(originalUrl) {
        return Config.VIDEO_PROXY_SERVER_HOST + '/direct/' + btoa(originalUrl.toString());
    }

    function getPlayer(payload) {
        // Also request the /next response if a later /next request is likely.
        if (!nextResponseCache[payload.videoId] && !isMusic && !isEmbed) {
            payload.includeNext = 1;
        }

        return sendRequest('getPlayer', payload);
    }

    function getNext(payload) {
        // Next response already cached? => Return cached content
        if (nextResponseCache[payload.videoId]) {
            return nextResponseCache[payload.videoId];
        }

        return sendRequest('getNext', payload);
    }

    function sendRequest(endpoint, payload) {
        const queryParams = new URLSearchParams(payload);
        const proxyUrl = `${Config.ACCOUNT_PROXY_SERVER_HOST}/${endpoint}?${queryParams}&client=js`;

        try {
            const xmlhttp = new XMLHttpRequest();
            xmlhttp.open('GET', proxyUrl, false);
            xmlhttp.send(null);

            const proxyResponse = nativeJSONParse(xmlhttp.responseText);

            // Mark request as 'proxied'
            proxyResponse.proxied = true;

            // Put included /next response in the cache
            if (proxyResponse.nextResponse) {
                nextResponseCache[payload.videoId] = proxyResponse.nextResponse;
                delete proxyResponse.nextResponse;
            }

            return proxyResponse;
        } catch (err) {
            error(err, 'Proxy API Error');
            return { errorMessage: 'Proxy Connection failed' };
        }
    }

    var proxy = {
        getPlayer,
        getNext,
        getGoogleVideoUrl,
    };

    function getUnlockStrategies$1(videoId, lastPlayerUnlockReason) {
        var _getYtcfgValue$client;
        const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
        const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
        const hl = getYtcfgValue('HL');
        const userInterfaceTheme = (_getYtcfgValue$client = getYtcfgValue('INNERTUBE_CONTEXT').client.userInterfaceTheme) !== null && _getYtcfgValue$client !== void 0
            ? _getYtcfgValue$client
            : document.documentElement.hasAttribute('dark')
            ? 'USER_INTERFACE_THEME_DARK'
            : 'USER_INTERFACE_THEME_LIGHT';

        return [
            /**
             * Retrieve the sidebar and video description by just adding `racyCheckOk` and `contentCheckOk` params
             * This strategy can be used to bypass content warnings
             */
            {
                name: 'Content Warning Bypass',
                skip: !lastPlayerUnlockReason || !lastPlayerUnlockReason.includes('CHECK_REQUIRED'),
                optionalAuth: true,
                payload: {
                    context: {
                        client: {
                            clientName,
                            clientVersion,
                            hl,
                            userInterfaceTheme,
                        },
                    },
                    videoId,
                    racyCheckOk: true,
                    contentCheckOk: true,
                },
                endpoint: innertube,
            },
            /**
             * Retrieve the sidebar and video description from an account proxy server.
             * Session cookies of an age-verified Google account are stored on server side.
             * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
             */
            {
                name: 'Account Proxy',
                payload: {
                    videoId,
                    clientName,
                    clientVersion,
                    hl,
                    userInterfaceTheme,
                    isEmbed: +isEmbed,
                    isConfirmed: +isConfirmed,
                },
                endpoint: proxy,
            },
        ];
    }

    function getUnlockStrategies(videoId, reason) {
        const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
        const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
        const signatureTimestamp = getSignatureTimestamp();
        const startTimeSecs = getCurrentVideoStartTime(videoId);
        const hl = getYtcfgValue('HL');

        return [
            /**
             * Retrieve the video info by just adding `racyCheckOk` and `contentCheckOk` params
             * This strategy can be used to bypass content warnings
             */
            {
                name: 'Content Warning Bypass',
                skip: !reason || !reason.includes('CHECK_REQUIRED'),
                optionalAuth: true,
                payload: {
                    context: {
                        client: {
                            clientName: clientName,
                            clientVersion: clientVersion,
                            hl,
                        },
                    },
                    playbackContext: {
                        contentPlaybackContext: {
                            signatureTimestamp,
                        },
                    },
                    videoId,
                    startTimeSecs,
                    racyCheckOk: true,
                    contentCheckOk: true,
                },
                endpoint: innertube,
            },
            /**
             * Retrieve the video info by using the TVHTML5 Embedded client
             * This client has no age restrictions in place (2022-03-28)
             * See https://github.com/zerodytrash/YouTube-Internal-Clients
             */
            {
                name: 'TV Embedded Player',
                requiresAuth: false,
                payload: {
                    context: {
                        client: {
                            clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
                            clientVersion: '2.0',
                            clientScreen: 'WATCH',
                            hl,
                        },
                        thirdParty: {
                            embedUrl: 'https://www.youtube.com/',
                        },
                    },
                    playbackContext: {
                        contentPlaybackContext: {
                            signatureTimestamp,
                        },
                    },
                    videoId,
                    startTimeSecs,
                    racyCheckOk: true,
                    contentCheckOk: true,
                },
                endpoint: innertube,
            },
            /**
             * Retrieve the video info by using the WEB_CREATOR client in combination with user authentication
             * Requires that the user is logged in. Can bypass the tightened age verification in the EU.
             * See https://github.com/yt-dlp/yt-dlp/pull/600
             */
            {
                name: 'Creator + Auth',
                requiresAuth: true,
                payload: {
                    context: {
                        client: {
                            clientName: 'WEB_CREATOR',
                            clientVersion: '1.20210909.07.00',
                            hl,
                        },
                    },
                    playbackContext: {
                        contentPlaybackContext: {
                            signatureTimestamp,
                        },
                    },
                    videoId,
                    startTimeSecs,
                    racyCheckOk: true,
                    contentCheckOk: true,
                },
                endpoint: innertube,
            },
            /**
             * Retrieve the video info from an account proxy server.
             * Session cookies of an age-verified Google account are stored on server side.
             * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
             */
            {
                name: 'Account Proxy',
                payload: {
                    videoId,
                    reason,
                    clientName,
                    clientVersion,
                    signatureTimestamp,
                    startTimeSecs,
                    hl,
                    isEmbed: +isEmbed,
                    isConfirmed: +isConfirmed,
                },
                endpoint: proxy,
            },
        ];
    }

    var buttonTemplate =
        '<div style="margin-top: 15px !important; padding: 3px 10px 3px 10px; margin: 0px auto; background-color: #4d4d4d; width: fit-content; font-size: 1.2em; text-transform: uppercase; border-radius: 3px; cursor: pointer;">\n    <div class="button-text"></div>\n</div>';

    let buttons = {};

    async function addButton(id, text, backgroundColor, onClick) {
        const errorScreenElement = await waitForElement('.ytp-error', 2000);
        const buttonElement = createElement('div', { class: 'button-container', innerHTML: buttonTemplate });
        buttonElement.getElementsByClassName('button-text')[0].innerText = text;

        if (typeof onClick === 'function') {
            buttonElement.addEventListener('click', onClick);
        }

        // Button already attached?
        if (buttons[id] && buttons[id].isConnected) {
            return;
        }

        buttons[id] = buttonElement;
        errorScreenElement.append(buttonElement);
    }

    function removeButton(id) {
        if (buttons[id] && buttons[id].isConnected) {
            buttons[id].remove();
        }
    }

    const confirmationButtonId = 'confirmButton';
    const confirmationButtonText = 'Click to unlock';

    function isConfirmationRequired() {
        return !isConfirmed && isEmbed && Config.ENABLE_UNLOCK_CONFIRMATION_EMBED;
    }

    function requestConfirmation() {
        addButton(confirmationButtonId, confirmationButtonText, null, () => {
            removeButton(confirmationButtonId);
            confirm();
        });
    }

    function confirm() {
        setUrlParams({
            unlock_confirmed: 1,
            autoplay: 1,
        });
    }

    var tDesktop = '<tp-yt-paper-toast></tp-yt-paper-toast>\n';

    var tMobile =
        '<c3-toast>\n    <ytm-notification-action-renderer>\n        <div class="notification-action-response-text"></div>\n    </ytm-notification-action-renderer>\n</c3-toast>\n';

    const template = isDesktop ? tDesktop : tMobile;

    const nToastContainer = createElement('div', { id: 'toast-container', innerHTML: template });
    const nToast = nToastContainer.querySelector(':scope > *');

    // On YT Music show the toast above the player controls
    if (isMusic) {
        nToast.style['margin-bottom'] = '85px';
    }

    if (!isDesktop) {
        nToast.nMessage = nToast.querySelector('.notification-action-response-text');
        nToast.show = (message) => {
            nToast.nMessage.innerText = message;
            nToast.setAttribute('dir', 'in');
            setTimeout(() => {
                nToast.setAttribute('dir', 'out');
            }, nToast.duration + 225);
        };
    }

    async function show(message, duration = 5) {
        if (!Config.ENABLE_UNLOCK_NOTIFICATION) return;
        if (isEmbed) return;

        await pageLoaded();

        // Do not show notification when tab is in background
        if (document.visibilityState === 'hidden') return;

        // Append toast container to DOM, if not already done
        if (!nToastContainer.isConnected) document.documentElement.append(nToastContainer);

        nToast.duration = duration * 1000;
        nToast.show(message);
    }

    var Toast = { show };

    const messagesMap = {
        success: 'Age-restricted video successfully unlocked!',
        fail: 'Unable to unlock this video 🙁 - More information in the developer console',
    };

    let lastPlayerUnlockVideoId = null;
    let lastPlayerUnlockReason = null;

    let lastProxiedGoogleVideoUrlParams;
    let cachedPlayerResponse = {};

    function getLastProxiedGoogleVideoId() {
        var _lastProxiedGoogleVid;
        return (_lastProxiedGoogleVid = lastProxiedGoogleVideoUrlParams) === null || _lastProxiedGoogleVid === void 0 ? void 0 : _lastProxiedGoogleVid.get('id');
    }

    function unlockResponse$1(playerResponse) {
        var _playerResponse$video, _playerResponse$playa, _playerResponse$previ, _unlockedPlayerRespon, _unlockedPlayerRespon3;
        // Check if the user has to confirm the unlock first
        if (isConfirmationRequired()) {
            info('Unlock confirmation required.');
            requestConfirmation();
            return;
        }

        const videoId = ((_playerResponse$video = playerResponse.videoDetails) === null || _playerResponse$video === void 0 ? void 0 : _playerResponse$video.videoId)
            || getYtcfgValue('PLAYER_VARS').video_id;
        const reason = ((_playerResponse$playa = playerResponse.playabilityStatus) === null || _playerResponse$playa === void 0 ? void 0 : _playerResponse$playa.status)
            || ((_playerResponse$previ = playerResponse.previewPlayabilityStatus) === null || _playerResponse$previ === void 0 ? void 0 : _playerResponse$previ.status);

        if (!Config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
            info(`SKIP_CONTENT_WARNINGS disabled and ${reason} status detected.`);
            return;
        }

        lastPlayerUnlockVideoId = videoId;
        lastPlayerUnlockReason = reason;

        const unlockedPlayerResponse = getUnlockedPlayerResponse(videoId, reason);

        // account proxy error?
        if (unlockedPlayerResponse.errorMessage) {
            Toast.show(`${messagesMap.fail} (ProxyError)`, 10);
            throw new Error(`Player Unlock Failed, Proxy Error Message: ${unlockedPlayerResponse.errorMessage}`);
        }

        // check if the unlocked response isn't playable
        if (
            !Config.VALID_PLAYABILITY_STATUSES.includes(
                (_unlockedPlayerRespon = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon === void 0 ? void 0 : _unlockedPlayerRespon.status,
            )
        ) {
            var _unlockedPlayerRespon2;
            Toast.show(`${messagesMap.fail} (PlayabilityError)`, 10);
            throw new Error(
                `Player Unlock Failed, playabilityStatus: ${
                    (_unlockedPlayerRespon2 = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon2 === void 0 ? void 0 : _unlockedPlayerRespon2.status
                }`,
            );
        }

        // if the video info was retrieved via proxy, store the URL params from the url-attribute to detect later if the requested video file (googlevideo.com) need a proxy.
        if (
            unlockedPlayerResponse.proxied && (_unlockedPlayerRespon3 = unlockedPlayerResponse.streamingData) !== null && _unlockedPlayerRespon3 !== void 0
            && _unlockedPlayerRespon3.adaptiveFormats
        ) {
            var _unlockedPlayerRespon4, _unlockedPlayerRespon5;
            const cipherText = (_unlockedPlayerRespon4 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) =>
                            x.signatureCipher
                        )) === null || _unlockedPlayerRespon4 === void 0
                ? void 0
                : _unlockedPlayerRespon4.signatureCipher;
            const videoUrl = cipherText
                ? new URLSearchParams(cipherText).get('url')
                : (_unlockedPlayerRespon5 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) => x.url)) === null || _unlockedPlayerRespon5 === void 0
                ? void 0
                : _unlockedPlayerRespon5.url;

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

        // Overwrite the embedded (preview) playabilityStatus with the unlocked one
        if (playerResponse.previewPlayabilityStatus) {
            playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;
        }

        // Transfer all unlocked properties to the original player response
        Object.assign(playerResponse, unlockedPlayerResponse);

        playerResponse.unlocked = true;

        Toast.show(messagesMap.success);
    }

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

        const unlockStrategies = getUnlockStrategies(videoId, reason);

        let unlockedPlayerResponse = {};

        // Try every strategy until one of them works
        unlockStrategies.every((strategy, index) => {
            var _unlockedPlayerRespon6;
            // Skip strategy if authentication is required and the user is not logged in
            if (strategy.skip || strategy.requiresAuth && !isUserLoggedIn()) return true;

            info(`Trying Player Unlock Method #${index + 1} (${strategy.name})`);

            try {
                unlockedPlayerResponse = strategy.endpoint.getPlayer(strategy.payload, strategy.requiresAuth || strategy.optionalAuth);
            } catch (err) {
                error(err, `Player Unlock Method ${index + 1} failed with exception`);
            }

            const isStatusValid = Config.VALID_PLAYABILITY_STATUSES.includes(
                (_unlockedPlayerRespon6 = unlockedPlayerResponse) === null || _unlockedPlayerRespon6 === void 0
                    || (_unlockedPlayerRespon6 = _unlockedPlayerRespon6.playabilityStatus) === null || _unlockedPlayerRespon6 === void 0
                    ? void 0
                    : _unlockedPlayerRespon6.status,
            );

            if (isStatusValid) {
                var _unlockedPlayerRespon7;
                /**
                 * Workaround: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/191
                 *
                 * YouTube checks if the `trackingParams` in the response matches the decoded `trackingParam` in `responseContext.mainAppWebResponseContext`.
                 * However, sometimes the response does not include the `trackingParam` in the `responseContext`, causing the check to fail.
                 *
                 * This workaround addresses the issue by hardcoding the `trackingParams` in the response context.
                 */
                if (
                    !unlockedPlayerResponse.trackingParams
                    || !((_unlockedPlayerRespon7 = unlockedPlayerResponse.responseContext) !== null && _unlockedPlayerRespon7 !== void 0
                        && (_unlockedPlayerRespon7 = _unlockedPlayerRespon7.mainAppWebResponseContext) !== null && _unlockedPlayerRespon7 !== void 0
                        && _unlockedPlayerRespon7.trackingParam)
                ) {
                    unlockedPlayerResponse.trackingParams = 'CAAQu2kiEwjor8uHyOL_AhWOvd4KHavXCKw=';
                    unlockedPlayerResponse.responseContext = {
                        mainAppWebResponseContext: {
                            trackingParam: 'kx_fmPxhoPZRzgL8kzOwANUdQh8ZwHTREkw2UqmBAwpBYrzRgkuMsNLBwOcCE59TDtslLKPQ-SS',
                        },
                    };
                }

                /**
                 * Workaround: Account proxy response currently does not include `playerConfig`
                 *
                 * Stays here until we rewrite the account proxy to only include the necessary and bare minimum response
                 */
                if (strategy.payload.startTimeSecs && strategy.name === 'Account Proxy') {
                    unlockedPlayerResponse.playerConfig = {
                        playbackStartConfig: {
                            startSeconds: strategy.payload.startTimeSecs,
                        },
                    };
                }
            }

            return !isStatusValid;
        });

        // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
        cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };

        return unlockedPlayerResponse;
    }

    let cachedNextResponse = {};

    function unlockResponse(originalNextResponse) {
        const videoId = originalNextResponse.currentVideoEndpoint.watchEndpoint.videoId;

        if (!videoId) {
            throw new Error(`Missing videoId in nextResponse`);
        }

        // Only unlock the /next response when the player has been unlocked as well
        if (videoId !== lastPlayerUnlockVideoId) {
            return;
        }

        const unlockedNextResponse = getUnlockedNextResponse(videoId);

        // check if the sidebar of the unlocked response is still empty
        if (isWatchNextSidebarEmpty(unlockedNextResponse)) {
            throw new Error(`Sidebar Unlock Failed`);
        }

        // Transfer some parts of the unlocked response to the original response
        mergeNextResponse(originalNextResponse, unlockedNextResponse);
    }

    function getUnlockedNextResponse(videoId) {
        // Check if response is cached
        if (cachedNextResponse.videoId === videoId) return createDeepCopy(cachedNextResponse);

        const unlockStrategies = getUnlockStrategies$1(videoId, lastPlayerUnlockReason);

        let unlockedNextResponse = {};

        // Try every strategy until one of them works
        unlockStrategies.every((strategy, index) => {
            if (strategy.skip) return true;

            info(`Trying Next Unlock Method #${index + 1} (${strategy.name})`);

            try {
                unlockedNextResponse = strategy.endpoint.getNext(strategy.payload, strategy.optionalAuth);
            } catch (err) {
                error(err, `Next Unlock Method ${index + 1} failed with exception`);
            }

            return isWatchNextSidebarEmpty(unlockedNextResponse);
        });

        // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
        cachedNextResponse = { videoId, ...createDeepCopy(unlockedNextResponse) };

        return unlockedNextResponse;
    }

    function mergeNextResponse(originalNextResponse, unlockedNextResponse) {
        var _unlockedNextResponse;
        if (isDesktop) {
            // Transfer WatchNextResults to original response
            originalNextResponse.contents.twoColumnWatchNextResults.secondaryResults = unlockedNextResponse.contents.twoColumnWatchNextResults.secondaryResults;

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

            // TODO: Throw if description not found?
            if (unlockedVideoSecondaryInfoRenderer.description) {
                originalVideoSecondaryInfoRenderer.description = unlockedVideoSecondaryInfoRenderer.description;
            } else if (unlockedVideoSecondaryInfoRenderer.attributedDescription) {
                originalVideoSecondaryInfoRenderer.attributedDescription = unlockedVideoSecondaryInfoRenderer.attributedDescription;
            }

            return;
        }

        // Transfer WatchNextResults to original response
        const unlockedWatchNextFeed = (_unlockedNextResponse = unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
                || (_unlockedNextResponse = _unlockedNextResponse.singleColumnWatchNextResults) === null || _unlockedNextResponse === void 0
                || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
                || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
                || (_unlockedNextResponse = _unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
            ? void 0
            : _unlockedNextResponse.find(
                (x) => {
                    var _x$itemSectionRendere;
                    return ((_x$itemSectionRendere = x.itemSectionRenderer) === null || _x$itemSectionRendere === void 0 ? void 0 : _x$itemSectionRendere.targetId)
                        === 'watch-next-feed';
                },
            );

        if (unlockedWatchNextFeed) originalNextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(unlockedWatchNextFeed);

        // Transfer video description to original response
        const originalStructuredDescriptionContentRenderer = originalNextResponse.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 (unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer) {
            originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer =
                unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer;
        }
    }

    /**
     *  Handles XMLHttpRequests and
     * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
     * - Store auth headers for the authentication of further unlock requests.
     * - Add "content check ok" flags to request bodys
     */
    function handleXhrOpen(method, url, xhr) {
        const url_obj = new URL(url);
        let proxyUrl = unlockGoogleVideo(url_obj);
        if (proxyUrl) {
            // Exclude credentials from XMLHttpRequest
            Object.defineProperty(xhr, 'withCredentials', {
                set: () => {},
                get: () => false,
            });
            return proxyUrl.toString();
        }

        if (url_obj.pathname.indexOf('/youtubei/') === 0) {
            // Store auth headers in storage for further usage.
            attach$4(xhr, 'setRequestHeader', ([headerName, headerValue]) => {
                if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
                    set(headerName, headerValue);
                }
            });
        }

        if (Config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
            // Add content check flags to player and next request (this will skip content warnings)
            attach$4(xhr, 'send', (args) => {
                if (typeof args[0] === 'string') {
                    args[0] = setContentCheckOk(args[0]);
                }
            });
        }
    }

    /**
     *  Handles Fetch requests and
     * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
     * - Store auth headers for the authentication of further unlock requests.
     * - Add "content check ok" flags to request bodys
     */
    function handleFetchRequest(url, requestOptions) {
        const url_obj = new URL(url);
        const newGoogleVideoUrl = unlockGoogleVideo(url_obj);
        if (newGoogleVideoUrl) {
            // Exclude credentials from Fetch Request
            if (requestOptions.credentials) {
                requestOptions.credentials = 'omit';
            }
            return newGoogleVideoUrl.toString();
        }

        if (url_obj.pathname.indexOf('/youtubei/') === 0 && isObject(requestOptions.headers)) {
            // Store auth headers in authStorage for further usage.
            for (let headerName in requestOptions.headers) {
                if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
                    set(headerName, requestOptions.headers[headerName]);
                }
            }
        }

        if (Config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
            // Add content check flags to player and next request (this will skip content warnings)
            requestOptions.body = setContentCheckOk(requestOptions.body);
        }
    }

    /**
     * If the account proxy was used to retrieve 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...
     * @returns The rewitten url (if a proxy is required)
     */
    function unlockGoogleVideo(url) {
        if (Config.VIDEO_PROXY_SERVER_HOST && isGoogleVideoUrl(url)) {
            if (isGoogleVideoUnlockRequired(url, getLastProxiedGoogleVideoId())) {
                return proxy.getGoogleVideoUrl(url);
            }
        }
    }

    /**
     * Adds `contentCheckOk` and `racyCheckOk` to the given json data (if the data contains a video id)
     * @returns {string} The modified json
     */
    function setContentCheckOk(bodyJson) {
        try {
            let parsedBody = JSON.parse(bodyJson);
            if (parsedBody.videoId) {
                parsedBody.contentCheckOk = true;
                parsedBody.racyCheckOk = true;
                return JSON.stringify(parsedBody);
            }
        } catch {}
        return bodyJson;
    }

    function processThumbnails(responseObject) {
        const thumbnails = findNestedObjectsByAttributeNames(responseObject, ['url', 'height']);

        let blurredThumbnailCount = 0;

        for (const thumbnail of thumbnails) {
            if (isThumbnailBlurred(thumbnail)) {
                blurredThumbnailCount++;
                thumbnail.url = thumbnail.url.split('?')[0];
            }
        }

        info(blurredThumbnailCount + '/' + thumbnails.length + ' thumbnails detected as blurred.');
    }

    function isThumbnailBlurred(thumbnail) {
        const hasSQPParam = thumbnail.url.indexOf('?sqp=') !== -1;

        if (!hasSQPParam) {
            return false;
        }

        const SQPLength = new URL(thumbnail.url).searchParams.get('sqp').length;
        const isBlurred = Config.BLURRED_THUMBNAIL_SQP_LENGTHS.includes(SQPLength);

        return isBlurred;
    }

    try {
        attach$3(processYtData);
        attach$2(processYtData);
        attach(handleXhrOpen);
        attach$1(handleFetchRequest);
    } catch (err) {
        error(err, 'Error while attaching data interceptors');
    }

    function processYtData(ytData) {
        try {
            // Player Unlock #1: Initial page data structure and response from `/youtubei/v1/player` XHR request
            if (isPlayerObject(ytData) && isAgeRestricted(ytData.playabilityStatus)) {
                unlockResponse$1(ytData);
            } // Player Unlock #2: Embedded Player inital data structure
            else if (isEmbeddedPlayerObject(ytData) && isAgeRestricted(ytData.previewPlayabilityStatus)) {
                unlockResponse$1(ytData);
            }
        } catch (err) {
            error(err, 'Video unlock failed');
        }

        try {
            // Unlock sidebar watch next feed (sidebar) and video description
            if (isWatchNextObject(ytData) && isWatchNextSidebarEmpty(ytData)) {
                unlockResponse(ytData);
            }

            // Mobile version
            if (isWatchNextObject(ytData.response) && isWatchNextSidebarEmpty(ytData.response)) {
                unlockResponse(ytData.response);
            }
        } catch (err) {
            error(err, 'Sidebar unlock failed');
        }

        try {
            // Unlock blurry video thumbnails in search results
            if (isSearchResult(ytData)) {
                processThumbnails(ytData);
            }
        } catch (err) {
            error(err, 'Thumbnail unlock failed');
        }

        return ytData;
    }
})();