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