theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

目前为 2025-01-06 提交的版本,查看 最新版本

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