Kelsondre69

Oaoa

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