返回首頁 

Greasy Fork is available in English.

Simple YouTube Age Restriction Bypass

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

  1. // ==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 matchif (this !== window && !ranOnce) {window.eval('(' + iife.toString() + ')(true);');return;}// Script configuration variablesconst 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#privacyconst 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 Shorts84, // Desktop Playlist (LONG)88, // Desktop (LONG)];// small hack to prevent tree shaking on these exportsvar 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 r###lts = [];// Does the current object match the attribute conditions?if (attributeNames.every((key) => typeof object[key] !== 'undefined')) {r###lts.push(object);}// Diggin' deeper for each nested object (recursive)Object.keys(object).forEach((key) => {if (object[key] && typeof object[key] === 'object') {r###lts.push(...findNestedObjectsByAttributeNames(object[key], attributeNames));}});return r###lts;}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 fallbackif (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 playerconst 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.twoColumnWatchNextR###lts || !!parsedData.contents.singleColumnWatchNextR###lts;}function isWatchNextSidebarEmpty(parsedData) {var _parsedData$contents2, _content$find;if (isDesktop) {var _parsedData$contents;// WEB response layoutconst r###lt = (_parsedData$contents = parsedData.contents) === null || _parsedData$contents === void 0|| (_parsedData$contents = _parsedData$contents.twoColumnWatchNextR###lts) === null || _parsedData$contents === void 0|| (_parsedData$contents = _parsedData$contents.secondaryR###lts) === null || _parsedData$contents === void 0|| (_parsedData$contents = _parsedData$contents.secondaryR###lts) === null || _parsedData$contents === void 0? void 0: _parsedData$contents.r###lts;return !r###lt;}// MWEB response layoutconst content = (_parsedData$contents2 = parsedData.contents) === null || _parsedData$contents2 === void 0|| (_parsedData$contents2 = _parsedData$contents2.singleColumnWatchNextR###lts) === null || _parsedData$contents2 === void 0|| (_parsedData$contents2 = _parsedData$contents2.r###lts) === null || _parsedData$contents2 === void 0|| (_parsedData$contents2 = _parsedData$contents2.r###lts) === null || _parsedData$contents2 === void 0? void 0: _parsedData$contents2.contents;const r###lt = 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 r###lt !== '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-946853553return (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 isSearchR###lt(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.twoColumnSearchR###ltsRenderer) === 'object' // Desktop initial r###lts|| (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 r###lts|| (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 descriptionif (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 r###lts, sidebar data and meta information// Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127window.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 similarconst 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 functionfunction 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 contentif (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 cacheif (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 controlsif (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 backgroundif (document.visibilityState === 'hidden') return;// Append toast container to DOM, if not already doneif (!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 firstif (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 playableif (!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 oneif (playerResponse.previewPlayabilityStatus) {playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;}// Transfer all unlocked properties to the original player responseObject.assign(playerResponse, unlockedPlayerResponse);playerResponse.unlocked = true;Toast.show(messagesMap.success);}function getUnlockedPlayerResponse(videoId, reason) {// Check if response is cachedif (cachedPlayerResponse.videoId === videoId) return createDeepCopy(cachedPlayerResponse);const unlockStrategies = getUnlockStrategies(videoId, reason);let unlockedPlayerResponse = {};// Try every strategy until one of them worksunlockStrategies.every((strategy, index) => {var _unlockedPlayerRespon6;// Skip strategy if authentication is required and the user is not logged inif (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 wellif (videoId !== lastPlayerUnlockVideoId) {return;}const unlockedNextResponse = getUnlockedNextResponse(videoId);// check if the sidebar of the unlocked response is still emptyif (isWatchNextSidebarEmpty(unlockedNextResponse)) {throw new Error(`Sidebar Unlock Failed`);}// Transfer some parts of the unlocked response to the original responsemergeNextResponse(originalNextResponse, unlockedNextResponse);}function getUnlockedNextResponse(videoId) {// Check if response is cachedif (cachedNextResponse.videoId === videoId) return createDeepCopy(cachedNextResponse);const unlockStrategies = getUnlockStrategies$1(videoId, lastPlayerUnlockReason);let unlockedNextResponse = {};// Try every strategy until one of them worksunlockStrategies.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 WatchNextR###lts to original responseoriginalNextResponse.contents.twoColumnWatchNextR###lts.secondaryR###lts = unlockedNextResponse.contents.twoColumnWatchNextR###lts.secondaryR###lts;// Transfer video description to original responseconst originalVideoSecondaryInfoRenderer = originalNextResponse.contents.twoColumnWatchNextR###lts.r###lts.r###lts.contents.find((x) => x.videoSecondaryInfoRenderer,).videoSecondaryInfoRenderer;const unlockedVideoSecondaryInfoRenderer = unlockedNextResponse.contents.twoColumnWatchNextR###lts.r###lts.r###lts.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 WatchNextR###lts to original responseconst unlockedWatchNextFeed = (_unlockedNextResponse = unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0|| (_unlockedNextResponse = _unlockedNextResponse.singleColumnWatchNextR###lts) === null || _unlockedNextResponse === void 0|| (_unlockedNextResponse = _unlockedNextResponse.r###lts) === null || _unlockedNextResponse === void 0|| (_unlockedNextResponse = _unlockedNextResponse.r###lts) === 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.singleColumnWatchNextR###lts.r###lts.r###lts.contents.push(unlockedWatchNextFeed);// Transfer video description to original responseconst 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 XMLHttpRequestObject.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 Requestif (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 requestif (isPlayerObject(ytData) && isAgeRestricted(ytData.playabilityStatus)) {unlockResponse$1(ytData);} // Player Unlock #2: Embedded Player inital data structureelse 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 descriptionif (isWatchNextObject(ytData) && isWatchNextSidebarEmpty(ytData)) {unlockResponse(ytData);}// Mobile versionif (isWatchNextObject(ytData.response) && isWatchNextSidebarEmpty(ytData.response)) {unlockResponse(ytData.response);}} catch (err) {error(err, 'Sidebar unlock failed');}try {// Unlock blurry video thumbnails in search r###ltsif (isSearchR###lt(ytData)) {processThumbnails(ytData);}} catch (err) {error(err, 'Thumbnail unlock failed');}return ytData;}})();