TL.net Stream Direct Open via Middle-Click

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
})();