ohli24 downloader

영상을 다운로드하는 yt-dlp 명령어를 생성

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         ohli24 downloader
// @namespace    https://ohli24.net/
// @version      2026-02-27
// @description  영상을 다운로드하는 yt-dlp 명령어를 생성
// @author       OMIN7
// @match        *://*.ohli24.net/*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABMCAMAAADwSaEZAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB7FBMVEUAAABEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotJEotL///+I0xiUAAAAonRSTlMAAAMRBSZ34eN2JQhq0PzOaHrv7nliYMTBI/rz29b0+XTxGQkYh/bXGh3YzwYHHCIBb9zwsk9A6uYVsTWD/mYPhJogxUznexKj++RfPsqTJPU4oPJDEGTV3xcCPLqkK4BWVMflWLCXrURdbN7ibQsxOgRTy4iPVTu1pS7D2f1uFL/ti1sMKhZwOeyUIWXTMEbNLKngqy0TeKc9YXx+adFFJ3HSnOCkAAAAAWJLR0Sjx9rvGgAAAAd0SU1FB+QJBw8cNWped78AAAKISURBVFjD7dj5WxJBGAfwfZerWsEUKiWBAMsLiCKzMrrMY+0i0TIzOq0sKtEuM7P7sMvu+9q/tNmdATskdnnnt/j+BM8+z+fZ2Xnf2ZkVhFJKyRsA0YSMCMAss8Vqm4eKbf4Cs6aBVGZ3KMg4yhdKoFoVlVhKjdNFNFjExSLaYhAsS/hYilJVLVjd6o+lNR5Earyq4VsmaJbfFZCCRUeqXb5CVdyCdod19YAq1IZGjaFYUwiHhZt+w3A9FCphuUAostIULTw5ejAIr1odW9NceKp1YWtbyMV1rvUAHLAyn9YnG1oLDFUXtpEudHHPps2AxrZkV03/1m3/GqoxTHG0bQ8CL0xR2js6gRumeLtaRcBicls34zw78gxVP+bd2bwrTrXK3XvmnAf9WGIvVPckqSZX9c7VD4Yw6Nu3nw21v+MA4DABBg4OsslIHEr91Q/GMLIfCRxup1q88UgYUBj5Ix495qbc8RN/3JthjAzV0sOGOmRCYxA4KVPsVB8Sg4HTg2yYsWFAYaQ6zpxl1ZE+dx43AXChK0Yp39BIBlUaEB4dYx2VvNiALNrIJSdb1uyXg6h28l4ZucpKwj9+Ddno8sT1SWrZbkxhl6Bs5Ju9GfTiyHLrdj2vZXty7M5dXi+UxPi9KK9XXbf1Pv4l/EDbHsgTeZ+8Eexhmlx8NP2Yy8Yl8yTtezoscdlSkc3es+eRDJ/Nnu7wxWY4YuK0ukDZ+WDw4uWr/tdvuBx3BIi+TaXIjkvDPAEclg09Ilpriz4iSubZrn2nacmiD6915e9Hc5XI4Vjd8iGLwUcnFnN/ymFSBVaTP+ceGkhfvuI+kji/zdYChL7jPt/8MP9SWNgPSzqWlVL+8/wEmVmqzHr2JDEAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDktMDdUMTU6Mjg6NTMrMDI6MDA3vHR4AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA5LTA3VDE1OjI4OjUzKzAyOjAwRuHMxAAAAFd6VFh0UmF3IHByb2ZpbGUgdHlwZSBpcHRjAAB4nOPyDAhxVigoyk/LzEnlUgADIwsuYwsTIxNLkxQDEyBEgDTDZAMjs1Qgy9jUyMTMxBzEB8uASKBKLgDqFxF08kI1lQAAAABJRU5ErkJggg==
// @grant        GM_setClipboard
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. 단일 및 전체 화 제목 추출 / 버튼 생성
    // ==========================================
    if (window.location.hostname.includes('ohli24.net')) {
        let videoFileName = 'output.mp4'; // 제목 추출 실패 시 대비용 최소한의 기본값

        // 전체 화 일괄 추출을 위한 백그라운드 상태 관리 변수들 (필수)
        let isBatchExtracting = false;
        let batchIndex = 0;
        let batchCommands = [];
        let batchLinks = [];
        let hiddenIframe = null;
        let batchTimeout = null;

        window.addEventListener('DOMContentLoaded', () => {
            // [단일 화] 페이지 제목을 가져와 파일명으로 덮어쓰기
            try {
                const ogTitleMeta = document.querySelector('meta[property="og:title"]');
                if (ogTitleMeta && ogTitleMeta.content) {
                    videoFileName = ogTitleMeta.content.replace(/[\\/:*?"<>|]/g, '_').trim() + '.mp4';
                }
            } catch (e) {}

            // [전체 화] 에피소드 목록 확인 후 버튼 생성
            if (window === window.top) {
                const epSpans = document.querySelectorAll('span.eps-date');
                if (epSpans.length > 0) {
                    const links = Array.from(epSpans)
                                       .map(s => s.parentElement)
                                       .filter(a => a.tagName === 'A' && a.href);

                    const uniqueLinks = [];
                    const seen = new Set();
                    for (const link of links) {
                        if (!seen.has(link.href)) {
                            seen.add(link.href);
                            uniqueLinks.push(link);
                        }
                    }

                    if (uniqueLinks.length > 0) {
                        createBatchButtonOutside(uniqueLinks);
                    }
                }
            }
        });

        // iframe에서 전달한 securedLink 수신
        window.addEventListener('message', (event) => {
            if (event.data && event.data.action === 'm3u8_url_extracted') {
                if (isBatchExtracting && hiddenIframe && event.source === hiddenIframe.contentWindow) {
                    // 일괄 추출 진행 중일 때
                    clearTimeout(batchTimeout);
                    const url = event.data.url;
                    const fileName = hiddenIframe.dataset.expectedFileName || `output_${batchIndex}.mp4`;

                    const ytdlpCommand = `yt-dlp -N 32 -o "${fileName}" "${url}"`;
                    batchCommands.push(ytdlpCommand);

                    batchIndex++;
                    loadNextEpisode();
                }
                else if (!isBatchExtracting) {
                    // 단일 추출일 때
                    createButtonOutside(event.data.url, videoFileName);
                }
            }
        });

        function createButtonOutside(url, fileName) {
            if (document.getElementById('m3u8-copy-btn-outside')) return;

            const btn = document.createElement('button');
            btn.id = 'm3u8-copy-btn-outside';
            btn.innerText = '현재 화 yt-dlp 복사';

            Object.assign(btn.style, {
                position: 'fixed',
                bottom: '30px',
                right: '30px',
                zIndex: '2147483647',
                padding: '15px 20px',
                background: '#0984e3',
                color: 'white',
                border: 'none',
                borderRadius: '8px',
                cursor: 'pointer',
                fontWeight: 'bold',
                fontSize: '14px',
                boxShadow: '0 4px 10px rgba(0,0,0,0.3)'
            });

            btn.onclick = function() {
                const ytdlpCommand = `yt-dlp -N 32 -o "${fileName}" "${url}"`;
                try {
                    GM_setClipboard(ytdlpCommand);
                    alert('✅ 명령어 추출이 완료되었습니다! 클립보드에 복사되었으니 터미널(CMD)에 그대로 붙여넣기 하세요.\n\nPC 사양에 따라 숫자(32)를 조절하세요. [숫자가 높을수록 빠릅니다.]');
                } catch (err) {
                    alert('복사 실패 (권한 오류): ' + err);
                }
            };
            document.body.appendChild(btn);
        }

        function createBatchButtonOutside(links) {
            if (document.getElementById('m3u8-batch-btn')) return;

            const btn = document.createElement('button');
            btn.id = 'm3u8-batch-btn';
            btn.innerText = '모든 화 yt-dlp 일괄 복사';

            Object.assign(btn.style, {
                position: 'fixed',
                bottom: '85px',
                right: '30px',
                zIndex: '2147483647',
                padding: '15px 20px',
                background: '#e84393',
                color: 'white',
                border: 'none',
                borderRadius: '8px',
                cursor: 'pointer',
                fontWeight: 'bold',
                fontSize: '14px',
                boxShadow: '0 4px 10px rgba(0,0,0,0.3)'
            });

            btn.onclick = function() {
                if (isBatchExtracting) return;
                if (!confirm(`총 ${links.length}화의 영상 주소를 추출합니다.\n진행하시겠습니까?`)) return;

                btn.disabled = true;
                btn.style.opacity = '0.7';

                startBatchExtraction(links);
            };

            document.body.appendChild(btn);
        }

        function updateBatchButtonText(text) {
            const btn = document.getElementById('m3u8-batch-btn');
            if (btn) btn.innerText = text;
        }

        function startBatchExtraction(links) {
            isBatchExtracting = true;
            batchLinks = links;
            batchLinks.reverse(); // 1화부터 순서대로 다운로드되도록 배열 뒤집기
            batchIndex = 0;
            batchCommands = [];

            hiddenIframe = document.createElement('iframe');
            hiddenIframe.style.display = 'none';
            document.body.appendChild(hiddenIframe);

            loadNextEpisode();
        }

        async function loadNextEpisode() {
            if (batchIndex >= batchLinks.length) {
                finishBatchExtraction();
                return;
            }

            const epLink = batchLinks[batchIndex];
            updateBatchButtonText(`추출 중... (${batchIndex + 1}/${batchLinks.length})`);

            try {
                const res = await fetch(epLink.href);
                const html = await res.text();

                const parser = new DOMParser();
                const doc = parser.parseFromString(html, "text/html");
                const ogTitleMeta = doc.querySelector('meta[property="og:title"]');

                let fileName = `output_${batchIndex + 1}.mp4`;
                if (ogTitleMeta && ogTitleMeta.content) {
                    fileName = ogTitleMeta.content.replace(/[\\/:*?"<>|]/g, '_').trim() + '.mp4';
                } else {
                    let epTitleText = epLink.textContent.trim().replace(/[\\/:*?"<>|]/g, '_');
                    fileName = `${epTitleText}.mp4`;
                }

                const match = html.match(/<iframe[^>]+src=["'](https?:\/\/[^"']*cdndania[^"']+)["']/i);

                if (match && match[1]) {
                    const iframeUrl = match[1];
                    hiddenIframe.dataset.expectedFileName = fileName;

                    hiddenIframe.src = iframeUrl;

                    clearTimeout(batchTimeout);
                    batchTimeout = setTimeout(() => {
                        batchIndex++;
                        loadNextEpisode();
                    }, 10000);
                } else {
                    batchIndex++;
                    loadNextEpisode();
                }
            } catch (e) {
                batchIndex++;
                loadNextEpisode();
            }
        }

        function finishBatchExtraction() {
            isBatchExtracting = false;
            if (hiddenIframe) {
                hiddenIframe.remove();
                hiddenIframe = null;
            }

            const finalScript = batchCommands.join(' && ');
            try {
                GM_setClipboard(finalScript);
                alert(`✅ 총 ${batchCommands.length}화의 명령어 추출이 완료되었습니다!\n클립보드에 복사되었으니 터미널(CMD)에 그대로 붙여넣기 하세요.\n\nPC 사양에 따라 숫자(32)를 조절하세요. [숫자가 높을수록 빠릅니다.]`);
            } catch (err) {
                alert('복사 실패: ' + err);
            }

            updateBatchButtonText('✅ 모든 화 명령어 복사 완료!');
            setTimeout(() => {
                const btn = document.getElementById('m3u8-batch-btn');
                if(btn) {
                    btn.innerText = '모든 화 yt-dlp 일괄 복사';
                    btn.style.opacity = '1';
                    btn.disabled = false;
                }
            }, 3000);
        }

        return;
    }

    // ==========================================
    // 2. 통신 가로채기 및 securedLink 추출
    // ==========================================
    if (window.location.hostname.includes('cdndania.com')) {
        const originalXHR = window.XMLHttpRequest.prototype.open;
        window.XMLHttpRequest.prototype.open = function(method, url) {
            if (url && typeof url === 'string' && (url.includes('/player/index.php') || url.includes('getVideo'))) {
                this.addEventListener('load', function() {
                    const rawData = this.response || this.responseText;
                    extractAndSendMessage(rawData);
                });
            }
            originalXHR.apply(this, arguments);
        };

        function extractAndSendMessage(data) {
            if (!data) return;
            let securedLink = null;
            if (typeof data === 'object' && data !== null) {
                securedLink = data.securedLink;
            } else if (typeof data === 'string') {
                try {
                    const jsonObj = JSON.parse(data);
                    securedLink = jsonObj.securedLink;
                } catch (e) {
                    const linkMatch = data.match(/"?securedLink"?\s*:\s*["']([^"']+)["']/i);
                    if (linkMatch && linkMatch[1]) {
                        securedLink = linkMatch[1].replace(/\\\//g, '/');
                    }
                }
            }
            if (securedLink) {
                window.parent.postMessage({ action: 'm3u8_url_extracted', url: securedLink }, '*');
            }
        }
    }
})();