0% found this document useful (0 votes)
1K views18 pages

Simple YouTube Age Restriction Bypass

Download as txt, pdf, or txt
Download as txt, pdf, or txt
Download as txt, pdf, or txt
You are on page 1/ 18

// ==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.3.1
// @author Zerody (https://github.com/zerodytrash)
// @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-
Bypass/
// @supportURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-
Bypass/issues
// @license MIT
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @grant none
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox + Greasemonkey or Tampermonkey or Violentmonkey
// @compatible opera Opera + Tampermonkey or Violentmonkey
// @compatible edge Edge + Tampermonkey or Violentmonkey
// @compatible safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==

/*
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(inject) {
// Trick to get around the sandbox restrictions in Greasemonkey (Firefox)
// Inject code into the main window if criteria match
if (typeof GM_info === "object" && GM_info.scriptHandler === "Greasemonkey" &&
inject) {
window.eval("(" + iife.toString() + ")();");
return;
}

// Script configuration variables


const UNLOCKABLE_PLAYER_STATES = ['AGE_VERIFICATION_REQUIRED',
'AGE_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
const PLAYER_RESPONSE_ALIASES = ['ytInitialPlayerResponse', 'playerResponse'];

// 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';
// Whether a thumbnail is blurred can be detected by the following "sqp"
parameter values in the thumbnail URL.
// Seems to be base64 encoded protobuf objects, see
https://stackoverflow.com/a/51203860
const THUMBNAIL_BLURRED_SQPS = [
'-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=', // Desktop
480x270
'-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==', // Desktop 480x270
'-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==', // Desktop 360x202
'-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==', // Desktop 720x404
'-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=', // Desktop 720x404
'-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E=', // Desktop 336x188
'-oaymwESCMACELQB8quKqQMG7QHMzMxB', // Mobile 320x180
'-oaymwESCOADEOgC8quKqQMG7QGZmRlC' // Mobile 480x360
];

const isDesktop = window.location.host !== 'm.youtube.com';


const isEmbed = window.location.pathname.includes('/embed/');

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

}}

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 getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}

// Source: https://coursesweb.net/javascript/sha1-encrypt-data_cs
function generateSha1Hash(msg) {
function rotate_left(n, s) {
var t4 = n << s | n >>> 32 - s;
return t4;
}
function cvt_hex(val) {
var str = '';
var i;
var v;
for (i = 7; i >= 0; i--) {
v = val >>> i * 4 & 0x0f;
str += v.toString(16);
}
return str;
}
function Utf8Encode(string) {
string = string.replace(/\r\n/g, '\n');
var utftext = '';
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode(c >> 6 | 192);
utftext += String.fromCharCode(c & 63 | 128);
} else {
utftext += String.fromCharCode(c >> 12 | 224);
utftext += String.fromCharCode(c >> 6 & 63 | 128);
utftext += String.fromCharCode(c & 63 | 128);
}
}
return utftext;
}
var blockstart;
var i, j;
var W = new Array(80);
var H0 = 0x67452301;
var H1 = 0xefcdab89;
var H2 = 0x98badcfe;
var H3 = 0x10325476;
var H4 = 0xc3d2e1f0;
var A, B, C, D, E;
var temp;
msg = Utf8Encode(msg);
var msg_len = msg.length;
var word_array = new Array();
for (i = 0; i < msg_len - 3; i += 4) {
j = msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 | msg.charCodeAt(i
+ 2) << 8 | msg.charCodeAt(i + 3);
word_array.push(j);
}
switch (msg_len % 4) {
case 0:
i = 0x080000000;
break;
case 1:
i = msg.charCodeAt(msg_len - 1) << 24 | 0x0800000;
break;
case 2:
i = msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 |
0x08000;
break;
case 3:
i = msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 |
msg.charCodeAt(msg_len - 1) << 8 | 0x80;
break;}

word_array.push(i);
while (word_array.length % 16 != 14) word_array.push(0);
word_array.push(msg_len >>> 29);
word_array.push(msg_len << 3 & 0x0ffffffff);
for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14]
^ W[i - 16], 1);
A = H0;
B = H1;
C = H2;
D = H3;
E = H4;
for (i = 0; i <= 19; i++) {
temp = rotate_left(A, 5) + (B & C | ~B & D) + E + W[i] + 0x5a827999 &
0x0ffffffff;
E = D;
D = C;
C = rotate_left(B, 30);
B = A;
A = temp;
}
for (i = 20; i <= 39; i++) {
temp = rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ed9eba1 &
0x0ffffffff;
E = D;
D = C;
C = rotate_left(B, 30);
B = A;
A = temp;
}
for (i = 40; i <= 59; i++) {
temp = rotate_left(A, 5) + (B & C | B & D | C & D) + E + W[i] + 0x8f1bbcdc
& 0x0ffffffff;
E = D;
D = C;
C = rotate_left(B, 30);
B = A;
A = temp;
}
for (i = 60; i <= 79; i++) {
temp = rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xca62c1d6 &
0x0ffffffff;
E = D;
D = C;
C = rotate_left(B, 30);
B = A;
A = temp;
}
H0 = H0 + A & 0x0ffffffff;
H1 = H1 + B & 0x0ffffffff;
H2 = H2 + C & 0x0ffffffff;
H3 = H3 + D & 0x0ffffffff;
H4 = H4 + E & 0x0ffffffff;
}

return (cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) +


cvt_hex(H4)).toLowerCase();
}

let pageLoadedAndVisible = (() => {


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

window.addEventListener(pageLoadEventName, () => {
if (document.visibilityState === 'hidden') {
document.addEventListener('visibilitychange', ready, { once: true });
} else {
ready();
}
});

function ready() {
pageLoadedAndVisible.resolve();
pageLoadedAndVisible = new Deferred();
}

return new Deferred();


})();

const nativeJSONParse = window.JSON.parse;

const nativeXMLHttpRequestOpen = XMLHttpRequest.prototype.open;

// 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 :(
// To solve this problem the native defineProperty function will be retrieved
from another window (iframe)
const nativeObjectDefineProperty = (() => {
// Check if function is native
if (Object.defineProperty.toString().includes('[native code]')) {
return Object.defineProperty;
}

// If function is overidden, restore the native function from another window...


const tempFrame = createElement('iframe', { style: `display: none;` });
document.documentElement.append(tempFrame);

const native = tempFrame.contentWindow.Object.defineProperty;


tempFrame.remove();

return native;
})();

function getYtcfgValue(value) {var _window$ytcfg;


return (_window$ytcfg = window.ytcfg) === null || _window$ytcfg === void 0 ?
void 0 : _window$ytcfg.get(value);
}

function isUserLoggedIn() {
// Session Cookie exists?
if (!getSidCookie()) return false;

// LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID as


fallback
if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return
getYtcfgValue('LOGGED_IN');
if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;

return false;
}

function getPlayer$1(payload, requiresAuth) {


return sendInnertubeRequest('v1/player', payload, requiresAuth);
}

function getNext(payload) {
return sendInnertubeRequest('v1/next', payload, false);
}

function getSignatureTimestamp() {
return (
getYtcfgValue('STS') ||
(() => {var _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 sendInnertubeRequest(endpoint, payload, useAuth) {


const xmlhttp = new XMLHttpRequest();
xmlhttp.open('POST', `/youtubei/${endpoint}?key=$
{getYtcfgValue('INNERTUBE_API_KEY')}`, false);
if (useAuth && isUserLoggedIn()) {
xmlhttp.withCredentials = true;
xmlhttp.setRequestHeader('Authorization', generateSidBasedAuth());
}
xmlhttp.send(JSON.stringify(payload));
return nativeJSONParse(xmlhttp.responseText);
}

function getSidCookie() {
return getCookie('SAPISID') || getCookie('__Secure-3PAPISID');
}

function generateSidBasedAuth() {
const sid = getSidCookie();
const timestamp = Math.floor(new Date().getTime() / 1000);
const input = timestamp + ' ' + sid + ' ' + location.origin;
const hash = generateSha1Hash(input);
return `SAPISIDHASH ${timestamp}_${hash}`;
}

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

function info(msg) {
console.info(logPrefix, logPrefixStyle, 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}`;
}
}

let wrappedPlayerResponse;
let wrappedNextResponse;

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

// 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)) {
info("Another extension tries to redefine '" + prop + "' (probably an
AdBlock extension). Chain it...");

if (descriptor !== null && descriptor !== void 0 && descriptor.set)


chainedPlayerSetter = descriptor.set;
if (descriptor !== null && descriptor !== void 0 && descriptor.get)
chainedPlayerGetter = descriptor.get;
} else {
nativeObjectDefineProperty(obj, prop, descriptor);
}
};

// Redefine 'ytInitialPlayerResponse' to inspect and modify the initial player


response as soon as the variable is set on page load
nativeObjectDefineProperty(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 = isObject(playerResponse) ?
onInititalDataSet(playerResponse) : playerResponse;
if (typeof chainedPlayerSetter === 'function')
chainedPlayerSetter(wrappedPlayerResponse);
},
get: () => {
if (typeof chainedPlayerGetter === 'function')
try {
return chainedPlayerGetter();
} catch (err) {
// ignore the error
}
return wrappedPlayerResponse || {};
},
configurable: true });

// Also redefine 'ytInitialData' for the initial next/sidebar response


nativeObjectDefineProperty(window, 'ytInitialData', {
set: (nextResponse) => {
wrappedNextResponse = isObject(nextResponse) ?
onInititalDataSet(nextResponse) : nextResponse;
},
get: () => wrappedNextResponse,
configurable: true });

// Intercept, inspect and modify JSON-based communication to unlock player


responses by hijacking the JSON.parse function
function attachJsonInterceptor(onJsonDataReceived) {
window.JSON.parse = (text, reviver) => {
const data = nativeJSONParse(text, reviver);
return !isObject(data) ? data : onJsonDataReceived(data);
};
}
function attachXhrOpenInterceptor(onXhrOpenCalled) {
XMLHttpRequest.prototype.open = function (method, url) {
if (arguments.length > 1 && typeof url === 'string' &&
url.indexOf('https://') === 0) {
const modifiedUrl = onXhrOpenCalled(this, method, new URL(url));

if (typeof modifiedUrl === 'string') {


url = modifiedUrl;
}
}

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

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,


_playabilityStatus$er2, _playabilityStatus$er3, _playabilityStatus$er4,
_playabilityStatus$er5, _playabilityStatus$er6, _playabilityStatus$er7,
_playabilityStatus$er8;
if (!(playabilityStatus !== null && playabilityStatus !== void 0 &&
playabilityStatus.status)) return false;
if (playabilityStatus.desktopLegacyAgeGateReason) return true;
if (UNLOCKABLE_PLAYER_STATES.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 ?
void 0 : (_playabilityStatus$er2 =
_playabilityStatus$er.playerErrorMessageRenderer) === null ||
_playabilityStatus$er2 === void 0 ? void 0 : (_playabilityStatus$er3 =
_playabilityStatus$er2.reason) === null || _playabilityStatus$er3 === void 0 ? void
0 : (_playabilityStatus$er4 = _playabilityStatus$er3.runs) === null ||
_playabilityStatus$er4 === void 0 ? void 0 : (_playabilityStatus$er5 =
_playabilityStatus$er4.find((x) => x.navigationEndpoint)) === null ||
_playabilityStatus$er5 === void 0 ? void 0 : (_playabilityStatus$er6 =
_playabilityStatus$er5.navigationEndpoint) === null || _playabilityStatus$er6 ===
void 0 ? void 0 : (_playabilityStatus$er7 = _playabilityStatus$er6.urlEndpoint) ===
null || _playabilityStatus$er7 === void 0 ? void 0 : (_playabilityStatus$er8 =
_playabilityStatus$er7.url) === null || _playabilityStatus$er8 === void 0 ? void
0 : _playabilityStatus$er8.includes('/2802167')));

function isWatchNextObject(parsedData) {var _parsedData$currentVi,


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

function isWatchNextSidebarEmpty(parsedData) {var _parsedData$contents2,


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

// MWEB response layout


const content = (_parsedData$contents2 = parsedData.contents) === null ||
_parsedData$contents2 === void 0 ? void 0 : (_parsedData$contents3 =
_parsedData$contents2.singleColumnWatchNextResults) === null ||
_parsedData$contents3 === void 0 ? void 0 : (_parsedData$contents4 =
_parsedData$contents3.results) === null || _parsedData$contents4 === void 0 ? void
0 : (_parsedData$contents5 = _parsedData$contents4.results) === null ||
_parsedData$contents5 === void 0 ? void 0 : _parsedData$contents5.contents;
const result = content === null || content === void 0 ? 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 isGoogleVideo(method, url) {


return method === 'GET' && 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;


}

function isSearchResult(parsedData) {var _parsedData$contents6,


_parsedData$contents7, _parsedData$contents8, _parsedData$onRespons,
_parsedData$onRespons2, _parsedData$onRespons3;
return (
typeof (parsedData === null || parsedData === void 0 ? void 0 :
(_parsedData$contents6 = parsedData.contents) === null || _parsedData$contents6 ===
void 0 ? void 0 : _parsedData$contents6.twoColumnSearchResultsRenderer) ===
'object' || // Desktop initial results
(parsedData === null || parsedData === void 0 ? void 0 :
(_parsedData$contents7 = parsedData.contents) === null || _parsedData$contents7 ===
void 0 ? void 0 : (_parsedData$contents8 =
_parsedData$contents7.sectionListRenderer) === null || _parsedData$contents8 ===
void 0 ? void 0 : _parsedData$contents8.targetId) === 'search-feed' || // Mobile
initial results
(parsedData === null || parsedData === void 0 ? void 0 :
(_parsedData$onRespons = parsedData.onResponseReceivedCommands) === null ||
_parsedData$onRespons === void 0 ? void 0 : (_parsedData$onRespons2 =
_parsedData$onRespons.find((x) => x.appendContinuationItemsAction)) === null ||
_parsedData$onRespons2 === void 0 ? void 0 : (_parsedData$onRespons3 =
_parsedData$onRespons2.appendContinuationItemsAction) === null ||
_parsedData$onRespons3 === void 0 ? void 0 : _parsedData$onRespons3.targetId) ===
'search-feed' // Desktop & Mobile scroll continuation
);
}

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

function getPlayer(payload) {
const queryParams = new URLSearchParams(payload).toString();

const proxyUrl = ACCOUNT_PROXY_SERVER_HOST + '/getPlayer?' + queryParams;

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

const playerResponse = nativeJSONParse(xmlhttp.responseText);

// mark request as 'proxied'


playerResponse.proxied = true;

return playerResponse;
} catch (err) {
error(err);
return { errorMessage: 'Proxy Connection failed' };
}
}

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

var tMobile = "<c3-toast>\r\n <ytm-notification-action-renderer>\r\n


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

const template = isDesktop ? tDesktop : tMobile;

const nToastContainer = createElement('div', { id: 'toast-container', innerHTML:


template });
const nToast = nToastContainer.querySelector(':scope > *');
document.documentElement.append(nToastContainer);

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) {let duration = arguments.length > 1 && arguments[1]


!== undefined ? arguments[1] : 5;

await pageLoadedAndVisible;

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 lastProxiedGoogleVideoUrlParams;
let responseCache = {};

function getUnlockStrategies(playerResponse) {var _playerResponse$video,


_playerResponse$playa, _playerResponse$previ;
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);
const clientName = isEmbed || isDesktop ? 'WEB' : 'MWEB';
const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION');
const signatureTimestamp = getSignatureTimestamp();

return [
// 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
{
name: 'Embed',
requiresAuth: false,
payload: {
context: {
client: {
clientName,
clientVersion,
clientScreen: 'EMBED' },
thirdParty: {
embedUrl: 'https://www.youtube.com/' } },

playbackContext: {
contentPlaybackContext: {
signatureTimestamp } },

videoId },

getPlayer: getPlayer$1 },

// Strategy 2: Retrieve the video info by using the WEB_CREATOR client in


combination with user authentication
// 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',
thirdParty: {
embedUrl: 'https://www.youtube.com/' } } },

playbackContext: {
contentPlaybackContext: {
signatureTimestamp } },

videoId },

getPlayer: getPlayer$1 },

// Strategy 3: Retrieve the video info from an account proxy server.


// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/
tree/main/account-proxy
{
name: 'Account Proxy',
requireAuth: false,
payload: {
videoId,
reason,
clientName,
clientVersion,
signatureTimestamp,
isEmbed: +isEmbed },

getPlayer: getPlayer }];

function getLastProxiedGoogleVideoId() {var _lastProxiedGoogleVid;


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

function unlockPlayerResponse(playerResponse) {var _unlockedPlayerRespon,


_unlockedPlayerRespon3;
const unlockedPlayerResponse = getUnlockedPlayerResponse(playerResponse);

// 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 (((_unlockedPlayerRespon = unlockedPlayerResponse.playabilityStatus) ===
null || _unlockedPlayerRespon === void 0 ? void 0 : _unlockedPlayerRespon.status) !
== 'OK') {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


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

Toast.show(messagesMap.success);
}

function getUnlockedPlayerResponse(playerResponse) {var _playerResponse$video2;


const videoId = ((_playerResponse$video2 = playerResponse.videoDetails) ===
null || _playerResponse$video2 === void 0 ? void 0 :
_playerResponse$video2.videoId) || getYtcfgValue('PLAYER_VARS').video_id;

// Check if response is cached


if (responseCache.videoId === videoId) return
responseCache.unlockedPlayerResponse;

const unlockStrategies = getUnlockStrategies(playerResponse);

let unlockedPlayerResponse;

// Try every strategy until one of them works


unlockStrategies.every((strategy, index) => {var _unlockedPlayerRespon6,
_unlockedPlayerRespon7;
// Skip strategy if authentication is required and the user is not logged in
if (strategy.requiresAuth && !isUserLoggedIn()) return true;

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

unlockedPlayerResponse = strategy.getPlayer(strategy.payload,
strategy.requiresAuth);

return ((_unlockedPlayerRespon6 = unlockedPlayerResponse) === null ||


_unlockedPlayerRespon6 === void 0 ? void 0 : (_unlockedPlayerRespon7 =
_unlockedPlayerRespon6.playabilityStatus) === null || _unlockedPlayerRespon7 ===
void 0 ? void 0 : _unlockedPlayerRespon7.status) !== 'OK';
});

// Cache response to prevent a flood of requests in case youtube processes a


blocked response mutiple times.
responseCache = { videoId, unlockedPlayerResponse };

return unlockedPlayerResponse;
}

function unlockNextResponse(originalNextResponse) {
info('Trying sidebar unlock');

const { videoId } = originalNextResponse.currentVideoEndpoint.watchEndpoint;


const { clientName, clientVersion } =
getYtcfgValue('INNERTUBE_CONTEXT').client;
const payload = {
context: {
client: {
clientName,
clientVersion,
clientScreen: 'EMBED' } },

videoId };

const unlockedNextResponse = getNext(payload);

// 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 mergeNextResponse(originalNextResponse, unlockedNextResponse) {var


_unlockedNextResponse, _unlockedNextResponse2, _unlockedNextResponse3,
_unlockedNextResponse4, _unlockedNextResponse5;
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.fi
nd(
(x) => x.videoSecondaryInfoRenderer).
videoSecondaryInfoRenderer;
const unlockedVideoSecondaryInfoRenderer =
unlockedNextResponse.contents.twoColumnWatchNextResults.results.results.contents.fi
nd(
(x) => x.videoSecondaryInfoRenderer).
videoSecondaryInfoRenderer;

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

return;
}

// Transfer WatchNextResults to original response


const unlockedWatchNextFeed = (_unlockedNextResponse =
unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0 ? void
0 : (_unlockedNextResponse2 = _unlockedNextResponse.singleColumnWatchNextResults)
=== null || _unlockedNextResponse2 === void 0 ? void 0 : (_unlockedNextResponse3 =
_unlockedNextResponse2.results) === null || _unlockedNextResponse3 === void 0 ?
void 0 : (_unlockedNextResponse4 = _unlockedNextResponse3.results) === null ||
_unlockedNextResponse4 === void 0 ? void 0 : (_unlockedNextResponse5 =
_unlockedNextResponse4.contents) === null || _unlockedNextResponse5 === void 0 ?
void 0 : _unlockedNextResponse5.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.ite
ms.find((x) => x.expandableVideoDescriptionBodyRenderer);
const unlockedStructuredDescriptionContentRenderer =
unlockedNextResponse.engagementPanels.
find((x) => x.engagementPanelSectionListRenderer).

engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.ite
ms.find((x) => x.expandableVideoDescriptionBodyRenderer);

if
(unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRendere
r)

originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer
=
unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer
;
}

function processThumbnails(responseObject) {
const thumbnails = findNestedObjectsByAttributeNames(responseObject, ['url',
'height']).filter((x) => typeof x.url === 'string' &&
x.url.indexOf('https://i.ytimg.com/') === 0);
const blurredThumbnails = thumbnails.filter((thumbnail) =>
THUMBNAIL_BLURRED_SQPS.some((sqp) => thumbnail.url.includes(sqp)));

// Simply remove all URL parameters to eliminate the blur effect.


blurredThumbnails.forEach((x) => x.url = x.url.split('?')[0]);

info(blurredThumbnails.length + '/' + thumbnails.length + ' thumbnails detected


as blurred.');
}

try {
attachJsonInterceptor(checkAndUnlock);
attachXhrOpenInterceptor(onXhrOpenCalled);
attachInitialDataInterceptor(checkAndUnlock);
} catch (err) {
error(err, 'Error while attaching data interceptors');
}

function checkAndUnlock(ytData) {
try {
// Unlock #1: Initial page data structure and response from the
'/youtubei/v1/player' endpoint
if (isPlayerObject(ytData) && isAgeRestricted(ytData.playabilityStatus)) {
unlockPlayerResponse(ytData);
}
// Unlock #2: Legacy response data structure (only used by m.youtube.com with
&pbj=1)
else if (isPlayerObject(ytData.playerResponse) &&
isAgeRestricted(ytData.playerResponse.playabilityStatus)) {
unlockPlayerResponse(ytData.playerResponse);
}
// Unlock #3: Embedded Player inital data structure
else if (isEmbeddedPlayerObject(ytData) &&
isAgeRestricted(ytData.previewPlayabilityStatus)) {
unlockPlayerResponse(ytData);
}
// Equivelant of unlock #1 for sidebar/next response
else if (isWatchNextObject(ytData) && isWatchNextSidebarEmpty(ytData)) {
unlockNextResponse(ytData);
}
// Equivelant of unlock #2 for sidebar/next response
else if (isWatchNextObject(ytData.response) &&
isWatchNextSidebarEmpty(ytData.response)) {
unlockNextResponse(ytData.response);
}
} catch (err) {
error(err, 'Video or sidebar unlock failed');
}

try {
// Unlock blurry video thumbnails
if (isSearchResult(ytData) || isSearchResult(ytData.response)) {
processThumbnails(ytData);
}
} catch (err) {
error(err, 'Thumbnail unlock failed');
}

return ytData;
}

function onXhrOpenCalled(xhr, method, url) {


if (!isGoogleVideo(method, url)) return;

if (isGoogleVideoUnlockRequired(url, getLastProxiedGoogleVideoId())) {
// 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...

// solve CORS errors by preventing YouTube from enabling the


"withCredentials" option (required for the proxy)
Object.defineProperty(xhr, 'withCredentials', {
set: () => {},
get: () => false });

return getGoogleVideoUrl(url.toString());
}
}

})(true);

You might also like