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