ohli24 downloader

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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 }, '*');
            }
        }
    }
})();