theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

当前为 2025-01-17 提交的版本,查看 最新版本

  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 9.5
  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. for (const [, group_url] of thumbnailURL.matchAll(
  104. /^https?:\/\/(?:media\.theync\.(?:com|org|net)|(www\.)?theync\.(?:com|org|net)\/media)\/thumbs\/(.+?)\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v)/gim
  105. )) {
  106. if (group_url) {
  107. return 'https://media.theync.com/videos/' + group_url + '.mp4';
  108. }
  109. }
  110. }
  111.  
  112. return null;
  113. }
  114.  
  115. /**
  116. * Retrieves the video URL from a theYNC video page
  117. *
  118. * @param {Element} [element=document]
  119. * @returns {string?}
  120. */
  121. function retrieveVideoURL(element = document) {
  122. if (location.host === 'archive.ph' || location.host === 'archive.today') {
  123. const attribute = element
  124. .querySelector('[id="thisPlayer"] video[old-src]')
  125. ?.getAttribute('old-src');
  126. if (attribute) {
  127. return attribute;
  128. }
  129. }
  130. /**
  131. * @type {string | null | undefined}
  132. */
  133. const videoSrc = element.querySelector(
  134. '.stage-video > .inner-stage video[src]'
  135. )?.src;
  136. if (videoSrc) {
  137. return videoSrc;
  138. }
  139. const playerSetupScript = element.querySelector(
  140. '[id=thisPlayer] + script'
  141. )?.textContent;
  142. if (playerSetupScript) {
  143. // TODO: Find a non-regex solution to this that doesn't involve eval
  144. for (const [, videoURL] of playerSetupScript.matchAll(
  145. /(?<=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))"/gim
  146. )) {
  147. if (videoURL) {
  148. return decodeURIComponent(videoURL);
  149. }
  150. }
  151. }
  152. return null;
  153. }
  154.  
  155. /**
  156. * Retrieves the video URL from an archived YNC URL
  157. *
  158. * @param {string} archiveURL
  159. * @returns {Promise<string>}
  160. */
  161. function getVideoURLFromArchive(archiveURL) {
  162. return GM.xmlHttpRequest({ url: archiveURL, method: 'GET' })
  163. .then((result) => {
  164. if (result.status >= 300) {
  165. console.error(result.status);
  166. return Promise.reject(result);
  167. }
  168. return result;
  169. })
  170.  
  171. .then((result) => {
  172. // Initialize the DOM parser
  173. const parser = new DOMParser();
  174.  
  175. // Parse the text
  176. const doc = parser.parseFromString(
  177. result.responseText,
  178. 'text/html'
  179. );
  180.  
  181. // You can now even select part of that html as you would in the regular DOM
  182. // Example:
  183. // const docArticle = doc.querySelector('article').innerHTML
  184. const videoURL = retrieveVideoURL(doc);
  185. if (videoURL) {
  186. return videoURL;
  187. }
  188. return Promise.reject();
  189. });
  190. }
  191.  
  192.  
  193. /**
  194. * Description placeholder
  195. *
  196. * @param {number} ms
  197. * @returns {Promise<void>}
  198. */
  199. const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  200. (function () {
  201. 'use strict';
  202.  
  203. const allowedExtensions = [
  204. 'flv',
  205. 'mpg',
  206. 'wmv',
  207. 'avi',
  208. '3gp',
  209. 'qt',
  210. 'mp4',
  211. 'mov',
  212. 'm4v',
  213. 'f4v',
  214. ];
  215.  
  216. GM_addStyle(`
  217. .loader {
  218. border: 0.25em solid #f3f3f3;
  219. border-top: 0.25em solid rgba(0, 0, 0, 0);
  220. border-radius: 50%;
  221. width: 1em;
  222. height: 1em;
  223. animation: spin 2s linear infinite;
  224. }
  225. @keyframes spin {
  226. 0% {
  227. transform: rotate(0deg);
  228. }
  229. 100% {
  230. transform: rotate(360deg);
  231. }
  232. }
  233. .border-gold {
  234. display: flex !important;
  235. align-items: center;
  236. justify-content: center;
  237. gap: 1em;
  238. }
  239. `);
  240.  
  241. waitForKeyElement(
  242. '[id="content"],[id="related-videos"] .content-block'
  243. ).then((contentBlock) =>
  244. Promise.all(
  245. Array.from(
  246. contentBlock.querySelectorAll(
  247. '.inner-block > a:has(.item-info > .border-gold)'
  248. )
  249. ).map((element) => {
  250. const undergroundLogo = element.querySelector(
  251. '.item-info > .border-gold'
  252. );
  253.  
  254. const loadingElement = GM_addElement('div');
  255. loadingElement.classList.add('loader');
  256. undergroundLogo.appendChild(loadingElement);
  257. return isValidURL(getTheYNCVideoURL(element))
  258. .then(
  259. (url) => ({
  260. url: url,
  261. text: 'BYPASSED',
  262. color: 'green',
  263. }),
  264.  
  265. () => {
  266. /**
  267. * @type {RegExpMatchArray | null}
  268. */
  269. const match = element.href.match(
  270. /(^https?:\/\/(?:www\.)?theync\.)(?:com|org|net)(\/.*$)/im
  271. );
  272. if (!match?.[1]) {
  273. return Promise.reject(
  274. 'Error with the URL: ' + element.href
  275. );
  276. }
  277. const [, secondLevelDomain, path] = match;
  278.  
  279. return ['com', 'org', 'net']
  280. .reduce(
  281. /**
  282. * @param {Promise<string>} accumulator
  283. * @param {string} currentTLD
  284. * @returns {Promise<string>}
  285. */
  286. (accumulator, currentTLD) =>
  287. accumulator.catch(() =>
  288. wait(1400).then(() =>
  289. queryArchive(
  290. secondLevelDomain +
  291. currentTLD +
  292. path
  293. )
  294. )
  295. ),
  296. Promise.reject()
  297. )
  298. .then((archiveURL) =>
  299. getVideoURLFromArchive(archiveURL).then(
  300. (videoURL) => ({
  301. url: videoURL,
  302. text: 'ARCHIVED',
  303. color: 'blue',
  304. }),
  305. () => ({
  306. url: archiveURL,
  307. text: 'MAYBE ARCHIVED',
  308. color: 'aqua',
  309. })
  310. )
  311. );
  312. }
  313. )
  314. .catch(() => ({
  315. url:
  316. 'https://archive.ph/' +
  317. encodeURIComponent(element.href),
  318. text: 'Try archive.today',
  319. color: 'red',
  320. }))
  321. .then(({ url, text, color }) => {
  322. undergroundLogo.textContent = text;
  323. undergroundLogo.style.backgroundColor = color;
  324. element.href = url;
  325. })
  326. .finally(() => loadingElement.remove());
  327. })
  328. )
  329. );
  330. waitForKeyElement('[id="stage"]:has([id="thisPlayer"])').then((stage) => {
  331. const videoURL = retrieveVideoURL();
  332. if (videoURL) {
  333. stage.innerHTML = '';
  334. stage.style.textAlign = 'center';
  335.  
  336. const video = GM_addElement(stage, 'video', {
  337. controls: 'controls',
  338. });
  339. video.style.width = 'auto';
  340. video.style.height = '100%';
  341. const source = GM_addElement(video, 'source');
  342. source.src = videoURL;
  343. source.type = 'video/mp4';
  344. }
  345. });
  346. })();