// ==UserScript==
// @name theYNC.com Underground bypass
// @description Watch theYNC Underground videos without needing an account
// @require https://cdn.jsdelivr.net/npm/@trim21/[email protected]
// @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
// @version 6.8
// @supportURL https://gf.qytechs.cn/en/scripts/520352-theync-com-underground-bypass/feedback
// @license MIT
// @author -
// ==/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();
});
}
(() => {
'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';
element.href = 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';
element.href = videoURL;
},
() => {
undergroundLogo.textContent =
'MAYBE ARCHIVED';
undergroundLogo.style.backgroundColor =
'aqua';
element.href = 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', (event) => {
const videoURL = retrieveVideoURL();
if (videoURL) {
location.href = videoURL;
}
});
element.appendChild(downloadButton);
});
})();