You are on page 1of 11

// ==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.4
// @author Zerody (https://github.com/zerodytrash)
// @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-
Bypass/
// @updateURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-
Bypass/raw/main/Simple-YouTube-Age-Restriction-Bypass.user.js
// @downloadURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-
Bypass/raw/main/Simple-YouTube-Age-Restriction-Bypass.user.js
// @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 = {};

const Deferred = function () {


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

const Notification = (() => {


const isPolymer = !!window.Polymer;

const pageLoad = new Deferred();


const pageLoadEventName = isPolymer ? 'yt-navigate-finish' : 'state-
navigateend';

const node = isPolymer


? createElement('tp-yt-paper-toast')
: createElement('c3-toast', { innerHTML: '<ytm-notification-action-
renderer><div class="notification-action-response-text"></div></ytm-notification-
action-renderer>' });

const nMobileText = !isPolymer && node.querySelector('.notification-action-


response-text');

window.addEventListener(pageLoadEventName, init, { once: true });

function init() {
document.body.append(node);
pageLoad.resolve();
}

function show(message, duration = 5) {


if (!ENABLE_UNLOCK_NOTIFICATION) return;

pageLoad.then(_show);

function _show() {
if (isPolymer) {
node.duration = duration * 1000;
node.show(message);
} else {
nMobileText.innerHTML = message;
node.setAttribute('dir', 'in');
setTimeout(() => {
node.setAttribute('dir', 'out');
}, duration * 1000 + 225);
}
}
}

return { show };
})();

// 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);
}
}
}

// 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) {
Notification.show("Unable to unlock this video 🙁 - More information in
the developer console (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") {
Notification.show(`Unable to unlock this video 🙁 - More information in
the developer console (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;
}

Notification.show("Video successfully unlocked!");

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?.con
tents
?.find(x => x.itemSectionRenderer?.targetId === "watch-next-feed");
if (unlockedWatchNextFeed)
nextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(un
lockedWatchNextFeed);
}

// 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?.content
s
?.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?.expandableVideoDescriptionBodyRendere
r)

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...`);
}
}
}

// 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();
}

You might also like