Greasy Fork 还支持 简体中文。

theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

目前為 2025-01-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name theYNC.com Underground bypass
  3. // @description Watch theYNC Underground videos without needing an account
  4. // @require https://cdn.jsdelivr.net/npm/@trim21/gm-fetch@0.2.1
  5. // @namespace Violentmonkey Scripts
  6. // @match *://*.theync.com/*
  7. // @match *://theync.com/*
  8. // @match *://*.theync.net/*
  9. // @match *://theync.net/*
  10. // @match *://*.theync.org/*
  11. // @match *://theync.org/*
  12. // @match *://archive.ph/*
  13. // @match *://archive.today/*
  14. // @include /https?:\/\/web\.archive\.org\/web\/\d+?\/https?:\/\/theync\.(?:com|org|net)/
  15. // @require https://update.greasyfork.org/scripts/523012/1519437/WaitForKeyElement.js
  16. // @grant GM.xmlHttpRequest
  17. // @connect media.theync.com
  18. // @connect archive.org
  19. // @grant GM_addStyle
  20. // @grant GM_log
  21. // @grant GM_addElement
  22. // @version 7.7
  23. // @supportURL https://greasyfork.org/en/scripts/520352-theync-com-underground-bypass/feedback
  24. // @license MIT
  25. // @author https://greasyfork.org/en/users/1409235-paywalldespiser
  26. // ==/UserScript==
  27.  
  28. /**
  29. * Fetches available archives of a given address.
  30. *
  31. * @param {string} address
  32. * @returns {Promise<Response>}
  33. */
  34. function fetchArchive(address) {
  35. try {
  36. const url = new URL('https://archive.org/wayback/available');
  37. url.searchParams.append('url', address);
  38. return GM_fetch(url, {
  39. method: 'GET',
  40. });
  41. } catch (e) {
  42. return Promise.reject();
  43. }
  44. }
  45.  
  46. /**
  47. * Fetches available archives of a given address and retrieves their URLs.
  48. *
  49. * @param {string} address
  50. * @returns {Promise<string>}
  51. */
  52. function queryArchive(address) {
  53. return fetchArchive(address)
  54. .then((archiveResponse) => {
  55. if (!archiveResponse.ok) {
  56. console.error(archiveResponse);
  57. return Promise.reject(archiveResponse);
  58. }
  59. return archiveResponse;
  60. })
  61. .then((archiveResponse) => archiveResponse.json())
  62. .then(({ archived_snapshots }) => {
  63. if (archived_snapshots.closest) {
  64. return archived_snapshots.closest.url;
  65. }
  66. return Promise.reject(archived_snapshots.closest?.url);
  67. })
  68. .then((url) => {
  69. // Avoid "Mixed content"
  70. if (location.protocol === 'https:') {
  71. return url.replace(/^http:\/\//i, 'https://');
  72. }
  73. return url;
  74. });
  75. }
  76.  
  77. /**
  78. * Checks whether a URL is valid and accessible.
  79. *
  80. * @param {string} address
  81. * @returns {Promise<string>}
  82. */
  83. function isValidURL(address) {
  84. if (address) {
  85. try {
  86. const url = new URL(address);
  87. return GM_fetch(url, { method: 'HEAD' }).then((response) => {
  88. if (response.ok) {
  89. return address;
  90. }
  91. return Promise.reject(address);
  92. });
  93. } catch {
  94. return Promise.reject(address);
  95. }
  96. }
  97. return Promise.reject(address);
  98. }
  99.  
  100. /**
  101. * Tries to guess the video URL of a given theYNC video via the thumbnail URL.
  102. * Only works on videos published before around May 2023.
  103. *
  104. * @param {Element} element
  105. * @returns {string | undefined}
  106. */
  107. function getTheYNCVideoURL(element) {
  108. const thumbnailURL = element.querySelector('.image > img')?.src;
  109. if (!thumbnailURL) return;
  110. for (const [, group_url] of thumbnailURL.matchAll(
  111. /^https?:\/\/theync\.(?:com|org|net)\/media\/thumbs\/(.+?)\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v)/gim
  112. )) {
  113. if (group_url) {
  114. return `https://media.theync.com/videos/${group_url}.mp4`;
  115. }
  116. }
  117. }
  118.  
  119. /**
  120. * Retrieves the video URL from a theYNC video page
  121. *
  122. * @param {Element} element
  123. * @returns {string | undefined}
  124. */
  125. function retrieveVideoURL(element = document) {
  126. if (location.host === 'archive.ph' || location.host === 'archive.today') {
  127. const videoElement = element.querySelector(
  128. '[id="thisPlayer"] video[old-src]'
  129. );
  130. if (videoElement) {
  131. return videoElement.getAttribute('old-src');
  132. }
  133. }
  134. const videoSrc = element.querySelector(
  135. '.stage-video > .inner-stage video[src]'
  136. )?.src;
  137. if (videoSrc) {
  138. return videoSrc;
  139. }
  140. const playerSetupScript = element.querySelector(
  141. '[id=thisPlayer] + script'
  142. )?.textContent;
  143. if (playerSetupScript) {
  144. // TODO: Find a non-regex solution to this that doesn't involve eval
  145. for (const [, videoURL] of playerSetupScript.matchAll(
  146. /(?<=thisPlayer\.setup\(\{).*?file:\ *"(https?\:\/\/.+?.\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v))"/gms
  147. )) {
  148. if (videoURL) {
  149. return videoURL;
  150. }
  151. }
  152. }
  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_fetch(archiveURL, {
  163. method: 'GET',
  164. })
  165. .then((response) => {
  166. if (!response.ok) {
  167. console.error(response);
  168. return Promise.reject(response);
  169. }
  170. // When the page is loaded convert it to text
  171. return response;
  172. })
  173. .then((response) => response.text())
  174. .then((html) => {
  175. // Initialize the DOM parser
  176. const parser = new DOMParser();
  177.  
  178. // Parse the text
  179. const doc = parser.parseFromString(html, 'text/html');
  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. * set Link of element
  194. *
  195. * @param {HTMLLinkElement} element
  196. * @param {string} url
  197. */
  198. function setElementLink(element, url) {
  199. element.href = url;
  200. element.target = '_blank';
  201. element.rel = 'noopener noreferrer';
  202. }
  203.  
  204. (function () {
  205. 'use strict';
  206.  
  207. const allowedExtensions = [
  208. 'flv',
  209. 'mpg',
  210. 'wmv',
  211. 'avi',
  212. '3gp',
  213. 'qt',
  214. 'mp4',
  215. 'mov',
  216. 'm4v',
  217. 'f4v',
  218. ];
  219.  
  220. GM_addStyle(`
  221. .loader {
  222. border: 0.25em solid #f3f3f3;
  223. border-top: 0.25em solid rgba(0, 0, 0, 0);
  224. border-radius: 50%;
  225. width: 1em;
  226. height: 1em;
  227. animation: spin 2s linear infinite;
  228. }
  229. @keyframes spin {
  230. 0% {
  231. transform: rotate(0deg);
  232. }
  233. 100% {
  234. transform: rotate(360deg);
  235. }
  236. }
  237. .border-gold {
  238. display: flex !important;
  239. align-items: center;
  240. justify-content: center;
  241. gap: 1em;
  242. }
  243. `);
  244.  
  245. waitForKeyElement(
  246. '[id="content"],[id="related-videos"] .content-block'
  247. ).then((contentBlock) => {
  248. for (const element of contentBlock.querySelectorAll(
  249. '.upgrade-profile > .upgrade-info-block > .image-block'
  250. )) {
  251. isValidURL(getTheYNCVideoURL(element)).then(
  252. (url) => (location.href = url)
  253. );
  254. }
  255. for (const element of contentBlock.querySelectorAll(
  256. '.inner-block > a'
  257. )) {
  258. const undergroundLogo = element.querySelector(
  259. '.item-info > .border-gold'
  260. );
  261. if (!undergroundLogo) {
  262. continue;
  263. }
  264. const loadingElement = document.createElement('div');
  265. loadingElement.classList.add('loader');
  266. undergroundLogo.appendChild(loadingElement);
  267. isValidURL(getTheYNCVideoURL(element))
  268. .then(
  269. (url) => {
  270. undergroundLogo.textContent = 'BYPASSED';
  271. undergroundLogo.style.backgroundColor = 'green';
  272. setElementLink(element, url);
  273. },
  274. () =>
  275. ['com', 'org', 'net']
  276. .reduce(
  277. (accumulator, currentTLD) =>
  278. accumulator.catch(() =>
  279. queryArchive(
  280. element.href.replace(
  281. /(^https?:\/\/theync\.)(com|org|net)(\/.*$)/gim,
  282. `$1${currentTLD}$3`
  283. )
  284. )
  285. ),
  286. Promise.reject()
  287. )
  288. .then(
  289. (archiveURL) =>
  290. getVideoURLFromArchive(archiveURL).then(
  291. (videoURL) => {
  292. undergroundLogo.textContent =
  293. 'ARCHIVED';
  294. undergroundLogo.style.backgroundColor =
  295. 'blue';
  296. setElementLink(element, videoURL);
  297. },
  298. () => {
  299. undergroundLogo.textContent =
  300. 'MAYBE ARCHIVED';
  301. undergroundLogo.style.backgroundColor =
  302. 'aqua';
  303. setElementLink(element, archiveURL);
  304. }
  305. ),
  306. () => {
  307. undergroundLogo.textContent =
  308. 'Try archive.today';
  309. undergroundLogo.style.backgroundColor =
  310. 'red';
  311. setElementLink(
  312. element,
  313. `https://archive.ph/${element.href}`
  314. );
  315.  
  316. GM_log(
  317. `No bypass or archive found for ${element.href}`
  318. );
  319. }
  320. )
  321. )
  322.  
  323. .finally(() => loadingElement.remove());
  324. }
  325. });
  326. waitForKeyElement('[id="stage"]:has([id="thisPlayer"])').then((stage) => {
  327. const videoURL = retrieveVideoURL();
  328. if (videoURL) {
  329. stage.innerHTML = '';
  330. stage.style.textAlign = 'center';
  331.  
  332. const video = GM_addElement(stage, 'video', {
  333. controls: 'controls',
  334. });
  335. video.style.width = 'auto';
  336. video.style.height = '100%';
  337. const source = GM_addElement(video, 'source');
  338. source.src = videoURL;
  339. source.type = 'video/mp4';
  340. }
  341. });
  342. })();