Simple YouTube Age Restriction Bypass - by Hexxonn

Watch age restricted videos on YouTube without login and without age verification 😎 | Mod by Hexxonn

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