Simple YouTube Age Restriction Bypass

Watch age restricted videos on YouTube without login and without age verification :)

目前为 2022-11-12 提交的版本。查看 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址