Greasy Fork

Greasy Fork is available in English.

TL.net Stream Direct Open via Middle-Click

Open stream directly by middle-clicking any stream on tl.net (Teamliquid)

// ==UserScript==
// @name         TL.net Stream Direct Open via Middle-Click
// @description  Open stream directly by middle-clicking any stream on tl.net (Teamliquid)
// @author       NWP
// @namespace    http://greasyfork.icu/users/877912
// @version      0.2
// @license      MIT
// @compatible   Chrome
// @compatible   Firefox
// @match        https://tl.net/*
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        window.open
// ==/UserScript==

(function() {
    'use strict';

    const isDebugMode = false;
    const CLICK_COOLDOWN_MS = 200;

    function debugLog(...args) { if (isDebugMode) console.log('[SMC]', ...args); }
    function debugWarn(...args) { if (isDebugMode) console.warn('[SMC]', ...args); }
    function debugError(...args) { if (isDebugMode) console.error('[SMC]', ...args); }

    const activeFetchUrls = new Set();               
    const clickTimestamps = new Map();
    let lastStreamsHtmlSnapshot = null;

    function handleAuxClick(event) {
        if (event.button !== 1 || event.type !== 'auxclick') return;
        debugLog('Middle-click detected');
        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();

        // Capture and dedupe page snapshot
        const streamsContainer = document.getElementById('streams_content');
        const currentHtml = streamsContainer ? streamsContainer.innerHTML.trim() : '';
        const timestampIso = new Date().toISOString();
        if (currentHtml !== lastStreamsHtmlSnapshot) {
            lastStreamsHtmlSnapshot = currentHtml;
            debugLog(`Snapshot updated at ${timestampIso} (length=${currentHtml.length})`);
        } else {
            debugLog(`Snapshot unchanged at ${timestampIso}`);
        }

        // Identify the clicked stream link element
        const linkElement = event.target.closest('a.rightmenu');
        if (!linkElement) {
            debugWarn('Clicked element is not a stream link');
            return;
        }
        const linkUrl = linkElement.href;
        debugLog('Clicked link URL:', linkUrl);

        // If link is external (not a TL stream page), open directly
        if (!linkUrl.includes('/video/streams/')) {
            debugLog('Opening external link directly:', linkUrl);
            const newTab = window.open(linkUrl, '_blank');
            if (newTab) newTab.opener = null;
            return;
        }

        // Avoid duplicate fetches and enforce cooldown
        if (activeFetchUrls.has(linkUrl)) return;
        const nowMs = Date.now();
        if (nowMs - (clickTimestamps.get(linkUrl) || 0) < CLICK_COOLDOWN_MS) return;
        clickTimestamps.set(linkUrl, nowMs);
        activeFetchUrls.add(linkUrl);

        // Fetch TL page to extract final stream URL
        debugLog('Fetching TL page for:', linkUrl);
        GM_xmlhttpRequest({
            method: 'GET',
            url: linkUrl,
            onload(response) {
                activeFetchUrls.delete(linkUrl);
                if (response.status !== 200) {
                    debugWarn('Failed to fetch TL page:', response.status);
                    return;
                }
                try {
                    const parser = new DOMParser();
                    const responseDocument = parser.parseFromString(response.responseText, 'text/html');
                    const viewOnLink = Array.from(responseDocument.querySelectorAll('a'))
                        .find(a => a.textContent.trim().startsWith('View on'));
                    if (viewOnLink) {
                        const finalStreamUrl = viewOnLink.href;
                        debugLog('Opening final URL:', finalStreamUrl);
                        const newTab = window.open(finalStreamUrl, '_blank');
                        if (newTab) newTab.opener = null;
                    } else {
                        debugWarn('No "View on" link found in TL response page');
                    }
                } catch (parseError) {
                    debugError('Error parsing TL response page:', parseError);
                }
            },
            onerror() {
                activeFetchUrls.delete(linkUrl);
                debugWarn('Network error during fetch of TL page');
            }
        });
    }

    // Ensure each stream-link element only gets one listener
    const processedLinks = new WeakSet();
    function attachListeners() {
        const streamLinkElements = document.querySelectorAll('#streams_content a.rightmenu, #non-featured a.rightmenu');
        const newlyAdded = [];
        streamLinkElements.forEach(element => {
            if (!processedLinks.has(element)) {
                processedLinks.add(element);
                element.addEventListener('auxclick', handleAuxClick, true);
                newlyAdded.push(element);
            }
        });
        if (newlyAdded.length > 0) {
            debugLog(`Listener added for ${newlyAdded.length} link(s)`);
        }
    }

    // Observe dynamic changes and attach listeners
    const domObserver = new MutationObserver(attachListeners);
    document.addEventListener('DOMContentLoaded', () => {
        attachListeners();
        const container = document.getElementById('streams_content');
        if (container) domObserver.observe(document.body, { childList: true, subtree: true });
    });
    window.addEventListener('load', attachListeners);
})();