TL.net Stream Direct Open via Middle-Click

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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    https://greasyfork.org/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);
})();