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