Greasy Fork 还支持 简体中文。

theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

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

  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. // @grant GM.xmlHttpRequest
  13. // @connect media.theync.com
  14. // @connect archive.org
  15. // @grant GM_addStyle
  16. // @grant GM_log
  17. // @version 6.2
  18. // @license MIT
  19. // @author -
  20. // ==/UserScript==
  21.  
  22. /**
  23. * Waits for a element of a given selector.
  24. *
  25. * @param {string} selector
  26. * @returns {Promise<Element>}
  27. */
  28. function waitForElement(selector) {
  29. return new Promise((resolve) => {
  30. {
  31. const element = document.querySelector(selector);
  32. if (element) {
  33. return resolve(element);
  34. }
  35. }
  36.  
  37. const observer = new MutationObserver((mutations) => {
  38. for (const mutation of mutations) {
  39. if (mutation.type !== 'childList' || !mutation.addedNodes) {
  40. continue;
  41. }
  42. for (const addedNode of mutation.addedNodes) {
  43. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  44. if (addedNode.matches(selector)) {
  45. observer.disconnect();
  46. return resolve(element);
  47. }
  48. const childElement = addedNode.querySelector(selector);
  49. if (childElement) {
  50. observer.disconnect();
  51. return resolve(childElement);
  52. }
  53. }
  54. }
  55. }
  56. });
  57.  
  58. // If you get 'parameter 1 is not of type 'Node'' error, see https://stackoverflow.com/a/77855838/492336
  59. observer.observe(document.body, {
  60. childList: true,
  61. subtree: true,
  62. });
  63. });
  64. }
  65.  
  66. /**
  67. * Fetches available archives of a given address.
  68. *
  69. * @param {string} address
  70. * @returns {Promise<Response>}
  71. */
  72. function fetchArchive(address) {
  73. try {
  74. const url = new URL('https://archive.org/wayback/available');
  75. url.searchParams.append('url', address);
  76. return GM_fetch(url, {
  77. method: 'GET',
  78. });
  79. } catch (e) {
  80. return Promise.reject();
  81. }
  82. }
  83.  
  84. /**
  85. * Fetches available archives of a given address and retrieves their URLs.
  86. *
  87. * @param {string} address
  88. * @returns {Promise<string>}
  89. */
  90. function queryArchive(address) {
  91. return fetchArchive(address)
  92. .then((archiveResponse) => {
  93. if (!archiveResponse.ok) {
  94. console.error(archiveResponse);
  95. return Promise.reject(archiveResponse);
  96. }
  97. return archiveResponse;
  98. })
  99. .then((archiveResponse) => archiveResponse.json())
  100. .then(({ archived_snapshots }) => {
  101. if (archived_snapshots.closest) {
  102. return archived_snapshots.closest.url;
  103. }
  104. return Promise.reject(archived_snapshots.closest?.url);
  105. })
  106. .then((url) => {
  107. // Avoid "Mixed content"
  108. if (location.protocol === 'https:') {
  109. return url.replace(/^http:\/\//i, 'https://');
  110. }
  111. return url;
  112. });
  113. }
  114.  
  115. /**
  116. * Gets the comments given a video id
  117. *
  118. * @param {number} id
  119. * @returns {Promise<string>}
  120. */
  121. function getComments(id) {
  122. const url = new URL(
  123. 'https://theync.com/templates/theync/template.ajax_comments.php'
  124. );
  125. url.searchParams.append('id', id);
  126. url.searchParams.append('time', new Date().getTime());
  127. return GM_fetch(url, { method: 'GET' })
  128. .then((response) => response.text())
  129. .then((text) => {
  130. // Initialize the DOM parser
  131. const parser = new DOMParser();
  132.  
  133. // Parse the text
  134. return parser.parseFromString(text, 'text/html');
  135. });
  136. }
  137.  
  138. /**
  139. * Checks whether a URL is valid and accessible.
  140. *
  141. * @param {string} address
  142. * @returns {Promise<string>}
  143. */
  144. function isValidURL(address) {
  145. if (address) {
  146. try {
  147. const url = new URL(address);
  148. return GM_fetch(url, { method: 'HEAD' }).then((response) => {
  149. if (response.ok) {
  150. return address;
  151. }
  152. return Promise.reject(address);
  153. });
  154. } catch {
  155. return Promise.reject(address);
  156. }
  157. }
  158. return Promise.reject(address);
  159. }
  160.  
  161. /**
  162. * Tries to guess the video URL of a given theYNC video via the thumbnail URL.
  163. * Only works on videos published before around May 2023.
  164. *
  165. * @param {Element} element
  166. * @returns {string | undefined}
  167. */
  168. function getTheYNCVideoURL(element) {
  169. const thumbnailURL = element.querySelector('.image > img')?.src;
  170. if (!thumbnailURL) return;
  171. for (const [, group_url] of thumbnailURL.matchAll(
  172. /^https?:\/\/theync\.(?:com|org|net)\/media\/thumbs\/(.+?)\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v)/gim
  173. )) {
  174. if (group_url) {
  175. return `https://media.theync.com/videos/${group_url}.mp4`;
  176. }
  177. }
  178. }
  179.  
  180. /**
  181. * Retrieves the video URL from a theYNC video page
  182. *
  183. * @param {Element} element
  184. * @returns {string | undefined}
  185. */
  186. function retrieveVideoURL(element = document) {
  187. const archivedScript = element.querySelector(
  188. '[id=thisPlayer] + script'
  189. )?.textContent;
  190. if (archivedScript) {
  191. for (const [, videoURL] of archivedScript.matchAll(
  192. /(?<=thisPlayer\.setup\(\{).*?file:\ *"(https?\:\/\/.+?.\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v))"/gms
  193. )) {
  194. if (videoURL) {
  195. return videoURL;
  196. }
  197. }
  198. }
  199. }
  200.  
  201. /**
  202. * Retrieves the video URL from an archived YNC URL
  203. *
  204. * @param {string} archiveURL
  205. * @returns {Promise<string>}
  206. */
  207. function getVideoURLFromArchive(archiveURL) {
  208. return GM_fetch(archiveURL, {
  209. method: 'GET',
  210. })
  211. .then((response) => {
  212. if (!response.ok) {
  213. console.error(response);
  214. return Promise.reject(response);
  215. }
  216. // When the page is loaded convert it to text
  217. return response;
  218. })
  219. .then((response) => response.text())
  220. .then((html) => {
  221. // Initialize the DOM parser
  222. const parser = new DOMParser();
  223.  
  224. // Parse the text
  225. const doc = parser.parseFromString(html, 'text/html');
  226.  
  227. // You can now even select part of that html as you would in the regular DOM
  228. // Example:
  229. // const docArticle = doc.querySelector('article').innerHTML
  230. const videoURL = retrieveVideoURL(doc);
  231. if (videoURL) {
  232. return videoURL;
  233. }
  234. return Promise.reject();
  235. });
  236. }
  237.  
  238. (() => {
  239. 'use strict';
  240.  
  241. const allowedExtensions = [
  242. 'flv',
  243. 'mpg',
  244. 'wmv',
  245. 'avi',
  246. '3gp',
  247. 'qt',
  248. 'mp4',
  249. 'mov',
  250. 'm4v',
  251. 'f4v',
  252. ];
  253.  
  254. GM_addStyle(`
  255. .loader {
  256. border: 0.25em solid #f3f3f3;
  257. border-top-width: 0.25em;
  258. border-top-style: solid;
  259. border-top-color: hsl(0, 0%, 95.3%);
  260. border-top: 0.25em solid rgb(0, 0, 0);
  261. border-radius: 50%;
  262. width: 1em;
  263. height: 1em;
  264. animation: spin 2s linear infinite;
  265. }
  266. @keyframes spin {
  267. 0% {
  268. transform: rotate(0deg);
  269. }
  270. 100% {
  271. transform: rotate(360deg);
  272. }
  273. }
  274. .border-gold {
  275. display: flex !important;
  276. align-items: center;
  277. justify-content: center;
  278. gap: 1em;
  279. }
  280. `);
  281.  
  282. waitForElement('[id="content"],[id="related-videos"] .content-block').then(
  283. (contentBlock) => {
  284. for (const element of contentBlock.querySelectorAll(
  285. '.upgrade-profile > .upgrade-info-block > .image-block'
  286. )) {
  287. isValidURL(getTheYNCVideoURL(element)).then(
  288. (url) => (location.href = url)
  289. );
  290. }
  291. for (const element of contentBlock.querySelectorAll(
  292. '.inner-block > a'
  293. )) {
  294. const undergroundLogo = element.querySelector(
  295. '.item-info > .border-gold'
  296. );
  297. if (!undergroundLogo) {
  298. continue;
  299. }
  300. const loadingElement = document.createElement('div');
  301. loadingElement.classList.add('loader');
  302. undergroundLogo.appendChild(loadingElement);
  303. isValidURL(getTheYNCVideoURL(element))
  304. .then(
  305. (url) => {
  306. undergroundLogo.textContent = 'BYPASSED';
  307. undergroundLogo.style.backgroundColor = 'green';
  308. element.href = url;
  309. },
  310. () =>
  311. ['com', 'org', 'net']
  312. .reduce(
  313. (accumulator, currentTLD) =>
  314. accumulator.catch(() =>
  315. queryArchive(
  316. element.href.replace(
  317. /(^https?:\/\/theync\.)(com|org|net)(\/.*$)/gim,
  318. `$1${currentTLD}$3`
  319. )
  320. )
  321. ),
  322. Promise.reject()
  323. )
  324. .then(
  325. (url) =>
  326. getVideoURLFromArchive(url).then(
  327. (videoURL) => {
  328. undergroundLogo.textContent =
  329. 'ARCHIVED';
  330. undergroundLogo.style.backgroundColor =
  331. 'blue';
  332. element.href = videoURL;
  333. },
  334. () => {
  335. undergroundLogo.textContent =
  336. 'MAYBE ARCHIVED';
  337. undergroundLogo.style.backgroundColor =
  338. 'aqua';
  339. element.href = url;
  340. }
  341. ),
  342. () =>
  343. GM_log(
  344. `No bypass or archive found for ${element.href}`
  345. )
  346. )
  347. )
  348.  
  349. .finally(() => loadingElement.remove());
  350. }
  351. }
  352. );
  353. waitForElement('.jw-controlbar-right-group').then((element) => {
  354. const downloadButton = document.createElement('div');
  355. downloadButton.classList.add('fa', 'fa-arrow-down');
  356. downloadButton.addEventListener('click', (event) => {
  357. const videoURL = retrieveVideoURL();
  358. if (videoURL) {
  359. location.href = videoURL;
  360. }
  361. });
  362. element.appendChild(downloadButton);
  363. });
  364. })();