theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

  1. // ==UserScript==
  2. // @name theYNC.com Underground bypass
  3. // @description Watch theYNC Underground videos without needing an account
  4. // @namespace Violentmonkey Scripts
  5. // @match *://*.theync.com/*
  6. // @match *://theync.com/*
  7. // @match *://*.theync.net/*
  8. // @match *://theync.net/*
  9. // @match *://*.theync.org/*
  10. // @match *://theync.org/*
  11. // @match *://archive.ph/*
  12. // @match *://archive.today/*
  13. // @include /https?:\/\/web\.archive\.org\/web\/\d+?\/https?:\/\/theync\.(?:com|org|net)/
  14. // @require https://update.greasyfork.org/scripts/523012/1519437/WaitForKeyElement.js
  15. // @grant GM.xmlHttpRequest
  16. // @connect media.theync.com
  17. // @connect archive.org
  18. // @grant GM_addStyle
  19. // @grant GM_log
  20. // @grant GM_addElement
  21. // @version 10.7
  22. // @supportURL https://greasyfork.org/en/scripts/520352-theync-com-underground-bypass/feedback
  23. // @license MIT
  24. // @author https://greasyfork.org/en/users/1409235-paywalldespiser
  25. // ==/UserScript==
  26.  
  27. /**
  28. * Fetches available archives of a given address and retrieves their URLs.
  29. *
  30. * @param {string} address
  31. * @returns {Promise<string>}
  32. */
  33. function queryArchive(address) {
  34. try {
  35. const url = new URL('https://archive.org/wayback/available');
  36. url.searchParams.append('url', address);
  37.  
  38. return GM.xmlHttpRequest({
  39. method: 'GET',
  40. url,
  41. redirect: 'follow',
  42. responseType: 'json',
  43. })
  44. .then((result) => {
  45. if (result.status >= 300) {
  46. console.error(result.status);
  47. return Promise.reject(result);
  48. }
  49.  
  50. return result;
  51. })
  52. .then((result) => result.response)
  53. .then((result) => {
  54. if (
  55. result.archived_snapshots &&
  56. result.archived_snapshots.closest
  57. ) {
  58. return result.archived_snapshots.closest.url;
  59. }
  60. return Promise.reject();
  61. });
  62. } catch (e) {
  63. return Promise.reject();
  64. }
  65. }
  66.  
  67. /**
  68. * Checks whether a URL is valid and accessible.
  69. *
  70. * @param {string?} address
  71. * @returns {Promise<string>}
  72. */
  73. function isValidURL(address) {
  74. if (!address) {
  75. return Promise.reject(address);
  76. }
  77. try {
  78. const url = new URL(address);
  79. return GM.xmlHttpRequest({ url, method: 'HEAD' }).then((result) => {
  80. if (result.status === 404) {
  81. return Promise.reject(address);
  82. }
  83. return address;
  84. });
  85. } catch {
  86. return Promise.reject(address);
  87. }
  88. }
  89.  
  90. /**
  91. * Tries to guess the video URL of a given theYNC video via the thumbnail URL.
  92. * Only works on videos published before around May 2023.
  93. *
  94. * @param {Element} element
  95. * @returns {string?}
  96. */
  97. function getTheYNCVideoURL(element) {
  98. /**
  99. * @type {string | undefined | null}
  100. */
  101. const thumbnailURL = element.querySelector('.image > img')?.src;
  102. if (!thumbnailURL) {
  103. return null;
  104. }
  105. const group_url = thumbnailURL.match(
  106. /^https?:\/\/(?:media\.theync\.(?:com|org|net)|(?:www\.)?theync\.(?:com|org|net)\/media)\/thumbs\/(.+?)\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v)/im
  107. )?.[1];
  108. if (!group_url) {
  109. return null;
  110. }
  111. return 'https://media.theync.com/videos/' + group_url + '.mp4';
  112. }
  113.  
  114. /**
  115. * Retrieves the video URL from a theYNC video page
  116. *
  117. * @param {Element} [element=document]
  118. * @returns {string?}
  119. */
  120. function retrieveVideoURL(element = document) {
  121. if (location.host === 'archive.ph' || location.host === 'archive.today') {
  122. const attribute = element
  123. .querySelector('[id="thisPlayer"] video[old-src]')
  124. ?.getAttribute('old-src');
  125. if (attribute) {
  126. return attribute;
  127. }
  128. }
  129. /**
  130. * @type {string | null | undefined}
  131. */
  132. const videoSrc = element.querySelector(
  133. '.stage-video > .inner-stage video[src]'
  134. )?.src;
  135. if (videoSrc) {
  136. return videoSrc;
  137. }
  138. const playerSetupScript = element.querySelector(
  139. '[id=thisPlayer] + script'
  140. )?.textContent;
  141. if (!playerSetupScript) {
  142. return null;
  143. }
  144. // TODO: Find a non-regex solution to this that doesn't involve eval#
  145. const videoURL = playerSetupScript.match(
  146. /(?<=file\:) *?"(?:https?:\/\/web.archive.org\/web\/\d+?\/)?(https?:\/\/(?:(?:www\.)?theync\.(?:com|org|net)\/media|media.theync\.(?:com|org|net))\/videos\/.+?\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v))"/im
  147. )?.[1];
  148. if (!videoURL) {
  149. return null;
  150. }
  151. return decodeURIComponent(videoURL);
  152. }
  153.  
  154. /**
  155. * Retrieves the video URL from an archived YNC URL
  156. *
  157. * @param {string} archiveURL
  158. * @returns {Promise<string>}
  159. */
  160. function getVideoURLFromArchive(archiveURL) {
  161. return GM.xmlHttpRequest({ url: archiveURL, method: 'GET' })
  162. .then((result) => {
  163. if (result.status >= 300) {
  164. console.error(result.status);
  165. return Promise.reject(result);
  166. }
  167. return result;
  168. })
  169.  
  170. .then((result) => {
  171. // Initialize the DOM parser
  172. const parser = new DOMParser();
  173.  
  174. // Parse the text
  175. const doc = parser.parseFromString(
  176. result.responseText,
  177. 'text/html'
  178. );
  179.  
  180. // You can now even select part of that html as you would in the regular DOM
  181. // Example:
  182. // const docArticle = doc.querySelector('article').innerHTML
  183. const videoURL = retrieveVideoURL(doc);
  184. if (videoURL) {
  185. return videoURL;
  186. }
  187. return Promise.reject();
  188. });
  189. }
  190.  
  191. /**
  192. * Calls many async functions in chunks and returns the accumulated results of all chunks in one flattened array.
  193. *
  194. * @async
  195. * @template T
  196. * @param {(() => Promise<T>)[]} asyncFunctions A list of functions that make an async call and should be called in chunks. I.e. `() => this.service.loadData()`
  197. * @param {number} chunkSize how many async functions are called at once
  198. * @returns {Promise<T[]>}
  199. */
  200. async function callInChunks(asyncFunctions, chunkSize) {
  201. const numOfChunks = Math.ceil(asyncFunctions.length / chunkSize);
  202. const chunks = [...Array(numOfChunks)].map((_, i) =>
  203. asyncFunctions.slice(chunkSize * i, chunkSize * i + chunkSize)
  204. );
  205.  
  206. const result = [];
  207. for (const chunk of chunks) {
  208. const chunkResult = await Promise.allSettled(
  209. chunk.map((chunkFn) => chunkFn())
  210. );
  211. result.push(...chunkResult);
  212. }
  213. return result.flat();
  214. }
  215.  
  216. /**
  217. * setTimeout Promise Wrapper function
  218. *
  219. * @param {number} ms
  220. * @returns {Promise<void>}
  221. */
  222. function wait(ms) {
  223. return new Promise((resolve) => setTimeout(resolve, ms));
  224. }
  225.  
  226. (function () {
  227. 'use strict';
  228.  
  229. const allowedExtensions = [
  230. 'flv',
  231. 'mpg',
  232. 'wmv',
  233. 'avi',
  234. '3gp',
  235. 'qt',
  236. 'mp4',
  237. 'mov',
  238. 'm4v',
  239. 'f4v',
  240. ];
  241.  
  242. GM_addStyle(`
  243. .loader {
  244. border: 0.25em solid #f3f3f3;
  245. border-top: 0.25em solid rgba(0, 0, 0, 0);
  246. border-radius: 50%;
  247. width: 1em;
  248. height: 1em;
  249. animation: spin 2s linear infinite;
  250. }
  251. @keyframes spin {
  252. 0% {
  253. transform: rotate(0deg);
  254. }
  255. 100% {
  256. transform: rotate(360deg);
  257. }
  258. }
  259. .border-gold {
  260. display: flex !important;
  261. align-items: center;
  262. justify-content: center;
  263. gap: 1em;
  264. }
  265. `);
  266.  
  267. waitForKeyElement(
  268. '[id="content"],[id="related-videos"] .content-block'
  269. ).then((contentBlock) =>
  270. callInChunks(
  271. Array.from(
  272. contentBlock.querySelectorAll(
  273. '.inner-block > a:has(.item-info > .border-gold)'
  274. )
  275. ).map((element) => () => {
  276. const undergroundLogo = element.querySelector(
  277. '.item-info > .border-gold'
  278. );
  279.  
  280. const loadingElement = GM_addElement('div');
  281. loadingElement.classList.add('loader');
  282. undergroundLogo.appendChild(loadingElement);
  283. return isValidURL(getTheYNCVideoURL(element))
  284. .then((url) => ({
  285. url: url,
  286. text: 'BYPASSED',
  287. color: 'green',
  288. }))
  289. .catch(() => {
  290. /**
  291. * @type {RegExpMatchArray}
  292. */
  293. const [, secondLevelDomain, path] =
  294. element.href.match(
  295. /(^https?:\/\/(?:www\.)?theync\.)(?:com|org|net)(\/.*$)/im
  296. ) ?? [];
  297. if (!secondLevelDomain) {
  298. return Promise.reject(
  299. 'Error with the URL: ' + element.href
  300. );
  301. }
  302. return ['com', 'org', 'net']
  303. .reduce(
  304. /**
  305. * @param {Promise<string>} accumulator
  306. * @param {string} currentTLD
  307. * @param {number} currentIndex
  308. * @param {string[]} array
  309. * @returns {Promise<string>}
  310. */
  311. (
  312. accumulator,
  313. currentTLD,
  314. currentIndex,
  315. array
  316. ) =>
  317. accumulator.catch(() => {
  318. const archiveQuery = queryArchive(
  319. secondLevelDomain +
  320. currentTLD +
  321. path
  322. );
  323. const archiveCooldown = wait(5000);
  324.  
  325. return currentIndex < array.length - 1
  326. ? archiveQuery.catch((reason) =>
  327. archiveCooldown.then(() =>
  328. Promise.reject(reason)
  329. )
  330. )
  331. : archiveQuery;
  332. }),
  333. Promise.reject()
  334. )
  335.  
  336. .then((archiveURL) =>
  337. getVideoURLFromArchive(archiveURL).then(
  338. (videoURL) => ({
  339. url: videoURL,
  340. text: 'ARCHIVED',
  341. color: 'blue',
  342. }),
  343. () => ({
  344. url: archiveURL,
  345. text: 'MAYBE ARCHIVED',
  346. color: 'aqua',
  347. })
  348. )
  349. );
  350. })
  351. .catch(() => ({
  352. url:
  353. 'https://archive.ph/' +
  354. encodeURIComponent(element.href),
  355. text: 'Try archive.today',
  356. color: 'red',
  357. }))
  358.  
  359. .then(({ url, text, color }) => {
  360. undergroundLogo.textContent = text;
  361. undergroundLogo.style.backgroundColor = color;
  362. element.href = url;
  363. })
  364. .finally(() => loadingElement.remove());
  365. }),
  366. 32
  367. )
  368. );
  369. waitForKeyElement('[id="stage"]:has([id="thisPlayer"])').then((stage) => {
  370. const videoURL = retrieveVideoURL();
  371. if (videoURL) {
  372. stage.innerHTML = '';
  373. stage.style.textAlign = 'center';
  374.  
  375. const video = GM_addElement(stage, 'video', {
  376. controls: 'controls',
  377. });
  378. video.style.width = 'auto';
  379. video.style.height = '100%';
  380. const source = GM_addElement(video, 'source');
  381. source.src = videoURL;
  382. source.type = 'video/mp4';
  383. }
  384. });
  385. })();