// ==UserScript== // @name Simple YouTube Age Restriction Bypass // @name:de Simple YouTube Age Restriction Bypass // @version 2.0.5 // @description View age restricted videos on YouTube without verification and login :) // @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen :) // @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 // @downloadURL none // ==/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", "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 && isUnlockable(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 && isUnlockable(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 && isUnlockable(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 isUnlockable(playabilityStatus) { if (!playabilityStatus || !playabilityStatus.status) return false; return 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 (`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 (`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; } } })();