theYNC.com Underground bypass

Watch theYNC Underground videos without needing an account

2025-01-01 기준 버전입니다. 최신 버전을 확인하세요.

// ==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     4.9
// @license     MIT
// @author      -
// ==/UserScript==

'use strict';

/**
 * Description placeholder
 *
 * @param {string} selector
 * @returns {Promise<Element>}
 */
function waitForElement(selector) {
    return new Promise((resolve) => {
        {
            const element = document.querySelector(selector);
            if (element) {
                return resolve(element);
            }
        }

        const observer = new MutationObserver(() => {
            const element = document.querySelector(selector);
            if (element) {
                observer.disconnect();
                resolve(element);
            }
        });

        // If you get 'parameter 1 is not of type 'Node'' error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true,
        });
    });
}

/**
 * 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;
        });
}

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;
}
`);

/**
 * 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 fetch(url, { method: 'HEAD' }).then((response) => {
                if (response.ok) {
                    return address;
                }
                return Promise.reject(address);
            });
        } catch (e) {
            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 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 archivedScript = doc.querySelector(
                '[id=thisPlayer] + script'
            )?.textContent;
            if (archivedScript) {
                for (const [, videoURL] of archivedScript.matchAll(
                    /thisPlayer\.setup.*?file: "(?:https?\:\/\/web.archive.org\/web\/\d*?\/)?(https?\:\/\/.*?.\.(?:flv|mpg|wmv|avi|3gp|qt|mp4|mov|m4v|f4v))"/gms
                )) {
                    if (videoURL) {
                        return decodeURIComponent(videoURL);
                    }
                }
            }
            return Promise.reject();
        });
}

const allowedExtensions = [
    'flv',
    'mpg',
    'wmv',
    'avi',
    '3gp',
    'qt',
    'mp4',
    'mov',
    'm4v',
    'f4v',
];

waitForElement('.content-block').then((contentBlock) => {
    for (const element of contentBlock.querySelectorAll(
        '.inner-block > a:has(> .item-info > .border-gold)'
    )) {
        const undergroundLogo = element.querySelector(
            '.item-info > .border-gold'
        );
        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());
    }
});