- // ==UserScript==
- // @name theYNC.com Underground bypass
- // @description Watch theYNC Underground videos without needing an account
- // @require https://cdn.jsdelivr.net/npm/@trim21/gm-fetch@0.2.1
- // @namespace Violentmonkey Scripts
- // @match *://*.theync.com/*
- // @match *://theync.com/*
- // @match *://*.theync.net/*
- // @match *://theync.net/*
- // @match *://*.theync.org/*
- // @match *://theync.org/*
- // @grant GM.xmlHttpRequest
- // @connect media.theync.com
- // @connect archive.org
- // @grant GM_addStyle
- // @grant GM_log
- // @grant GM_openInTab
- // @version 7.0
- // @supportURL https://greasyfork.org/en/scripts/520352-theync-com-underground-bypass/feedback
- // @license MIT
- // @author https://greasyfork.org/en/users/1409235-paywalldespiser
- // ==/UserScript==
-
- /**
- * Waits for a element of a given selector.
- *
- * @param {string} selector
- * @param {Element} target
- * @returns {Promise<Element>}
- */
- function waitForKeyElement(selector, target = document.body) {
- return new Promise((resolve) => {
- {
- const element = target.querySelector(selector);
- if (element) {
- return resolve(element);
- }
- }
-
- const observer = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (!(node instanceof HTMLElement)) continue;
-
- if (node.matches(selector)) {
- observer.disconnect();
- resolve(node);
- return;
- }
- const childElement = node.querySelector(selector);
- if (childElement) {
- observer.disconnect();
- resolve(childElement);
- return;
- }
- }
- }
- });
-
- observer.observe(target, {
- childList: true,
- subtree: true,
- attributes: false,
- characterData: false,
- });
- });
- }
-
- /**
- * Fetches available archives of a given address.
- *
- * @param {string} address
- * @returns {Promise<Response>}
- */
- function fetchArchive(address) {
- try {
- const url = new URL('https://archive.org/wayback/available');
- url.searchParams.append('url', address);
- return GM_fetch(url, {
- method: 'GET',
- });
- } catch (e) {
- return Promise.reject();
- }
- }
-
- /**
- * Fetches available archives of a given address and retrieves their URLs.
- *
- * @param {string} address
- * @returns {Promise<string>}
- */
- function queryArchive(address) {
- return fetchArchive(address)
- .then((archiveResponse) => {
- if (!archiveResponse.ok) {
- console.error(archiveResponse);
- return Promise.reject(archiveResponse);
- }
- return archiveResponse;
- })
- .then((archiveResponse) => archiveResponse.json())
- .then(({ archived_snapshots }) => {
- if (archived_snapshots.closest) {
- return archived_snapshots.closest.url;
- }
- return Promise.reject(archived_snapshots.closest?.url);
- })
- .then((url) => {
- // Avoid "Mixed content"
- if (location.protocol === 'https:') {
- return url.replace(/^http:\/\//i, 'https://');
- }
- return url;
- });
- }
-
- /**
- * Gets the comments given a video id
- *
- * @param {number} id
- * @returns {Promise<string>}
- */
- function getComments(id) {
- const url = new URL(
- 'https://theync.com/templates/theync/template.ajax_comments.php'
- );
- url.searchParams.append('id', id);
- url.searchParams.append('time', new Date().getTime());
- return GM_fetch(url, { method: 'GET' })
- .then((response) => response.text())
- .then((text) => {
- // Initialize the DOM parser
- const parser = new DOMParser();
-
- // Parse the text
- return parser.parseFromString(text, 'text/html');
- });
- }
-
- /**
- * Checks whether a URL is valid and accessible.
- *
- * @param {string} address
- * @returns {Promise<string>}
- */
- function isValidURL(address) {
- if (address) {
- try {
- const url = new URL(address);
- return GM_fetch(url, { method: 'HEAD' }).then((response) => {
- if (response.ok) {
- return address;
- }
- return Promise.reject(address);
- });
- } catch {
- return Promise.reject(address);
- }
- }
- return Promise.reject(address);
- }
-
- /**
- * Tries to guess the video URL of a given theYNC video via the thumbnail URL.
- * Only works on videos published before around May 2023.
- *
- * @param {Element} element
- * @returns {string | undefined}
- */
- function getTheYNCVideoURL(element) {
- const thumbnailURL = element.querySelector('.image > img')?.src;
- if (!thumbnailURL) return;
- for (const [, group_url] of thumbnailURL.matchAll(
- /^https?:\/\/theync\.(?:com|org|net)\/media\/thumbs\/(.+?)\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v)/gim
- )) {
- if (group_url) {
- return `https://media.theync.com/videos/${group_url}.mp4`;
- }
- }
- }
-
- /**
- * Retrieves the video URL from a theYNC video page
- *
- * @param {Element} element
- * @returns {string | undefined}
- */
- function retrieveVideoURL(element = document) {
- const archivedScript = element.querySelector(
- '[id=thisPlayer] + script'
- )?.textContent;
- if (archivedScript) {
- // TODO: Find a non-regex solution to this that doesn't involve eval
- for (const [, videoURL] of archivedScript.matchAll(
- /(?<=thisPlayer\.setup\(\{).*?file:\ *"(https?\:\/\/.+?.\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v))"/gms
- )) {
- if (videoURL) {
- return videoURL;
- }
- }
- }
- }
-
- /**
- * Retrieves the video URL from an archived YNC URL
- *
- * @param {string} archiveURL
- * @returns {Promise<string>}
- */
- function getVideoURLFromArchive(archiveURL) {
- return GM_fetch(archiveURL, {
- method: 'GET',
- })
- .then((response) => {
- if (!response.ok) {
- console.error(response);
- return Promise.reject(response);
- }
- // When the page is loaded convert it to text
- return response;
- })
- .then((response) => response.text())
- .then((html) => {
- // Initialize the DOM parser
- const parser = new DOMParser();
-
- // Parse the text
- const doc = parser.parseFromString(html, 'text/html');
-
- // You can now even select part of that html as you would in the regular DOM
- // Example:
- // const docArticle = doc.querySelector('article').innerHTML
- const videoURL = retrieveVideoURL(doc);
- if (videoURL) {
- return videoURL;
- }
- return Promise.reject();
- });
- }
-
- /**
- * set Link of element
- *
- * @param {HTMLLinkElement} element
- * @param {string} url
- */
- function setElementLink(element, url) {
- element.href = url;
- element.target = '_blank';
- element.rel = 'noopener noreferrer';
- }
-
- (() => {
- 'use strict';
-
- const allowedExtensions = [
- 'flv',
- 'mpg',
- 'wmv',
- 'avi',
- '3gp',
- 'qt',
- 'mp4',
- 'mov',
- 'm4v',
- 'f4v',
- ];
-
- GM_addStyle(`
- .loader {
- border: 0.25em solid #f3f3f3;
- border-top-width: 0.25em;
- border-top-style: solid;
- border-top-color: hsl(0, 0%, 95.3%);
- border-top: 0.25em solid rgb(0, 0, 0);
- border-radius: 50%;
- width: 1em;
- height: 1em;
- animation: spin 2s linear infinite;
- }
-
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
- }
-
- .border-gold {
- display: flex !important;
- align-items: center;
- justify-content: center;
- gap: 1em;
- }
- `);
-
- waitForKeyElement(
- '[id="content"],[id="related-videos"] .content-block'
- ).then((contentBlock) => {
- for (const element of contentBlock.querySelectorAll(
- '.upgrade-profile > .upgrade-info-block > .image-block'
- )) {
- isValidURL(getTheYNCVideoURL(element)).then(
- (url) => (location.href = url)
- );
- }
- for (const element of contentBlock.querySelectorAll(
- '.inner-block > a'
- )) {
- const undergroundLogo = element.querySelector(
- '.item-info > .border-gold'
- );
- if (!undergroundLogo) {
- continue;
- }
- const loadingElement = document.createElement('div');
- loadingElement.classList.add('loader');
- undergroundLogo.appendChild(loadingElement);
- isValidURL(getTheYNCVideoURL(element))
- .then(
- (url) => {
- undergroundLogo.textContent = 'BYPASSED';
- undergroundLogo.style.backgroundColor = 'green';
- setElementLink(element, url);
- },
- () =>
- ['com', 'org', 'net']
- .reduce(
- (accumulator, currentTLD) =>
- accumulator.catch(() =>
- queryArchive(
- element.href.replace(
- /(^https?:\/\/theync\.)(com|org|net)(\/.*$)/gim,
- `$1${currentTLD}$3`
- )
- )
- ),
- Promise.reject()
- )
- .then(
- (url) =>
- getVideoURLFromArchive(url).then(
- (videoURL) => {
- undergroundLogo.textContent =
- 'ARCHIVED';
- undergroundLogo.style.backgroundColor =
- 'blue';
- setElementLink(element, videoURL);
- },
- () => {
- undergroundLogo.textContent =
- 'MAYBE ARCHIVED';
- undergroundLogo.style.backgroundColor =
- 'aqua';
- setElementLink(element, url);
- }
- ),
- () =>
- GM_log(
- `No bypass or archive found for ${element.href}`
- )
- )
- )
-
- .finally(() => loadingElement.remove());
- }
- });
- waitForKeyElement('.jw-controlbar-right-group').then((element) => {
- const downloadButton = document.createElement('div');
- downloadButton.style.fontSize = '1.45em';
- downloadButton.classList.add(
- 'jw-icon',
- 'jw-icon-inline',
- 'jw-button-color',
- 'jw-reset',
- 'jw-off'
- );
- downloadButton.textContent = '⇩';
- downloadButton.addEventListener('click', () => {
- const videoURL = retrieveVideoURL();
- if (videoURL) {
- GM_openInTab(videoURL);
- }
- });
- element.appendChild(downloadButton);
- });
- })();