Simple YouTube Age Restriction Bypass

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

  1. // ==UserScript==
  2. // @name Simple YouTube Age Restriction Bypass
  3. // @description Watch age restricted videos on YouTube without login and without age verification 😎
  4. // @description:de Schaue YouTube Videos mit Altersbeschränkungen ohne Anmeldung und ohne dein Alter zu bestätigen 😎
  5. // @description:fr Regardez des vidéos YouTube avec des restrictions d'âge sans vous inscrire et sans confirmer votre âge 😎
  6. // @description:it Guarda i video con restrizioni di età su YouTube senza login e senza verifica dell'età 😎
  7. // @icon https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/raw/v2.5.4/src/extension/icon/icon_64.png
  8. // @version 2.5.10
  9. // @author Zerody (https://github.com/zerodytrash)
  10. // @namespace https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/
  11. // @supportURL https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues
  12. // @license MIT
  13. // @match https://www.youtube.com/*
  14. // @match https://www.youtube-nocookie.com/*
  15. // @match https://m.youtube.com/*
  16. // @match https://music.youtube.com/*
  17. // @grant none
  18. // @run-at document-start
  19. // @compatible chrome
  20. // @compatible firefox
  21. // @compatible opera
  22. // @compatible edge
  23. // @compatible safari
  24. // ==/UserScript==
  25.  
  26. /*
  27. This is a transpiled version to achieve a clean code base and better browser compatibility.
  28. You can find the nicely readable source code at https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass
  29. */
  30.  
  31. (function iife(ranOnce) {
  32. // Trick to get around the sandbox restrictions in Greasemonkey (Firefox)
  33. // Inject code into the main window if criteria match
  34. if (this !== window && !ranOnce) {
  35. window.eval('(' + iife.toString() + ')(true);');
  36. return;
  37. }
  38.  
  39. // Script configuration variables
  40. const UNLOCKABLE_PLAYABILITY_STATUSES = ['AGE_VERIFICATION_REQUIRED', 'AGE_CHECK_REQUIRED', 'CONTENT_CHECK_REQUIRED', 'LOGIN_REQUIRED'];
  41. const VALID_PLAYABILITY_STATUSES = ['OK', 'LIVE_STREAM_OFFLINE'];
  42.  
  43. // These are the proxy servers that are sometimes required to unlock videos with age restrictions.
  44. // You can host your own account proxy instance. See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
  45. // To learn what information is transferred, please read: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#privacy
  46. const ACCOUNT_PROXY_SERVER_HOST = 'https://youtube-proxy.zerody.one';
  47. const VIDEO_PROXY_SERVER_HOST = 'https://ny.4everproxy.com';
  48.  
  49. // User needs to confirm the unlock process on embedded player?
  50. let ENABLE_UNLOCK_CONFIRMATION_EMBED = true;
  51.  
  52. // Show notification?
  53. let ENABLE_UNLOCK_NOTIFICATION = true;
  54.  
  55. // Disable content warnings?
  56. let SKIP_CONTENT_WARNINGS = true;
  57.  
  58. // Some Innertube bypass methods require the following authentication headers of the currently logged in user.
  59. const GOOGLE_AUTH_HEADER_NAMES = ['Authorization', 'X-Goog-AuthUser', 'X-Origin'];
  60.  
  61. /**
  62. * The SQP parameter length is different for blurred thumbnails.
  63. * They contain much less information, than normal thumbnails.
  64. * The thumbnail SQPs tend to have a long and a short version.
  65. */
  66. const BLURRED_THUMBNAIL_SQP_LENGTHS = [
  67. 32, // Mobile (SHORT)
  68. 48, // Desktop Playlist (SHORT)
  69. 56, // Desktop (SHORT)
  70. 68, // Mobile (LONG)
  71. 72, // Mobile Shorts
  72. 84, // Desktop Playlist (LONG)
  73. 88, // Desktop (LONG)
  74. ];
  75.  
  76. // small hack to prevent tree shaking on these exports
  77. var Config = window[Symbol()] = {
  78. UNLOCKABLE_PLAYABILITY_STATUSES,
  79. VALID_PLAYABILITY_STATUSES,
  80. ACCOUNT_PROXY_SERVER_HOST,
  81. VIDEO_PROXY_SERVER_HOST,
  82. ENABLE_UNLOCK_CONFIRMATION_EMBED,
  83. ENABLE_UNLOCK_NOTIFICATION,
  84. SKIP_CONTENT_WARNINGS,
  85. GOOGLE_AUTH_HEADER_NAMES,
  86. BLURRED_THUMBNAIL_SQP_LENGTHS,
  87. };
  88.  
  89. function isGoogleVideoUrl(url) {
  90. return url.host.includes('.googlevideo.com');
  91. }
  92.  
  93. function isGoogleVideoUnlockRequired(googleVideoUrl, lastProxiedGoogleVideoId) {
  94. const urlParams = new URLSearchParams(googleVideoUrl.search);
  95. const hasGcrFlag = urlParams.get('gcr');
  96. const wasUnlockedByAccountProxy = urlParams.get('id') === lastProxiedGoogleVideoId;
  97.  
  98. return hasGcrFlag && wasUnlockedByAccountProxy;
  99. }
  100.  
  101. const nativeJSONParse = window.JSON.parse;
  102. const nativeXMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;
  103.  
  104. const isDesktop = window.location.host !== 'm.youtube.com';
  105. const isMusic = window.location.host === 'music.youtube.com';
  106. const isEmbed = window.location.pathname.indexOf('/embed/') === 0;
  107. const isConfirmed = window.location.search.includes('unlock_confirmed');
  108.  
  109. class Deferred {
  110. constructor() {
  111. return Object.assign(
  112. new Promise((resolve, reject) => {
  113. this.resolve = resolve;
  114. this.reject = reject;
  115. }),
  116. this,
  117. );
  118. }
  119. }
  120.  
  121. // WORKAROUND: TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
  122. if (window.trustedTypes && trustedTypes.createPolicy) {
  123. if (!trustedTypes.defaultPolicy) {
  124. const passThroughFn = (x) => x;
  125. trustedTypes.createPolicy('default', {
  126. createHTML: passThroughFn,
  127. createScriptURL: passThroughFn,
  128. createScript: passThroughFn,
  129. });
  130. }
  131. }
  132.  
  133. function createElement(tagName, options) {
  134. const node = document.createElement(tagName);
  135. options && Object.assign(node, options);
  136. return node;
  137. }
  138.  
  139. function isObject(obj) {
  140. return obj !== null && typeof obj === 'object';
  141. }
  142.  
  143. function findNestedObjectsByAttributeNames(object, attributeNames) {
  144. var results = [];
  145.  
  146. // Does the current object match the attribute conditions?
  147. if (attributeNames.every((key) => typeof object[key] !== 'undefined')) {
  148. results.push(object);
  149. }
  150.  
  151. // Diggin' deeper for each nested object (recursive)
  152. Object.keys(object).forEach((key) => {
  153. if (object[key] && typeof object[key] === 'object') {
  154. results.push(...findNestedObjectsByAttributeNames(object[key], attributeNames));
  155. }
  156. });
  157.  
  158. return results;
  159. }
  160.  
  161. function pageLoaded() {
  162. if (document.readyState === 'complete') return Promise.resolve();
  163.  
  164. const deferred = new Deferred();
  165.  
  166. window.addEventListener('load', deferred.resolve, { once: true });
  167.  
  168. return deferred;
  169. }
  170.  
  171. function createDeepCopy(obj) {
  172. return nativeJSONParse(JSON.stringify(obj));
  173. }
  174.  
  175. function getYtcfgValue(name) {
  176. var _window$ytcfg;
  177. return (_window$ytcfg = window.ytcfg) === null || _window$ytcfg === void 0 ? void 0 : _window$ytcfg.get(name);
  178. }
  179.  
  180. function getSignatureTimestamp() {
  181. return (
  182. getYtcfgValue('STS')
  183. || ((_document$querySelect) => {
  184. // STS is missing on embedded player. Retrieve from player base script as fallback...
  185. const playerBaseJsPath = (_document$querySelect = document.querySelector('script[src*="/base.js"]')) === null || _document$querySelect === void 0
  186. ? void 0
  187. : _document$querySelect.src;
  188.  
  189. if (!playerBaseJsPath) return;
  190.  
  191. const xmlhttp = new XMLHttpRequest();
  192. xmlhttp.open('GET', playerBaseJsPath, false);
  193. xmlhttp.send(null);
  194.  
  195. return parseInt(xmlhttp.responseText.match(/signatureTimestamp:([0-9]*)/)[1]);
  196. })()
  197. );
  198. }
  199.  
  200. function isUserLoggedIn() {
  201. // LOGGED_IN doesn't exist on embedded page, use DELEGATED_SESSION_ID or SESSION_INDEX as fallback
  202. if (typeof getYtcfgValue('LOGGED_IN') === 'boolean') return getYtcfgValue('LOGGED_IN');
  203. if (typeof getYtcfgValue('DELEGATED_SESSION_ID') === 'string') return true;
  204. if (parseInt(getYtcfgValue('SESSION_INDEX')) >= 0) return true;
  205.  
  206. return false;
  207. }
  208.  
  209. function getCurrentVideoStartTime(currentVideoId) {
  210. // Check if the URL corresponds to the requested video
  211. // This is not the case when the player gets preloaded for the next video in a playlist.
  212. if (window.location.href.includes(currentVideoId)) {
  213. var _ref;
  214. // "t"-param on youtu.be urls
  215. // "start"-param on embed player
  216. // "time_continue" when clicking "watch on youtube" on embedded player
  217. const urlParams = new URLSearchParams(window.location.search);
  218. const startTimeString = (_ref = urlParams.get('t') || urlParams.get('start') || urlParams.get('time_continue')) === null || _ref === void 0
  219. ? void 0
  220. : _ref.replace('s', '');
  221.  
  222. if (startTimeString && !isNaN(startTimeString)) {
  223. return parseInt(startTimeString);
  224. }
  225. }
  226.  
  227. return 0;
  228. }
  229.  
  230. function setUrlParams(params) {
  231. const urlParams = new URLSearchParams(window.location.search);
  232. for (const paramName in params) {
  233. urlParams.set(paramName, params[paramName]);
  234. }
  235. window.location.search = urlParams;
  236. }
  237.  
  238. function waitForElement(elementSelector, timeout) {
  239. const deferred = new Deferred();
  240.  
  241. const checkDomInterval = setInterval(() => {
  242. const elem = document.querySelector(elementSelector);
  243. if (elem) {
  244. clearInterval(checkDomInterval);
  245. deferred.resolve(elem);
  246. }
  247. }, 100);
  248.  
  249. {
  250. setTimeout(() => {
  251. clearInterval(checkDomInterval);
  252. deferred.reject();
  253. }, timeout);
  254. }
  255.  
  256. return deferred;
  257. }
  258.  
  259. function isWatchNextObject(parsedData) {
  260. var _parsedData$currentVi;
  261. if (
  262. !(parsedData !== null && parsedData !== void 0 && parsedData.contents)
  263. || !(parsedData !== null && parsedData !== void 0 && (_parsedData$currentVi = parsedData.currentVideoEndpoint) !== null && _parsedData$currentVi !== void 0
  264. && (_parsedData$currentVi = _parsedData$currentVi.watchEndpoint) !== null && _parsedData$currentVi !== void 0 && _parsedData$currentVi.videoId)
  265. ) return false;
  266. return !!parsedData.contents.twoColumnWatchNextResults || !!parsedData.contents.singleColumnWatchNextResults;
  267. }
  268.  
  269. function isWatchNextSidebarEmpty(parsedData) {
  270. var _parsedData$contents2, _content$find;
  271. if (isDesktop) {
  272. var _parsedData$contents;
  273. // WEB response layout
  274. const result = (_parsedData$contents = parsedData.contents) === null || _parsedData$contents === void 0
  275. || (_parsedData$contents = _parsedData$contents.twoColumnWatchNextResults) === null || _parsedData$contents === void 0
  276. || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
  277. || (_parsedData$contents = _parsedData$contents.secondaryResults) === null || _parsedData$contents === void 0
  278. ? void 0
  279. : _parsedData$contents.results;
  280. return !result;
  281. }
  282.  
  283. // MWEB response layout
  284. const content = (_parsedData$contents2 = parsedData.contents) === null || _parsedData$contents2 === void 0
  285. || (_parsedData$contents2 = _parsedData$contents2.singleColumnWatchNextResults) === null || _parsedData$contents2 === void 0
  286. || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
  287. || (_parsedData$contents2 = _parsedData$contents2.results) === null || _parsedData$contents2 === void 0
  288. ? void 0
  289. : _parsedData$contents2.contents;
  290. const result = content === null || content === void 0 || (_content$find = content.find((e) => {
  291. var _e$itemSectionRendere;
  292. return ((_e$itemSectionRendere = e.itemSectionRenderer) === null || _e$itemSectionRendere === void 0 ? void 0 : _e$itemSectionRendere.targetId)
  293. === 'watch-next-feed';
  294. })) === null
  295. || _content$find === void 0
  296. ? void 0
  297. : _content$find.itemSectionRenderer;
  298. return typeof result !== 'object';
  299. }
  300.  
  301. function isPlayerObject(parsedData) {
  302. return (parsedData === null || parsedData === void 0 ? void 0 : parsedData.videoDetails)
  303. && (parsedData === null || parsedData === void 0 ? void 0 : parsedData.playabilityStatus);
  304. }
  305.  
  306. function isEmbeddedPlayerObject(parsedData) {
  307. return typeof (parsedData === null || parsedData === void 0 ? void 0 : parsedData.previewPlayabilityStatus) === 'object';
  308. }
  309.  
  310. function isAgeRestricted(playabilityStatus) {
  311. var _playabilityStatus$er;
  312. if (!(playabilityStatus !== null && playabilityStatus !== void 0 && playabilityStatus.status)) return false;
  313. if (playabilityStatus.desktopLegacyAgeGateReason) return true;
  314. if (Config.UNLOCKABLE_PLAYABILITY_STATUSES.includes(playabilityStatus.status)) return true;
  315.  
  316. // Fix to detect age restrictions on embed player
  317. // see https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/85#issuecomment-946853553
  318. return (
  319. isEmbed
  320. && ((_playabilityStatus$er = playabilityStatus.errorScreen) === null || _playabilityStatus$er === void 0
  321. || (_playabilityStatus$er = _playabilityStatus$er.playerErrorMessageRenderer) === null || _playabilityStatus$er === void 0
  322. || (_playabilityStatus$er = _playabilityStatus$er.reason) === null || _playabilityStatus$er === void 0
  323. || (_playabilityStatus$er = _playabilityStatus$er.runs) === null || _playabilityStatus$er === void 0
  324. || (_playabilityStatus$er = _playabilityStatus$er.find((x) => x.navigationEndpoint)) === null || _playabilityStatus$er === void 0
  325. || (_playabilityStatus$er = _playabilityStatus$er.navigationEndpoint) === null || _playabilityStatus$er === void 0
  326. || (_playabilityStatus$er = _playabilityStatus$er.urlEndpoint) === null || _playabilityStatus$er === void 0
  327. || (_playabilityStatus$er = _playabilityStatus$er.url) === null || _playabilityStatus$er === void 0
  328. ? void 0
  329. : _playabilityStatus$er.includes('/2802167'))
  330. );
  331. }
  332.  
  333. function isSearchResult(parsedData) {
  334. var _parsedData$contents3, _parsedData$contents4, _parsedData$onRespons;
  335. return (
  336. typeof (parsedData === null || parsedData === void 0 || (_parsedData$contents3 = parsedData.contents) === null || _parsedData$contents3 === void 0
  337. ? void 0
  338. : _parsedData$contents3.twoColumnSearchResultsRenderer) === 'object' // Desktop initial results
  339. || (parsedData === null || parsedData === void 0 || (_parsedData$contents4 = parsedData.contents) === null || _parsedData$contents4 === void 0
  340. || (_parsedData$contents4 = _parsedData$contents4.sectionListRenderer) === null || _parsedData$contents4 === void 0
  341. ? void 0
  342. : _parsedData$contents4.targetId) === 'search-feed' // Mobile initial results
  343. || (parsedData === null || parsedData === void 0 || (_parsedData$onRespons = parsedData.onResponseReceivedCommands) === null || _parsedData$onRespons === void 0
  344. || (_parsedData$onRespons = _parsedData$onRespons.find((x) => x.appendContinuationItemsAction)) === null || _parsedData$onRespons === void 0
  345. || (_parsedData$onRespons = _parsedData$onRespons.appendContinuationItemsAction) === null || _parsedData$onRespons === void 0
  346. ? void 0
  347. : _parsedData$onRespons.targetId) === 'search-feed' // Desktop & Mobile scroll continuation
  348. );
  349. }
  350.  
  351. function attach$4(obj, prop, onCall) {
  352. if (!obj || typeof obj[prop] !== 'function') {
  353. return;
  354. }
  355.  
  356. let original = obj[prop];
  357.  
  358. obj[prop] = function() {
  359. try {
  360. onCall(arguments);
  361. } catch {}
  362. original.apply(this, arguments);
  363. };
  364. }
  365.  
  366. const logPrefix = '%cSimple-YouTube-Age-Restriction-Bypass:';
  367. const logPrefixStyle = 'background-color: #1e5c85; color: #fff; font-size: 1.2em;';
  368. const logSuffix = '\uD83D\uDC1E You can report bugs at: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues';
  369.  
  370. function error(err, msg) {
  371. console.error(logPrefix, logPrefixStyle, msg, err, getYtcfgDebugString(), '\n\n', logSuffix);
  372. if (window.SYARB_CONFIG) {
  373. window.dispatchEvent(
  374. new CustomEvent('SYARB_LOG_ERROR', {
  375. detail: {
  376. message: (msg ? msg + '; ' : '') + (err && err.message ? err.message : ''),
  377. stack: err && err.stack ? err.stack : null,
  378. },
  379. }),
  380. );
  381. }
  382. }
  383.  
  384. function info(msg) {
  385. console.info(logPrefix, logPrefixStyle, msg);
  386. if (window.SYARB_CONFIG) {
  387. window.dispatchEvent(
  388. new CustomEvent('SYARB_LOG_INFO', {
  389. detail: {
  390. message: msg,
  391. },
  392. }),
  393. );
  394. }
  395. }
  396.  
  397. function getYtcfgDebugString() {
  398. try {
  399. return (
  400. `InnertubeConfig: `
  401. + `innertubeApiKey: ${getYtcfgValue('INNERTUBE_API_KEY')} `
  402. + `innertubeClientName: ${getYtcfgValue('INNERTUBE_CLIENT_NAME')} `
  403. + `innertubeClientVersion: ${getYtcfgValue('INNERTUBE_CLIENT_VERSION')} `
  404. + `loggedIn: ${getYtcfgValue('LOGGED_IN')} `
  405. );
  406. } catch (err) {
  407. return `Failed to access config: ${err}`;
  408. }
  409. }
  410.  
  411. /**
  412. * And here we deal with YouTube's crappy initial data (present in page source) and the problems that occur when intercepting that data.
  413. * YouTube has some protections in place that make it difficult to intercept and modify the global ytInitialPlayerResponse variable.
  414. * The easiest way would be to set a descriptor on that variable to change the value directly on declaration.
  415. * But some adblockers define their own descriptors on the ytInitialPlayerResponse variable, which makes it hard to register another descriptor on it.
  416. * As a workaround only the relevant playerResponse property of the ytInitialPlayerResponse variable will be intercepted.
  417. * This is achieved by defining a descriptor on the object prototype for that property, which affects any object with a `playerResponse` property.
  418. */
  419. function attach$3(onInitialData) {
  420. interceptObjectProperty('playerResponse', (obj, playerResponse) => {
  421. info(`playerResponse property set, contains sidebar: ${!!obj.response}`);
  422.  
  423. // The same object also contains the sidebar data and video description
  424. if (isObject(obj.response)) onInitialData(obj.response);
  425.  
  426. // If the script is executed too late and the bootstrap data has already been processed,
  427. // a reload of the player can be forced by creating a deep copy of the object.
  428. // This is especially relevant if the userscript manager does not handle the `@run-at document-start` correctly.
  429. playerResponse.unlocked = false;
  430. onInitialData(playerResponse);
  431. return playerResponse.unlocked ? createDeepCopy(playerResponse) : playerResponse;
  432. });
  433.  
  434. // The global `ytInitialData` variable can be modified on the fly.
  435. // It contains search results, sidebar data and meta information
  436. // Not really important but fixes https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/127
  437. window.addEventListener('DOMContentLoaded', () => {
  438. if (isObject(window.ytInitialData)) {
  439. onInitialData(window.ytInitialData);
  440. }
  441. });
  442. }
  443.  
  444. function interceptObjectProperty(prop, onSet) {
  445. var _Object$getOwnPropert;
  446. // Allow other userscripts to decorate this descriptor, if they do something similar
  447. const dataKey = '__SYARB_' + prop;
  448. const { get: getter, set: setter } = (_Object$getOwnPropert = Object.getOwnPropertyDescriptor(Object.prototype, prop)) !== null && _Object$getOwnPropert !== void 0
  449. ? _Object$getOwnPropert
  450. : {
  451. set(value) {
  452. this[dataKey] = value;
  453. },
  454. get() {
  455. return this[dataKey];
  456. },
  457. };
  458.  
  459. // Intercept the given property on any object
  460. // The assigned attribute value and the context (enclosing object) are passed to the onSet function.
  461. Object.defineProperty(Object.prototype, prop, {
  462. set(value) {
  463. setter.call(this, isObject(value) ? onSet(this, value) : value);
  464. },
  465. get() {
  466. return getter.call(this);
  467. },
  468. configurable: true,
  469. });
  470. }
  471.  
  472. // Intercept, inspect and modify JSON-based communication to unlock player responses by hijacking the JSON.parse function
  473. function attach$2(onJsonDataReceived) {
  474. window.JSON.parse = function() {
  475. const data = nativeJSONParse.apply(this, arguments);
  476. return isObject(data) ? onJsonDataReceived(data) : data;
  477. };
  478. }
  479.  
  480. function attach$1(onRequestCreate) {
  481. if (typeof window.Request !== 'function') {
  482. return;
  483. }
  484.  
  485. window.Request = new Proxy(window.Request, {
  486. construct(target, args) {
  487. let [url, options] = args;
  488. try {
  489. if (typeof url === 'string') {
  490. if (url.indexOf('/') === 0) {
  491. url = window.location.origin + url;
  492. }
  493.  
  494. if (url.indexOf('https://') !== -1) {
  495. const modifiedUrl = onRequestCreate(url, options);
  496.  
  497. if (modifiedUrl) {
  498. args[0] = modifiedUrl;
  499. }
  500. }
  501. }
  502. } catch (err) {
  503. error(err, `Failed to intercept Request()`);
  504. }
  505.  
  506. return Reflect.construct(target, args);
  507. },
  508. });
  509. }
  510.  
  511. function attach(onXhrOpenCalled) {
  512. XMLHttpRequest.prototype.open = function(...args) {
  513. let [method, url] = args;
  514. try {
  515. if (typeof url === 'string') {
  516. if (url.indexOf('/') === 0) {
  517. url = window.location.origin + url;
  518. }
  519.  
  520. if (url.indexOf('https://') !== -1) {
  521. const modifiedUrl = onXhrOpenCalled(method, url, this);
  522.  
  523. if (modifiedUrl) {
  524. args[1] = modifiedUrl;
  525. }
  526. }
  527. }
  528. } catch (err) {
  529. error(err, `Failed to intercept XMLHttpRequest.open()`);
  530. }
  531.  
  532. nativeXMLHttpRequestOpen.apply(this, args);
  533. };
  534. }
  535.  
  536. const localStoragePrefix = 'SYARB_';
  537.  
  538. function set(key, value) {
  539. localStorage.setItem(localStoragePrefix + key, JSON.stringify(value));
  540. }
  541.  
  542. function get(key) {
  543. try {
  544. return JSON.parse(localStorage.getItem(localStoragePrefix + key));
  545. } catch {
  546. return null;
  547. }
  548. }
  549.  
  550. function getPlayer$1(payload, useAuth) {
  551. return sendInnertubeRequest('v1/player', payload, useAuth);
  552. }
  553.  
  554. function getNext$1(payload, useAuth) {
  555. return sendInnertubeRequest('v1/next', payload, useAuth);
  556. }
  557.  
  558. function sendInnertubeRequest(endpoint, payload, useAuth) {
  559. const xmlhttp = new XMLHttpRequest();
  560. xmlhttp.open('POST', `/youtubei/${endpoint}?key=${getYtcfgValue('INNERTUBE_API_KEY')}&prettyPrint=false`, false);
  561.  
  562. if (useAuth && isUserLoggedIn()) {
  563. xmlhttp.withCredentials = true;
  564. Config.GOOGLE_AUTH_HEADER_NAMES.forEach((headerName) => {
  565. xmlhttp.setRequestHeader(headerName, get(headerName));
  566. });
  567. }
  568.  
  569. xmlhttp.send(JSON.stringify(payload));
  570. return nativeJSONParse(xmlhttp.responseText);
  571. }
  572.  
  573. var innertube = {
  574. getPlayer: getPlayer$1,
  575. getNext: getNext$1,
  576. };
  577.  
  578. let nextResponseCache = {};
  579.  
  580. function getGoogleVideoUrl(originalUrl) {
  581. return Config.VIDEO_PROXY_SERVER_HOST + '/direct/' + btoa(originalUrl.toString());
  582. }
  583.  
  584. function getPlayer(payload) {
  585. // Also request the /next response if a later /next request is likely.
  586. if (!nextResponseCache[payload.videoId] && !isMusic && !isEmbed) {
  587. payload.includeNext = 1;
  588. }
  589.  
  590. return sendRequest('getPlayer', payload);
  591. }
  592.  
  593. function getNext(payload) {
  594. // Next response already cached? => Return cached content
  595. if (nextResponseCache[payload.videoId]) {
  596. return nextResponseCache[payload.videoId];
  597. }
  598.  
  599. return sendRequest('getNext', payload);
  600. }
  601.  
  602. function sendRequest(endpoint, payload) {
  603. const queryParams = new URLSearchParams(payload);
  604. const proxyUrl = `${Config.ACCOUNT_PROXY_SERVER_HOST}/${endpoint}?${queryParams}&client=js`;
  605.  
  606. try {
  607. const xmlhttp = new XMLHttpRequest();
  608. xmlhttp.open('GET', proxyUrl, false);
  609. xmlhttp.send(null);
  610.  
  611. const proxyResponse = nativeJSONParse(xmlhttp.responseText);
  612.  
  613. // Mark request as 'proxied'
  614. proxyResponse.proxied = true;
  615.  
  616. // Put included /next response in the cache
  617. if (proxyResponse.nextResponse) {
  618. nextResponseCache[payload.videoId] = proxyResponse.nextResponse;
  619. delete proxyResponse.nextResponse;
  620. }
  621.  
  622. return proxyResponse;
  623. } catch (err) {
  624. error(err, 'Proxy API Error');
  625. return { errorMessage: 'Proxy Connection failed' };
  626. }
  627. }
  628.  
  629. var proxy = {
  630. getPlayer,
  631. getNext,
  632. getGoogleVideoUrl,
  633. };
  634.  
  635. function getUnlockStrategies$1(videoId, lastPlayerUnlockReason) {
  636. var _getYtcfgValue$client;
  637. const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
  638. const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
  639. const hl = getYtcfgValue('HL');
  640. const userInterfaceTheme = (_getYtcfgValue$client = getYtcfgValue('INNERTUBE_CONTEXT').client.userInterfaceTheme) !== null && _getYtcfgValue$client !== void 0
  641. ? _getYtcfgValue$client
  642. : document.documentElement.hasAttribute('dark')
  643. ? 'USER_INTERFACE_THEME_DARK'
  644. : 'USER_INTERFACE_THEME_LIGHT';
  645.  
  646. return [
  647. /**
  648. * Retrieve the sidebar and video description by just adding `racyCheckOk` and `contentCheckOk` params
  649. * This strategy can be used to bypass content warnings
  650. */
  651. {
  652. name: 'Content Warning Bypass',
  653. skip: !lastPlayerUnlockReason || !lastPlayerUnlockReason.includes('CHECK_REQUIRED'),
  654. optionalAuth: true,
  655. payload: {
  656. context: {
  657. client: {
  658. clientName,
  659. clientVersion,
  660. hl,
  661. userInterfaceTheme,
  662. },
  663. },
  664. videoId,
  665. racyCheckOk: true,
  666. contentCheckOk: true,
  667. },
  668. endpoint: innertube,
  669. },
  670. /**
  671. * Retrieve the sidebar and video description from an account proxy server.
  672. * Session cookies of an age-verified Google account are stored on server side.
  673. * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
  674. */
  675. {
  676. name: 'Account Proxy',
  677. payload: {
  678. videoId,
  679. clientName,
  680. clientVersion,
  681. hl,
  682. userInterfaceTheme,
  683. isEmbed: +isEmbed,
  684. isConfirmed: +isConfirmed,
  685. },
  686. endpoint: proxy,
  687. },
  688. ];
  689. }
  690.  
  691. function getUnlockStrategies(videoId, reason) {
  692. const clientName = getYtcfgValue('INNERTUBE_CLIENT_NAME') || 'WEB';
  693. const clientVersion = getYtcfgValue('INNERTUBE_CLIENT_VERSION') || '2.20220203.04.00';
  694. const signatureTimestamp = getSignatureTimestamp();
  695. const startTimeSecs = getCurrentVideoStartTime(videoId);
  696. const hl = getYtcfgValue('HL');
  697.  
  698. return [
  699. /**
  700. * Retrieve the video info by just adding `racyCheckOk` and `contentCheckOk` params
  701. * This strategy can be used to bypass content warnings
  702. */
  703. {
  704. name: 'Content Warning Bypass',
  705. skip: !reason || !reason.includes('CHECK_REQUIRED'),
  706. optionalAuth: true,
  707. payload: {
  708. context: {
  709. client: {
  710. clientName: clientName,
  711. clientVersion: clientVersion,
  712. hl,
  713. },
  714. },
  715. playbackContext: {
  716. contentPlaybackContext: {
  717. signatureTimestamp,
  718. },
  719. },
  720. videoId,
  721. startTimeSecs,
  722. racyCheckOk: true,
  723. contentCheckOk: true,
  724. },
  725. endpoint: innertube,
  726. },
  727. /**
  728. * Retrieve the video info by using the TVHTML5 Embedded client
  729. * This client has no age restrictions in place (2022-03-28)
  730. * See https://github.com/zerodytrash/YouTube-Internal-Clients
  731. */
  732. {
  733. name: 'TV Embedded Player',
  734. requiresAuth: false,
  735. payload: {
  736. context: {
  737. client: {
  738. clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
  739. clientVersion: '2.0',
  740. clientScreen: 'WATCH',
  741. hl,
  742. },
  743. thirdParty: {
  744. embedUrl: 'https://www.youtube.com/',
  745. },
  746. },
  747. playbackContext: {
  748. contentPlaybackContext: {
  749. signatureTimestamp,
  750. },
  751. },
  752. videoId,
  753. startTimeSecs,
  754. racyCheckOk: true,
  755. contentCheckOk: true,
  756. },
  757. endpoint: innertube,
  758. },
  759. /**
  760. * Retrieve the video info by using the WEB_CREATOR client in combination with user authentication
  761. * Requires that the user is logged in. Can bypass the tightened age verification in the EU.
  762. * See https://github.com/yt-dlp/yt-dlp/pull/600
  763. */
  764. {
  765. name: 'Creator + Auth',
  766. requiresAuth: true,
  767. payload: {
  768. context: {
  769. client: {
  770. clientName: 'WEB_CREATOR',
  771. clientVersion: '1.20210909.07.00',
  772. hl,
  773. },
  774. },
  775. playbackContext: {
  776. contentPlaybackContext: {
  777. signatureTimestamp,
  778. },
  779. },
  780. videoId,
  781. startTimeSecs,
  782. racyCheckOk: true,
  783. contentCheckOk: true,
  784. },
  785. endpoint: innertube,
  786. },
  787. /**
  788. * Retrieve the video info from an account proxy server.
  789. * Session cookies of an age-verified Google account are stored on server side.
  790. * See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/tree/main/account-proxy
  791. */
  792. {
  793. name: 'Account Proxy',
  794. payload: {
  795. videoId,
  796. reason,
  797. clientName,
  798. clientVersion,
  799. signatureTimestamp,
  800. startTimeSecs,
  801. hl,
  802. isEmbed: +isEmbed,
  803. isConfirmed: +isConfirmed,
  804. },
  805. endpoint: proxy,
  806. },
  807. ];
  808. }
  809.  
  810. var buttonTemplate =
  811. '<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>';
  812.  
  813. let buttons = {};
  814.  
  815. async function addButton(id, text, backgroundColor, onClick) {
  816. const errorScreenElement = await waitForElement('.ytp-error', 2000);
  817. const buttonElement = createElement('div', { class: 'button-container', innerHTML: buttonTemplate });
  818. buttonElement.getElementsByClassName('button-text')[0].innerText = text;
  819.  
  820. if (typeof onClick === 'function') {
  821. buttonElement.addEventListener('click', onClick);
  822. }
  823.  
  824. // Button already attached?
  825. if (buttons[id] && buttons[id].isConnected) {
  826. return;
  827. }
  828.  
  829. buttons[id] = buttonElement;
  830. errorScreenElement.append(buttonElement);
  831. }
  832.  
  833. function removeButton(id) {
  834. if (buttons[id] && buttons[id].isConnected) {
  835. buttons[id].remove();
  836. }
  837. }
  838.  
  839. const confirmationButtonId = 'confirmButton';
  840. const confirmationButtonText = 'Click to unlock';
  841.  
  842. function isConfirmationRequired() {
  843. return !isConfirmed && isEmbed && Config.ENABLE_UNLOCK_CONFIRMATION_EMBED;
  844. }
  845.  
  846. function requestConfirmation() {
  847. addButton(confirmationButtonId, confirmationButtonText, null, () => {
  848. removeButton(confirmationButtonId);
  849. confirm();
  850. });
  851. }
  852.  
  853. function confirm() {
  854. setUrlParams({
  855. unlock_confirmed: 1,
  856. autoplay: 1,
  857. });
  858. }
  859.  
  860. var tDesktop = '<tp-yt-paper-toast></tp-yt-paper-toast>\n';
  861.  
  862. var tMobile =
  863. '<c3-toast>\n <ytm-notification-action-renderer>\n <div class="notification-action-response-text"></div>\n </ytm-notification-action-renderer>\n</c3-toast>\n';
  864.  
  865. const template = isDesktop ? tDesktop : tMobile;
  866.  
  867. const nToastContainer = createElement('div', { id: 'toast-container', innerHTML: template });
  868. const nToast = nToastContainer.querySelector(':scope > *');
  869.  
  870. // On YT Music show the toast above the player controls
  871. if (isMusic) {
  872. nToast.style['margin-bottom'] = '85px';
  873. }
  874.  
  875. if (!isDesktop) {
  876. nToast.nMessage = nToast.querySelector('.notification-action-response-text');
  877. nToast.show = (message) => {
  878. nToast.nMessage.innerText = message;
  879. nToast.setAttribute('dir', 'in');
  880. setTimeout(() => {
  881. nToast.setAttribute('dir', 'out');
  882. }, nToast.duration + 225);
  883. };
  884. }
  885.  
  886. async function show(message, duration = 5) {
  887. if (!Config.ENABLE_UNLOCK_NOTIFICATION) return;
  888. if (isEmbed) return;
  889.  
  890. await pageLoaded();
  891.  
  892. // Do not show notification when tab is in background
  893. if (document.visibilityState === 'hidden') return;
  894.  
  895. // Append toast container to DOM, if not already done
  896. if (!nToastContainer.isConnected) document.documentElement.append(nToastContainer);
  897.  
  898. nToast.duration = duration * 1000;
  899. nToast.show(message);
  900. }
  901.  
  902. var Toast = { show };
  903.  
  904. const messagesMap = {
  905. success: 'Age-restricted video successfully unlocked!',
  906. fail: 'Unable to unlock this video 🙁 - More information in the developer console',
  907. };
  908.  
  909. let lastPlayerUnlockVideoId = null;
  910. let lastPlayerUnlockReason = null;
  911.  
  912. let lastProxiedGoogleVideoUrlParams;
  913. let cachedPlayerResponse = {};
  914.  
  915. function getLastProxiedGoogleVideoId() {
  916. var _lastProxiedGoogleVid;
  917. return (_lastProxiedGoogleVid = lastProxiedGoogleVideoUrlParams) === null || _lastProxiedGoogleVid === void 0 ? void 0 : _lastProxiedGoogleVid.get('id');
  918. }
  919.  
  920. function unlockResponse$1(playerResponse) {
  921. var _playerResponse$video, _playerResponse$playa, _playerResponse$previ, _unlockedPlayerRespon, _unlockedPlayerRespon3;
  922. // Check if the user has to confirm the unlock first
  923. if (isConfirmationRequired()) {
  924. info('Unlock confirmation required.');
  925. requestConfirmation();
  926. return;
  927. }
  928.  
  929. const videoId = ((_playerResponse$video = playerResponse.videoDetails) === null || _playerResponse$video === void 0 ? void 0 : _playerResponse$video.videoId)
  930. || getYtcfgValue('PLAYER_VARS').video_id;
  931. const reason = ((_playerResponse$playa = playerResponse.playabilityStatus) === null || _playerResponse$playa === void 0 ? void 0 : _playerResponse$playa.status)
  932. || ((_playerResponse$previ = playerResponse.previewPlayabilityStatus) === null || _playerResponse$previ === void 0 ? void 0 : _playerResponse$previ.status);
  933.  
  934. if (!Config.SKIP_CONTENT_WARNINGS && reason.includes('CHECK_REQUIRED')) {
  935. info(`SKIP_CONTENT_WARNINGS disabled and ${reason} status detected.`);
  936. return;
  937. }
  938.  
  939. lastPlayerUnlockVideoId = videoId;
  940. lastPlayerUnlockReason = reason;
  941.  
  942. const unlockedPlayerResponse = getUnlockedPlayerResponse(videoId, reason);
  943.  
  944. // account proxy error?
  945. if (unlockedPlayerResponse.errorMessage) {
  946. Toast.show(`${messagesMap.fail} (ProxyError)`, 10);
  947. throw new Error(`Player Unlock Failed, Proxy Error Message: ${unlockedPlayerResponse.errorMessage}`);
  948. }
  949.  
  950. // check if the unlocked response isn't playable
  951. if (
  952. !Config.VALID_PLAYABILITY_STATUSES.includes(
  953. (_unlockedPlayerRespon = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon === void 0 ? void 0 : _unlockedPlayerRespon.status,
  954. )
  955. ) {
  956. var _unlockedPlayerRespon2;
  957. Toast.show(`${messagesMap.fail} (PlayabilityError)`, 10);
  958. throw new Error(
  959. `Player Unlock Failed, playabilityStatus: ${
  960. (_unlockedPlayerRespon2 = unlockedPlayerResponse.playabilityStatus) === null || _unlockedPlayerRespon2 === void 0 ? void 0 : _unlockedPlayerRespon2.status
  961. }`,
  962. );
  963. }
  964.  
  965. // 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.
  966. if (
  967. unlockedPlayerResponse.proxied && (_unlockedPlayerRespon3 = unlockedPlayerResponse.streamingData) !== null && _unlockedPlayerRespon3 !== void 0
  968. && _unlockedPlayerRespon3.adaptiveFormats
  969. ) {
  970. var _unlockedPlayerRespon4, _unlockedPlayerRespon5;
  971. const cipherText = (_unlockedPlayerRespon4 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) =>
  972. x.signatureCipher
  973. )) === null || _unlockedPlayerRespon4 === void 0
  974. ? void 0
  975. : _unlockedPlayerRespon4.signatureCipher;
  976. const videoUrl = cipherText
  977. ? new URLSearchParams(cipherText).get('url')
  978. : (_unlockedPlayerRespon5 = unlockedPlayerResponse.streamingData.adaptiveFormats.find((x) => x.url)) === null || _unlockedPlayerRespon5 === void 0
  979. ? void 0
  980. : _unlockedPlayerRespon5.url;
  981.  
  982. lastProxiedGoogleVideoUrlParams = videoUrl ? new URLSearchParams(new window.URL(videoUrl).search) : null;
  983. }
  984.  
  985. // Overwrite the embedded (preview) playabilityStatus with the unlocked one
  986. if (playerResponse.previewPlayabilityStatus) {
  987. playerResponse.previewPlayabilityStatus = unlockedPlayerResponse.playabilityStatus;
  988. }
  989.  
  990. // Transfer all unlocked properties to the original player response
  991. Object.assign(playerResponse, unlockedPlayerResponse);
  992.  
  993. playerResponse.unlocked = true;
  994.  
  995. Toast.show(messagesMap.success);
  996. }
  997.  
  998. function getUnlockedPlayerResponse(videoId, reason) {
  999. // Check if response is cached
  1000. if (cachedPlayerResponse.videoId === videoId) return createDeepCopy(cachedPlayerResponse);
  1001.  
  1002. const unlockStrategies = getUnlockStrategies(videoId, reason);
  1003.  
  1004. let unlockedPlayerResponse = {};
  1005.  
  1006. // Try every strategy until one of them works
  1007. unlockStrategies.every((strategy, index) => {
  1008. var _unlockedPlayerRespon6;
  1009. // Skip strategy if authentication is required and the user is not logged in
  1010. if (strategy.skip || strategy.requiresAuth && !isUserLoggedIn()) return true;
  1011.  
  1012. info(`Trying Player Unlock Method #${index + 1} (${strategy.name})`);
  1013.  
  1014. try {
  1015. unlockedPlayerResponse = strategy.endpoint.getPlayer(strategy.payload, strategy.requiresAuth || strategy.optionalAuth);
  1016. } catch (err) {
  1017. error(err, `Player Unlock Method ${index + 1} failed with exception`);
  1018. }
  1019.  
  1020. const isStatusValid = Config.VALID_PLAYABILITY_STATUSES.includes(
  1021. (_unlockedPlayerRespon6 = unlockedPlayerResponse) === null || _unlockedPlayerRespon6 === void 0
  1022. || (_unlockedPlayerRespon6 = _unlockedPlayerRespon6.playabilityStatus) === null || _unlockedPlayerRespon6 === void 0
  1023. ? void 0
  1024. : _unlockedPlayerRespon6.status,
  1025. );
  1026.  
  1027. if (isStatusValid) {
  1028. var _unlockedPlayerRespon7;
  1029. /**
  1030. * Workaround: https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/issues/191
  1031. *
  1032. * YouTube checks if the `trackingParams` in the response matches the decoded `trackingParam` in `responseContext.mainAppWebResponseContext`.
  1033. * However, sometimes the response does not include the `trackingParam` in the `responseContext`, causing the check to fail.
  1034. *
  1035. * This workaround addresses the issue by hardcoding the `trackingParams` in the response context.
  1036. */
  1037. if (
  1038. !unlockedPlayerResponse.trackingParams
  1039. || !((_unlockedPlayerRespon7 = unlockedPlayerResponse.responseContext) !== null && _unlockedPlayerRespon7 !== void 0
  1040. && (_unlockedPlayerRespon7 = _unlockedPlayerRespon7.mainAppWebResponseContext) !== null && _unlockedPlayerRespon7 !== void 0
  1041. && _unlockedPlayerRespon7.trackingParam)
  1042. ) {
  1043. unlockedPlayerResponse.trackingParams = 'CAAQu2kiEwjor8uHyOL_AhWOvd4KHavXCKw=';
  1044. unlockedPlayerResponse.responseContext = {
  1045. mainAppWebResponseContext: {
  1046. trackingParam: 'kx_fmPxhoPZRzgL8kzOwANUdQh8ZwHTREkw2UqmBAwpBYrzRgkuMsNLBwOcCE59TDtslLKPQ-SS',
  1047. },
  1048. };
  1049. }
  1050.  
  1051. /**
  1052. * Workaround: Account proxy response currently does not include `playerConfig`
  1053. *
  1054. * Stays here until we rewrite the account proxy to only include the necessary and bare minimum response
  1055. */
  1056. if (strategy.payload.startTimeSecs && strategy.name === 'Account Proxy') {
  1057. unlockedPlayerResponse.playerConfig = {
  1058. playbackStartConfig: {
  1059. startSeconds: strategy.payload.startTimeSecs,
  1060. },
  1061. };
  1062. }
  1063. }
  1064.  
  1065. return !isStatusValid;
  1066. });
  1067.  
  1068. // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
  1069. cachedPlayerResponse = { videoId, ...createDeepCopy(unlockedPlayerResponse) };
  1070.  
  1071. return unlockedPlayerResponse;
  1072. }
  1073.  
  1074. let cachedNextResponse = {};
  1075.  
  1076. function unlockResponse(originalNextResponse) {
  1077. const videoId = originalNextResponse.currentVideoEndpoint.watchEndpoint.videoId;
  1078.  
  1079. if (!videoId) {
  1080. throw new Error(`Missing videoId in nextResponse`);
  1081. }
  1082.  
  1083. // Only unlock the /next response when the player has been unlocked as well
  1084. if (videoId !== lastPlayerUnlockVideoId) {
  1085. return;
  1086. }
  1087.  
  1088. const unlockedNextResponse = getUnlockedNextResponse(videoId);
  1089.  
  1090. // check if the sidebar of the unlocked response is still empty
  1091. if (isWatchNextSidebarEmpty(unlockedNextResponse)) {
  1092. throw new Error(`Sidebar Unlock Failed`);
  1093. }
  1094.  
  1095. // Transfer some parts of the unlocked response to the original response
  1096. mergeNextResponse(originalNextResponse, unlockedNextResponse);
  1097. }
  1098.  
  1099. function getUnlockedNextResponse(videoId) {
  1100. // Check if response is cached
  1101. if (cachedNextResponse.videoId === videoId) return createDeepCopy(cachedNextResponse);
  1102.  
  1103. const unlockStrategies = getUnlockStrategies$1(videoId, lastPlayerUnlockReason);
  1104.  
  1105. let unlockedNextResponse = {};
  1106.  
  1107. // Try every strategy until one of them works
  1108. unlockStrategies.every((strategy, index) => {
  1109. if (strategy.skip) return true;
  1110.  
  1111. info(`Trying Next Unlock Method #${index + 1} (${strategy.name})`);
  1112.  
  1113. try {
  1114. unlockedNextResponse = strategy.endpoint.getNext(strategy.payload, strategy.optionalAuth);
  1115. } catch (err) {
  1116. error(err, `Next Unlock Method ${index + 1} failed with exception`);
  1117. }
  1118.  
  1119. return isWatchNextSidebarEmpty(unlockedNextResponse);
  1120. });
  1121.  
  1122. // Cache response to prevent a flood of requests in case youtube processes a blocked response mutiple times.
  1123. cachedNextResponse = { videoId, ...createDeepCopy(unlockedNextResponse) };
  1124.  
  1125. return unlockedNextResponse;
  1126. }
  1127.  
  1128. function mergeNextResponse(originalNextResponse, unlockedNextResponse) {
  1129. var _unlockedNextResponse;
  1130. if (isDesktop) {
  1131. // Transfer WatchNextResults to original response
  1132. originalNextResponse.contents.twoColumnWatchNextResults.secondaryResults = unlockedNextResponse.contents.twoColumnWatchNextResults.secondaryResults;
  1133.  
  1134. // Transfer video description to original response
  1135. const originalVideoSecondaryInfoRenderer = originalNextResponse.contents.twoColumnWatchNextResults.results.results.contents.find(
  1136. (x) => x.videoSecondaryInfoRenderer,
  1137. ).videoSecondaryInfoRenderer;
  1138. const unlockedVideoSecondaryInfoRenderer = unlockedNextResponse.contents.twoColumnWatchNextResults.results.results.contents.find(
  1139. (x) => x.videoSecondaryInfoRenderer,
  1140. ).videoSecondaryInfoRenderer;
  1141.  
  1142. // TODO: Throw if description not found?
  1143. if (unlockedVideoSecondaryInfoRenderer.description) {
  1144. originalVideoSecondaryInfoRenderer.description = unlockedVideoSecondaryInfoRenderer.description;
  1145. } else if (unlockedVideoSecondaryInfoRenderer.attributedDescription) {
  1146. originalVideoSecondaryInfoRenderer.attributedDescription = unlockedVideoSecondaryInfoRenderer.attributedDescription;
  1147. }
  1148.  
  1149. return;
  1150. }
  1151.  
  1152. // Transfer WatchNextResults to original response
  1153. const unlockedWatchNextFeed = (_unlockedNextResponse = unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
  1154. || (_unlockedNextResponse = _unlockedNextResponse.singleColumnWatchNextResults) === null || _unlockedNextResponse === void 0
  1155. || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
  1156. || (_unlockedNextResponse = _unlockedNextResponse.results) === null || _unlockedNextResponse === void 0
  1157. || (_unlockedNextResponse = _unlockedNextResponse.contents) === null || _unlockedNextResponse === void 0
  1158. ? void 0
  1159. : _unlockedNextResponse.find(
  1160. (x) => {
  1161. var _x$itemSectionRendere;
  1162. return ((_x$itemSectionRendere = x.itemSectionRenderer) === null || _x$itemSectionRendere === void 0 ? void 0 : _x$itemSectionRendere.targetId)
  1163. === 'watch-next-feed';
  1164. },
  1165. );
  1166.  
  1167. if (unlockedWatchNextFeed) originalNextResponse.contents.singleColumnWatchNextResults.results.results.contents.push(unlockedWatchNextFeed);
  1168.  
  1169. // Transfer video description to original response
  1170. const originalStructuredDescriptionContentRenderer = originalNextResponse.engagementPanels
  1171. .find((x) => x.engagementPanelSectionListRenderer)
  1172. .engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find((x) => x.expandableVideoDescriptionBodyRenderer);
  1173. const unlockedStructuredDescriptionContentRenderer = unlockedNextResponse.engagementPanels
  1174. .find((x) => x.engagementPanelSectionListRenderer)
  1175. .engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find((x) => x.expandableVideoDescriptionBodyRenderer);
  1176.  
  1177. if (unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer) {
  1178. originalStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer =
  1179. unlockedStructuredDescriptionContentRenderer.expandableVideoDescriptionBodyRenderer;
  1180. }
  1181. }
  1182.  
  1183. /**
  1184. * Handles XMLHttpRequests and
  1185. * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
  1186. * - Store auth headers for the authentication of further unlock requests.
  1187. * - Add "content check ok" flags to request bodys
  1188. */
  1189. function handleXhrOpen(method, url, xhr) {
  1190. const url_obj = new URL(url);
  1191. let proxyUrl = unlockGoogleVideo(url_obj);
  1192. if (proxyUrl) {
  1193. // Exclude credentials from XMLHttpRequest
  1194. Object.defineProperty(xhr, 'withCredentials', {
  1195. set: () => {},
  1196. get: () => false,
  1197. });
  1198. return proxyUrl.toString();
  1199. }
  1200.  
  1201. if (url_obj.pathname.indexOf('/youtubei/') === 0) {
  1202. // Store auth headers in storage for further usage.
  1203. attach$4(xhr, 'setRequestHeader', ([headerName, headerValue]) => {
  1204. if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
  1205. set(headerName, headerValue);
  1206. }
  1207. });
  1208. }
  1209.  
  1210. if (Config.SKIP_CONTENT_WARNINGS && method === 'POST' && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
  1211. // Add content check flags to player and next request (this will skip content warnings)
  1212. attach$4(xhr, 'send', (args) => {
  1213. if (typeof args[0] === 'string') {
  1214. args[0] = setContentCheckOk(args[0]);
  1215. }
  1216. });
  1217. }
  1218. }
  1219.  
  1220. /**
  1221. * Handles Fetch requests and
  1222. * - Rewrite Googlevideo URLs to Proxy URLs (if necessary)
  1223. * - Store auth headers for the authentication of further unlock requests.
  1224. * - Add "content check ok" flags to request bodys
  1225. */
  1226. function handleFetchRequest(url, requestOptions) {
  1227. const url_obj = new URL(url);
  1228. const newGoogleVideoUrl = unlockGoogleVideo(url_obj);
  1229. if (newGoogleVideoUrl) {
  1230. // Exclude credentials from Fetch Request
  1231. if (requestOptions.credentials) {
  1232. requestOptions.credentials = 'omit';
  1233. }
  1234. return newGoogleVideoUrl.toString();
  1235. }
  1236.  
  1237. if (url_obj.pathname.indexOf('/youtubei/') === 0 && isObject(requestOptions.headers)) {
  1238. // Store auth headers in authStorage for further usage.
  1239. for (let headerName in requestOptions.headers) {
  1240. if (Config.GOOGLE_AUTH_HEADER_NAMES.includes(headerName)) {
  1241. set(headerName, requestOptions.headers[headerName]);
  1242. }
  1243. }
  1244. }
  1245.  
  1246. if (Config.SKIP_CONTENT_WARNINGS && ['/youtubei/v1/player', '/youtubei/v1/next'].includes(url_obj.pathname)) {
  1247. // Add content check flags to player and next request (this will skip content warnings)
  1248. requestOptions.body = setContentCheckOk(requestOptions.body);
  1249. }
  1250. }
  1251.  
  1252. /**
  1253. * If the account proxy was used to retrieve the video info, the following applies:
  1254. * 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.
  1255. * to get around this, the googlevideo URL will be replaced with a web-proxy URL in the same country (US).
  1256. * this is only required if the "gcr=[countrycode]" flag is set in the googlevideo-url...
  1257. * @returns The rewitten url (if a proxy is required)
  1258. */
  1259. function unlockGoogleVideo(url) {
  1260. if (Config.VIDEO_PROXY_SERVER_HOST && isGoogleVideoUrl(url)) {
  1261. if (isGoogleVideoUnlockRequired(url, getLastProxiedGoogleVideoId())) {
  1262. return proxy.getGoogleVideoUrl(url);
  1263. }
  1264. }
  1265. }
  1266.  
  1267. /**
  1268. * Adds `contentCheckOk` and `racyCheckOk` to the given json data (if the data contains a video id)
  1269. * @returns {string} The modified json
  1270. */
  1271. function setContentCheckOk(bodyJson) {
  1272. try {
  1273. let parsedBody = JSON.parse(bodyJson);
  1274. if (parsedBody.videoId) {
  1275. parsedBody.contentCheckOk = true;
  1276. parsedBody.racyCheckOk = true;
  1277. return JSON.stringify(parsedBody);
  1278. }
  1279. } catch {}
  1280. return bodyJson;
  1281. }
  1282.  
  1283. function processThumbnails(responseObject) {
  1284. const thumbnails = findNestedObjectsByAttributeNames(responseObject, ['url', 'height']);
  1285.  
  1286. let blurredThumbnailCount = 0;
  1287.  
  1288. for (const thumbnail of thumbnails) {
  1289. if (isThumbnailBlurred(thumbnail)) {
  1290. blurredThumbnailCount++;
  1291. thumbnail.url = thumbnail.url.split('?')[0];
  1292. }
  1293. }
  1294.  
  1295. info(blurredThumbnailCount + '/' + thumbnails.length + ' thumbnails detected as blurred.');
  1296. }
  1297.  
  1298. function isThumbnailBlurred(thumbnail) {
  1299. const hasSQPParam = thumbnail.url.indexOf('?sqp=') !== -1;
  1300.  
  1301. if (!hasSQPParam) {
  1302. return false;
  1303. }
  1304.  
  1305. const SQPLength = new URL(thumbnail.url).searchParams.get('sqp').length;
  1306. const isBlurred = Config.BLURRED_THUMBNAIL_SQP_LENGTHS.includes(SQPLength);
  1307.  
  1308. return isBlurred;
  1309. }
  1310.  
  1311. try {
  1312. attach$3(processYtData);
  1313. attach$2(processYtData);
  1314. attach(handleXhrOpen);
  1315. attach$1(handleFetchRequest);
  1316. } catch (err) {
  1317. error(err, 'Error while attaching data interceptors');
  1318. }
  1319.  
  1320. function processYtData(ytData) {
  1321. try {
  1322. // Player Unlock #1: Initial page data structure and response from `/youtubei/v1/player` XHR request
  1323. if (isPlayerObject(ytData) && isAgeRestricted(ytData.playabilityStatus)) {
  1324. unlockResponse$1(ytData);
  1325. } // Player Unlock #2: Embedded Player inital data structure
  1326. else if (isEmbeddedPlayerObject(ytData) && isAgeRestricted(ytData.previewPlayabilityStatus)) {
  1327. unlockResponse$1(ytData);
  1328. }
  1329. } catch (err) {
  1330. error(err, 'Video unlock failed');
  1331. }
  1332.  
  1333. try {
  1334. // Unlock sidebar watch next feed (sidebar) and video description
  1335. if (isWatchNextObject(ytData) && isWatchNextSidebarEmpty(ytData)) {
  1336. unlockResponse(ytData);
  1337. }
  1338.  
  1339. // Mobile version
  1340. if (isWatchNextObject(ytData.response) && isWatchNextSidebarEmpty(ytData.response)) {
  1341. unlockResponse(ytData.response);
  1342. }
  1343. } catch (err) {
  1344. error(err, 'Sidebar unlock failed');
  1345. }
  1346.  
  1347. try {
  1348. // Unlock blurry video thumbnails in search results
  1349. if (isSearchResult(ytData)) {
  1350. processThumbnails(ytData);
  1351. }
  1352. } catch (err) {
  1353. error(err, 'Thumbnail unlock failed');
  1354. }
  1355.  
  1356. return ytData;
  1357. }
  1358. })();