SOOP (숲) - 목록 탐색 자동 PIP (V14.29 External Fix)

V14.26 복구 + 검색 드롭다운 패치 + 외부 사이트 임베드 오류 수정

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         SOOP (숲) - 목록 탐색 자동 PIP (V14.29 External Fix)
// @namespace    http://tampermonkey.net/
// @version      14.29
// @description  V14.26 복구 + 검색 드롭다운 패치 + 외부 사이트 임베드 오류 수정
// @author       Gemini
// @license      MIT
// @match        https://play.sooplive.co.kr/*
// @match        https://vod.sooplive.co.kr/*
// @match        https://www.sooplive.co.kr/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================
    // [A] window.open 오버라이딩 (드롭다운 먹통/새창 방지 핵심)
    // ============================================================
    const originalOpen = window.open;
    const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

    const dummyWindow = {
        close: () => {},
        focus: () => {},
        document: { write: () => {}, close: () => {} },
        location: { href: '' }
    };

    function hijackWindowOpen(url, name, specs) {
        if (typeof url === 'string') {
            if (url.includes('/search')) {
                executeSearchUrl(url);
                return dummyWindow;
            }
            if (url.includes('play.sooplive.co.kr') || url.includes('vod.sooplive.co.kr')) {
                window.top.location.href = url;
                return dummyWindow;
            }
        }
        return originalOpen.call(window, url, name, specs);
    }
    window.open = hijackWindowOpen;
    targetWindow.open = hijackWindowOpen;


    // ============================================================
    // [B] 환경 판별
    // ============================================================
    const isIframe = window.self !== window.top;
    const isPlayerDomain = location.hostname.includes('play.sooplive.co.kr') || location.hostname.includes('vod.sooplive.co.kr');

    // ============================================================
    // [C] 탐색창 (Iframe) 내부 로직
    // ============================================================
    if (isIframe) {
        // [수정됨] 외부 사이트 임베드(플레이어)인 경우 스크립트 중단
        // 탐색용 Iframe은 보통 'www.sooplive.co.kr'이고,
        // 외부 임베드 플레이어는 'vod'나 'play' 도메인을 사용합니다.
        if (location.hostname.includes('play.sooplive.co.kr') || location.hostname.includes('vod.sooplive.co.kr')) {
            return; // 여기서 종료하여 video 태그 숨김 방지
        }

        // --------------------------------------------------------
        // [1] CSS UI 보호 (탐색창 리소스 절약)
        // --------------------------------------------------------
        const style = document.createElement('style');
        let cssContent = `
            * { text-rendering: optimizeSpeed !important; }
            video, .thumbs_box .thumb, .broad_thumb, iframe[title*="광고"] { display: none !important; }
            .thumbs_box { contain: layout paint; }
            body { overflow-x: hidden; }
        `;

        if (location.href.includes('/my/favorite')) {
            cssContent += `
                div[class*="list_wrap"], ul {
                    content-visibility: visible !important;
                    contain: none !important;
                }
                .thumbs_box li, .list_wrap li, .cBox {
                    content-visibility: auto;
                    contain-intrinsic-size: 300px;
                    contain: layout paint style;
                    transform: translateZ(0);
                }
            `;
        }
        style.innerHTML = cssContent;
        document.head.appendChild(style);


        // --------------------------------------------------------
        // [2] 네트워크 요청 스로틀링
        // --------------------------------------------------------
        (function installNetworkThrottler() {
            const QUEUE_DELAY_MS = 50;
            const requestQueue = [];
            let isProcessing = false;

            async function processQueue() {
                if (isProcessing || requestQueue.length === 0) return;
                isProcessing = true;
                while (requestQueue.length > 0) {
                    const task = requestQueue.shift();
                    await task();
                    await new Promise(resolve => setTimeout(resolve, QUEUE_DELAY_MS));
                }
                isProcessing = false;
            }

            function isImageRequest(url) {
                return /\.(jpg|jpeg|png|gif|webp|svg)/i.test(url);
            }

            const originalFetch = window.fetch;
            window.fetch = async function(...args) {
                const url = args[0] ? args[0].toString() : '';
                if (isImageRequest(url)) return originalFetch(...args);

                const isTargetApi = url.includes('/api/') || url.includes('station') || url.includes('list');
                if (isTargetApi) {
                    return new Promise((resolve, reject) => {
                        requestQueue.push(async () => {
                            try {
                                const response = await originalFetch(...args);
                                resolve(response);
                            } catch (err) { reject(err); }
                        });
                        processQueue();
                    });
                }
                return originalFetch(...args);
            };

            const originalXhrOpen = XMLHttpRequest.prototype.open;
            const originalXhrSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(method, url) {
                this._url = url;
                return originalXhrOpen.apply(this, arguments);
            };

            XMLHttpRequest.prototype.send = function(body) {
                const url = this._url || '';
                if (isImageRequest(url)) return originalXhrSend.call(this, body);

                const isTargetApi = url.includes('/api/') || url.includes('station') || url.includes('list');
                if (isTargetApi) {
                    requestQueue.push(() => {
                        return new Promise((resolve) => {
                            this.addEventListener('loadend', resolve);
                            originalXhrSend.call(this, body);
                            setTimeout(resolve, 1000);
                        });
                    });
                    processQueue();
                } else {
                    originalXhrSend.call(this, body);
                }
            };
        })();

        // --------------------------------------------------------
        // [3] 테마 및 클릭 핸들러
        // --------------------------------------------------------
        function sendTheme() {
            const isDark = document.documentElement.classList.contains('play-theme-dark') ||
                           document.body.classList.contains('dark') ||
                           getComputedStyle(document.body).backgroundColor === 'rgb(20, 21, 23)';
            window.parent.postMessage({ type: 'SOOP_THEME', isDarkMode: isDark }, '*');
        }
        window.addEventListener('load', sendTheme);
        setInterval(sendTheme, 2000);

        document.addEventListener('click', function(e) {
            const link = e.target.closest('a');
            if (!link) {
                window.parent.postMessage({ type: 'SOOP_BG_CLICK_SIMPLE' }, '*');
                return;
            }
            if (link && link.href) {
                if (link.href.includes('play.sooplive.co.kr') || link.href.includes('vod.sooplive.co.kr')) {
                    e.preventDefault();
                    window.top.location.href = link.href;
                }
            }
        }, true);
        return;
    }

    // ============================================================
    // [D] 메인 플레이어 로직 (부모 창)
    // ============================================================
    if (!isPlayerDomain) return;

    let isPipActive = false;

    // --- 1. CSS 스타일 ---
    GM_addStyle(`
        body.real-pip-mode #player_area {
            position: fixed !important; z-index: 999999 !important;
            width: 480px !important; height: 270px !important;
            bottom: auto !important; right: auto !important;
            border: none !important;
            box-shadow: 0 15px 50px rgba(0,0,0,0.9);
            background: #000; border-radius: 12px; overflow: hidden;
            cursor: move !important;
            contain: strict !important;
            transform: translate3d(0,0,0);
            will-change: transform, top, left;
        }
        body.real-pip-mode #player_area.is-dragging { transition: none !important; }

        body.real-pip-mode #player_area video {
            pointer-events: none !important; object-fit: contain !important;
            width: 100% !important; height: 100% !important;
            transform: translateZ(0);
            backface-visibility: hidden;
            will-change: transform;
        }

        body.real-pip-mode #player_area .player_cover {
            pointer-events: none !important; object-fit: contain !important;
            width: 100% !important; height: 100% !important;
        }
        body.real-pip-mode .play_control_box {
            display: block !important; pointer-events: auto !important;
            bottom: 0 !important; position: absolute !important;
            width: 100% !important; background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
            z-index: 20 !important; cursor: default !important;
        }
        body.real-pip-mode .play_control_box * { pointer-events: auto !important; }

        body.real-pip-mode #web_chatting,
        body.real-pip-mode .header_area,
        body.real-pip-mode .sidebar_area,
        body.real-pip-mode .start_ad_area,
        body.real-pip-mode #action_bar,
        body.real-pip-mode .btn_chat_open,
        body.real-pip-mode .btn_chat_fold,
        body.real-pip-mode .btn_expand,
        body.real-pip-mode button[class*="chat"],
        body.real-pip-mode .chat_layer,
        body.real-pip-mode .btn_sidebar
        { display: none !important; }

        #soop-real-frame {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            z-index: 100; border: none; background: #fff;
            contain: strict;
            will-change: transform;
        }
        body.real-pip-mode #soop-real-frame { display: block; }
        body.real-pip-mode.iframe-dark #soop-real-frame { background: #141517; }

        #pip-control-bar {
            display: none; position: absolute; top: 0; left: 0; width: 100%; height: 40px;
            background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);
            z-index: 1000000; justify-content: flex-end; align-items: center;
            padding-right: 10px; opacity: 0; transition: opacity 0.2s;
            pointer-events: auto !important; cursor: default;
        }
        body.real-pip-mode #player_area:hover #pip-control-bar { opacity: 1; }
        body.real-pip-mode #pip-control-bar { display: flex; }

        .pip-ctrl-btn {
            background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.3);
            color: white; margin-left: 8px; padding: 5px 12px;
            border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold;
        }
        .pip-ctrl-btn:hover { background: rgba(255,255,255,0.3); }
    `);

    // --- 2. 검색 실행 헬퍼 ---
    function executeSearchUrl(url) {
        const frame = document.getElementById('soop-real-frame');

        if (isPipActive && frame) {
            try {
                if (frame.contentWindow.location.href === url) return;
            } catch(e) {}
        }

        const loadTask = () => {
            if (isPipActive) {
                if (frame) frame.src = url;
            } else {
                startPip(url);
            }
        };

        if ('requestIdleCallback' in window) {
            window.requestIdleCallback(loadTask, { timeout: 1000 });
        } else {
            setTimeout(loadTask, 100);
        }

        if (document.activeElement instanceof HTMLElement) {
            document.activeElement.blur();
        }
    }

    function executeSearchKeyword(keyword) {
        if (!keyword || keyword.trim() === '') return;
        const searchUrl = 'https://www.sooplive.co.kr/search?keyword=' + encodeURIComponent(keyword);
        executeSearchUrl(searchUrl);
    }

    // --- 3. Form Submit ---
    document.addEventListener('submit', function(e) {
        const form = e.target;
        const isSearchForm = (form.action && form.action.includes('search')) ||
                             form.querySelector('input[name="szKeyword"]') ||
                             form.querySelector('input[name="keyword"]');

        if (isSearchForm) {
            if (form.target === '_blank') form.removeAttribute('target');
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const input = form.querySelector('input[type="text"]');
            if (input && input.value) {
                executeSearchKeyword(input.value);
            }
            return false;
        }
    }, true);

    // --- 4. 클릭 이벤트 ---
    document.addEventListener('click', function(e) {
        const target = e.target;
        const link = target.closest('a');
        const text = (target.innerText || '').replace(/\s/g, '');

        const searchBtn = target.closest('.btn_search') ||
                          target.closest('.btn_search_submit') ||
                          (target.tagName === 'BUTTON' && (target.type === 'submit' || target.className.includes('search')));

        if (searchBtn) {
            const header = searchBtn.closest('.header_area') || searchBtn.closest('#header') || searchBtn.closest('header');
            if (header) {
                const input = header.querySelector('input[type="text"]');
                if (input && input.value) {
                    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                    executeSearchKeyword(input.value);
                    return;
                }
            }
        }

        let targetUrl = null;

        if (link && link.href) {
            if (link.href.includes('/search')) targetUrl = link.href;
            else if (link.href.includes('/live/all')) targetUrl = 'https://www.sooplive.co.kr/live/all';
            else if (link.href.includes('/my/favorite')) targetUrl = 'https://www.sooplive.co.kr/my/favorite';
            else if (link.href.includes('/directory/category')) targetUrl = 'https://www.sooplive.co.kr/directory/category';
            else if (link.classList.contains('logo') || link.id === 'logo') targetUrl = 'https://www.sooplive.co.kr/';
        }

        if (!targetUrl) {
            const isLogo = target.closest('.logo') || target.closest('.header_logo') || target.closest('#logo');
            if (isLogo) targetUrl = 'https://www.sooplive.co.kr/';
            else if (text.includes('즐겨찾기') || text === 'MY') targetUrl = 'https://www.sooplive.co.kr/my/favorite';
            else if (text === '전체') targetUrl = 'https://www.sooplive.co.kr/live/all';
            else if (text === '카테고리') targetUrl = 'https://www.sooplive.co.kr/directory/category';
        }

        if (targetUrl) {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            executeSearchUrl(targetUrl);
        }
    }, true);


    // --- 5. PIP 제어 ---
    function startPip(url) {
        const player = document.getElementById('player_area');
        if (!player) return alert('플레이어를 찾을 수 없습니다.');

        isPipActive = true;
        document.body.classList.add('real-pip-mode');

        let frame = document.getElementById('soop-real-frame');
        if (!frame) {
            frame = document.createElement('iframe');
            frame.id = 'soop-real-frame';
            document.body.appendChild(frame);
        }
        if(url) frame.src = url;

        player.style.top = (window.innerHeight - 300) + 'px';
        player.style.left = (window.innerWidth - 500) + 'px';

        createControls(player);
        makeDraggable(player);
    }

    function stopPip() {
        isPipActive = false;
        document.body.classList.remove('real-pip-mode');
        const player = document.getElementById('player_area');
        if(player) {
            player.style.top = ''; player.style.left = '';
            player.style.width = ''; player.style.height = '';
        }
    }

    function exitPipMode() {
        const frame = document.getElementById('soop-real-frame');
        const targetUrl = frame ? frame.src : 'https://www.sooplive.co.kr';
        window.location.href = targetUrl;
    }

    // --- 6. 컨트롤 바 ---
    function createControls(player) {
        if (document.getElementById('pip-control-bar')) return;
        const bar = document.createElement('div');
        bar.id = 'pip-control-bar';
        bar.onmousedown = (e) => e.stopPropagation();

        const restoreBtn = document.createElement('button');
        restoreBtn.className = 'pip-ctrl-btn';
        restoreBtn.innerText = '⤢ 복귀';
        restoreBtn.onclick = stopPip;

        const exitBtn = document.createElement('button');
        exitBtn.className = 'pip-ctrl-btn';
        exitBtn.innerText = '✖ 종료';
        exitBtn.style.color = '#ff6b6b';
        exitBtn.style.borderColor = '#ff6b6b';
        exitBtn.onclick = exitPipMode;

        bar.appendChild(restoreBtn);
        bar.appendChild(exitBtn);
        player.appendChild(bar);
    }

    // --- 7. 드래그 ---
    function makeDraggable(elmnt) {
        let startX = 0, startY = 0, initialLeft = 0, initialTop = 0;
        elmnt.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            if (!isPipActive) return;
            if (e.target.closest('.play_control_box') ||
                e.target.closest('#pip-control-bar') ||
                e.target.tagName === 'INPUT' ||
                e.target.tagName === 'BUTTON') {
                return;
            }
            e.preventDefault();
            elmnt.classList.add('is-dragging');

            startX = e.clientX;
            startY = e.clientY;
            initialLeft = elmnt.offsetLeft;
            initialTop = elmnt.offsetTop;

            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            requestAnimationFrame(() => {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                elmnt.style.top = (initialTop + dy) + "px";
                elmnt.style.left = (initialLeft + dx) + "px";
            });
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
            elmnt.classList.remove('is-dragging');
        }
    }

    window.addEventListener('message', function(e) {
        if (!e.data) return;

        if (e.data.type === 'SOOP_THEME') {
            if (e.data.isDarkMode) document.body.classList.add('iframe-dark');
            else document.body.classList.remove('iframe-dark');
        }

        if (e.data.type === 'SOOP_BG_CLICK_SIMPLE') {
            if (document.activeElement && document.activeElement.tagName === 'INPUT') {
                document.activeElement.blur();
            }
            const ctrlBar = document.getElementById('pip-control-bar');
            if (ctrlBar) ctrlBar.click();
        }
    });

})();