// ==UserScript==
// @name Simple YouTube Age Restriction Bypass
// @description Watch age restricted videos on YouTube without login and without age verification :)
// @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen :)
// @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge :)
// @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età :)
// @version 2.0.6
// @author Zerody (https://github.com/zerodytrash)
// @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/
// @supportURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues
// @license MIT
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @grant none
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox + Tampermonkey or Violentmonkey
// @compatible opera Opera + Tampermonkey or Violentmonkey
// @compatible edge Edge + Tampermonkey or Violentmonkey
// @compatible safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==
(function () {
var nativeParse = window.JSON.parse; // Backup the original parse function
var nativeDefineProperty = getNativeDefineProperty(); // Backup the original defineProperty function to intercept setter & getter on the ytInitialPlayerResponse
var nativeXmlHttpOpen = XMLHttpRequest.prototype.open;
var wrappedPlayerResponse = null;
var unlockablePlayerStates = ["AGE_VERIFICATION_REQUIRED", "AGE_CHECK_REQUIRED", "LOGIN_REQUIRED"];
var playerResponsePropertyAliases = ["ytInitialPlayerResponse", "playerResponse"];
var lastProxiedGoogleVideoUrlParams = null;
var responseCache = {};
// Youtube API config (Innertube).
// The actual values will be determined later from the global ytcfg variable => setInnertubeConfigFromYtcfg()
var innertubeConfig = {
INNERTUBE_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
INNERTUBE_CLIENT_VERSION: "2.20210721.00.00",
STS: 18834 // signatureTimestamp (relevant for the cipher functions)
};
// The following proxies are currently used as fallback if the innertube age-gate bypass doesn't work...
// You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
var accountProxyServerHost = "https://youtube-proxy.zerody.one";
var videoProxyServerHost = "https://phx.4everproxy.com";
// UI related stuff (notifications, ...)
var enableUnlockNotification = true;
var playerCreationObserver = null;
var notificationElement = null;
var notificationTimeout = null;
// Just for compatibility: Backup original getter/setter for 'ytInitialPlayerResponse', defined by other extensions like AdBlock
var initialPlayerResponseDescriptor = window.Object.getOwnPropertyDescriptor(window, "ytInitialPlayerResponse");
var chainedSetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.set : null;
var chainedGetter = initialPlayerResponseDescriptor ? initialPlayerResponseDescriptor.get : null;
// Just for compatibility: Intercept (re-)definitions on Youtube's initial player response property to chain setter/getter from other extensions by hijacking the Object.defineProperty function
window.Object.defineProperty = function (obj, prop, descriptor) {
if (obj === window && playerResponsePropertyAliases.includes(prop)) {
console.info("Another extension tries to re-define '" + prop + "' (probably an AdBlock extension). Chain it...");
if (descriptor && descriptor.set) chainedSetter = descriptor.set;
if (descriptor && descriptor.get) chainedGetter = descriptor.get;
} else {
nativeDefineProperty(obj, prop, descriptor);
}
}
// Re-define 'ytInitialPlayerResponse' to inspect and modify the initial player response as soon as the variable is set on page load
nativeDefineProperty(window, "ytInitialPlayerResponse", {
set: function (playerResponse) {
// prevent recursive setter calls by ignoring unchanged data (this fixes a problem caused by brave browser shield)
if (playerResponse === wrappedPlayerResponse) return;
wrappedPlayerResponse = inspectJsonData(playerResponse);
if (typeof chainedSetter === "function") chainedSetter(wrappedPlayerResponse);
},
get: function () {
if (typeof chainedGetter === "function") try { return chainedGetter() } catch (err) { };
return wrappedPlayerResponse || {};
},
configurable: true
});
// Intercept XMLHttpRequest.open to rewrite video url's (sometimes required)
XMLHttpRequest.prototype.open = function () {
if (arguments.length > 1 && typeof arguments[1] === "string" && arguments[1].indexOf("https://") === 0) {
var method = arguments[0];
var url = new URL(arguments[1]);
var urlParams = new URLSearchParams(url.search);
// if the account proxy was used to retieve the video info, the following applies:
// some video files (mostly music videos) can only be accessed from IPs in the same country as the innertube api request (/youtubei/v1/player) was made.
// to get around this, the googlevideo url will be replaced with a web-proxy url in the same country (US).
// this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url...
function isGoogleVideo() {
return method === "GET" && url.host.indexOf(".googlevideo.com") > 0;
}
function hasGcrFlag() {
return urlParams.get("gcr") !== null;
}
function isUnlockedByAccountProxy() {
return urlParams.get("id") !== null && lastProxiedGoogleVideoUrlParams && urlParams.get("id") === lastProxiedGoogleVideoUrlParams.get("id");
}
if (videoProxyServerHost && isGoogleVideo() && hasGcrFlag() && isUnlockedByAccountProxy()) {
// rewrite request url
arguments[1] = videoProxyServerHost + "/direct/" + btoa(arguments[1]);
// solve CORS errors by preventing youtube from enabling the "withCredentials" option (not required for the proxy)
nativeDefineProperty(this, "withCredentials", {
set: function () { },
get: function () {
return false;
}
});
}
}
return nativeXmlHttpOpen.apply(this, arguments);
}
// Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
window.JSON.parse = function (text, reviver) {
return inspectJsonData(nativeParse(text, reviver));
}
function inspectJsonData(parsedData) {
// If youtube does JSON.parse(null) or similar weird things
if (typeof parsedData !== "object" || parsedData === null) return parsedData;
try {
// Unlock #1: Array based in "&pbj=1" AJAX response on any navigation (does not seem to be used anymore)
if (Array.isArray(parsedData)) {
var playerResponseArrayItem = parsedData.find(e => typeof e.playerResponse === "object");
var playerResponse = playerResponseArrayItem?.playerResponse;
if (playerResponse && isAgeRestricted(playerResponse.playabilityStatus)) {
playerResponseArrayItem.playerResponse = unlockPlayerResponse(playerResponse);
}
}
// Hide unlock notification on navigation (if still visible from the last unlock)
if (parsedData.playerResponse || parsedData.playabilityStatus) hidePlayerNotification();
// Unlock #2: Another JSON-Object containing the 'playerResponse'
if (parsedData.playerResponse?.playabilityStatus && parsedData.playerResponse?.videoDetails && isAgeRestricted(parsedData.playerResponse.playabilityStatus)) {
parsedData.playerResponse = unlockPlayerResponse(parsedData.playerResponse);
}
// Unlock #3: Initial page data structure and response from '/youtubei/v1/player' endpoint
if (parsedData.playabilityStatus && parsedData.videoDetails && isAgeRestricted(parsedData.playabilityStatus)) {
parsedData = unlockPlayerResponse(parsedData);
}
} catch (err) {
console.error("Simple-YouTube-Age-Restriction-Bypass-Error:", err, "You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues");
}
return parsedData;
}
function isAgeRestricted(playabilityStatus) {
if (!playabilityStatus || !playabilityStatus.status) return false;
return typeof playabilityStatus.desktopLegacyAgeGateReason !== "undefined" || unlockablePlayerStates.includes(playabilityStatus.status);
}
function unlockPlayerResponse(playerResponse) {
var videoId = playerResponse.videoDetails.videoId;
var reason = playerResponse.playabilityStatus?.status;
var unlockedPayerResponse = getUnlockedPlayerResponse(videoId, reason);
// account proxy error?
if (unlockedPayerResponse.errorMessage) {
showPlayerNotification("#7b1e1e", "Unable to unlock this video :( Please look into the developer console for more details. (ProxyError)", 10);
throw (`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;
}
}
})();