// ==UserScript== // @name sbxh6Downloader // @namespace https://github.com/crossSiteKikyo/tokiDownloader // @version 0.0.1 // @description sbxh6.com (뉴토끼) 만화 다운로더 // @author hehaho // @match https://sbxh*.com/manhwa/* // @match https://sbxh*.com/manhwa/*/* // @icon https://github.com/user-attachments/assets/99f5bb36-4ef8-40cc-8ae5-e3bf1c7952ad // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_download // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.js // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const downloadZip = (blob, filename) => { const url = URL.createObjectURL(blob); if (typeof GM_download !== "undefined") { GM_download({ url: url, name: filename, onload: () => URL.revokeObjectURL(url), onerror: () => { const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); } }); } else { const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); } }; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function generateComicInfo(title, series, writer, number, pageCount, imageCount = 0) { let pagesXml = ''; if (imageCount > 0) { for (let i = 0; i < imageCount; i++) { pagesXml += ` \n`; } } const comicInfo = ` ${title} ${series} ${number} -1 -1 -1 -1 ${writer} ${pageCount} ko Unknown Yes ${pagesXml ? ` \n${pagesXml} \n` : ''} `; return comicInfo; } function getEpisodeImages() { const imgs = document.querySelectorAll('div.vw-imgs img, div.vw-imgs--single img, div.vw-imgs--double img'); const urls = []; imgs.forEach(img => { if (img.src && img.src.startsWith('http') && !urls.includes(img.src)) { urls.push(img.src); } }); return urls; } function getSeriesName() { const metaEl = document.querySelector('div.vw-bot-meta strong'); if (metaEl) return metaEl.textContent.trim(); const titleEl = document.querySelector('h1'); if (titleEl && !titleEl.closest('.hero-v2')) { return titleEl.textContent.trim(); } const ogTitle = document.querySelector('meta[property="og:title"]'); if (ogTitle) { const content = ogTitle.getAttribute('content') || ''; return content.split('|')[0].trim() || content; } return document.title.split('|')[0].trim() || '제목없음'; } function getEpisodeTitle() { const metaEl = document.querySelector('div.vw-bot-meta span'); if (metaEl) return metaEl.textContent.trim(); const titleEl = document.querySelector('h1'); if (titleEl && !titleEl.closest('.hero-v2')) { return titleEl.textContent.trim(); } return document.title.split('|')[0].trim() || '제목없음'; } function getListPageInfo() { const currentURL = window.location.href; const pathParts = window.location.pathname.split('/').filter(Boolean); const isListPage = pathParts.length === 2; const isEpisodePage = pathParts.length >= 3; return { isListPage, isEpisodePage }; } async function downloadCurrentEpisode() { try { console.clear(); console.log('에피소드 다운로드 시작...'); let imageUrls = getEpisodeImages(); if (imageUrls.length === 0) { console.log('이미지 로드 대기 중...'); for (let i = 0; i < 10; i++) { await sleep(500); imageUrls = getEpisodeImages(); if (imageUrls.length > 0) break; } } if (imageUrls.length === 0) { alert('이미지를 찾을 수 없습니다. 페이지가 완전히 로드되었는지 확인해주세요.'); return; } const seriesName = getSeriesName(); const episodeTitle = getEpisodeTitle(); console.log(`시리즈: ${seriesName}`); console.log(`에피소드: ${episodeTitle}`); console.log(`이미지: ${imageUrls.length}개 발견`); const zip = new JSZip(); const protocolDomain = window.location.origin; const downloadImage = (url, index, total) => { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', headers: { 'Referer': protocolDomain, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, onload: (response) => { if (response.status === 200) { const ext = url.split('.').pop().split('?')[0] || 'jpg'; const blob = new Blob([response.response], { type: 'image/' + ext }); const filename = `image${String(index).padStart(4, '0')}.${ext}`; zip.file(filename, blob); console.log(`${index + 1}/${total} 다운로드 완료`); } else { console.log(`이미지 ${index} 실패 (${response.status})`); } resolve(); }, onerror: () => { console.log(`이미지 ${index} 오류`); resolve(); }, ontimeout: () => { console.log(`이미지 ${index} 타임아웃`); resolve(); } }); }); }; let promiseList = []; for (let i = 0; i < imageUrls.length; i++) { promiseList.push(downloadImage(imageUrls[i], i, imageUrls.length)); if (promiseList.length >= 5) { await Promise.all(promiseList); promiseList = []; await sleep(1000); } } if (promiseList.length > 0) { await Promise.all(promiseList); } const extMatch = imageUrls[0].match(/\.(jpg|jpeg|png|webp)/i); const ext = extMatch ? extMatch[1].toLowerCase() : 'jpg'; const extCounts = {}; imageUrls.forEach(url => { const e = url.match(/\.(jpg|jpeg|png|webp)/i); if (e) extCounts[e[1].toLowerCase()] = (extCounts[e[1].toLowerCase()] || 0) + 1; }); const primaryExt = Object.keys(extCounts).sort((a, b) => extCounts[b] - extCounts[a])[0] || 'jpg'; const comicInfoContent = generateComicInfo(episodeTitle, seriesName, '', '1', imageUrls.length, imageUrls.length); zip.file('ComicInfo.xml', comicInfoContent); const content = await zip.generateAsync({ type: "blob" }); downloadZip(content, `${seriesName} - ${episodeTitle}.zip`); console.log(`완료: ${imageUrls.length}개 이미지`); } catch (error) { alert('다운로드 중 오류 발생:\n' + error); console.error(error); } } async function downloadAllEpisodes() { try { console.clear(); console.log('목록 페이지 처리 시작'); const episodeList = document.querySelector('ul.ep-list-v2'); if (!episodeList) { alert('에피소드 목록을 찾을 수 없습니다.'); return; } const items = episodeList.querySelectorAll('li.ep-row-v2'); console.log(`총 ${items.length}개 에피소드 발견`); const episodes = []; items.forEach((li, index) => { const link = li.querySelector('a.ep-row-v2-link'); if (link) { const href = link.getAttribute('href'); const absoluteUrl = href.startsWith('http') ? href : new URL(href, window.location.href).href; const epNoEl = li.querySelector('.ep-row-v2-no'); const epTitleEl = li.querySelector('.ep-row-v2-title strong'); const epNumber = epNoEl ? epNoEl.textContent.trim() : String(index + 1); const epTitle = epTitleEl ? epTitleEl.textContent.trim() : epNumber; episodes.push({ index, number: epNumber, title: epTitle, url: absoluteUrl }); } }); console.log(`추출된 링크: ${episodes.length}개`); if (episodes.length === 0) { alert('에피소드 링크를 추출할 수 없습니다.'); return; } const seriesName = getSeriesName(); console.log(`시리즈: ${seriesName}`); const iframe = document.createElement('iframe'); iframe.width = 600; iframe.height = 600; iframe.style.position = 'fixed'; iframe.style.left = '-9999px'; iframe.style.top = '0'; document.body.appendChild(iframe); const waitIframeLoad = (url) => { return new Promise((resolve) => { iframe.addEventListener('load', () => resolve(), { once: true }); iframe.src = url; }); }; const getImagesInIframe = () => { try { const iframeDoc = iframe.contentWindow.document; const imgs = iframeDoc.querySelectorAll('div.vw-imgs img, div.vw-imgs--single img'); const urls = []; imgs.forEach(img => { if (img.src && img.src.startsWith('http') && !urls.includes(img.src)) { urls.push(img.src); } }); return urls; } catch (e) { console.log('iframe 접근 오류:', e); return []; } }; for (let i = 0; i < episodes.length; i++) { const episode = episodes[i]; console.clear(); console.log(`${i + 1}/${episodes.length} ${episode.title} 처리 중...`); await waitIframeLoad(episode.url); console.log('페이지 로드 완료, 이미지 대기 중...'); await sleep(3000); try { const iframeDoc = iframe.contentWindow.document; const viewContent = iframeDoc.querySelector('.vw-imgs, .vw-imgs--single, [class*="vw-imgs"]'); if (viewContent) { viewContent.scrollTop = 100; } } catch (e) {} await sleep(2000); let imageUrls = getImagesInIframe(); if (imageUrls.length === 0) { console.log(`이미지를 찾을 수 없음 - 스킵`); continue; } console.log(`이미지 ${imageUrls.length}개 발견`); const zip = new JSZip(); const protocolDomain = window.location.origin; const downloadImage = (url, index, total) => { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', headers: { 'Referer': protocolDomain, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, onload: (response) => { if (response.status === 200) { const ext = url.split('.').pop().split('?')[0] || 'jpg'; const blob = new Blob([response.response], { type: 'image/' + ext }); const filename = `image${String(index).padStart(4, '0')}.${ext}`; zip.file(filename, blob); console.log(` ${index + 1}/${total} 이미지 다운로드`); } else { console.log(` 이미지 ${index} 실패 (${response.status})`); } resolve(); }, onerror: () => { console.log(` 이미지 ${index} 오류`); resolve(); }, ontimeout: () => { console.log(` 이미지 ${index} 타임아웃`); resolve(); } }); }); }; let promiseList = []; for (let j = 0; j < imageUrls.length; j++) { promiseList.push(downloadImage(imageUrls[j], j, imageUrls.length)); if (promiseList.length >= 5) { await Promise.all(promiseList); promiseList = []; await sleep(1000); } } if (promiseList.length > 0) { await Promise.all(promiseList); } const comicInfoContent = generateComicInfo(episode.title, seriesName, '', String(i + 1), imageUrls.length, imageUrls.length); zip.file('ComicInfo.xml', comicInfoContent); const content = await zip.generateAsync({ type: "blob" }); downloadZip(content, `${seriesName} - ${episode.title}.zip`); console.log(`${i + 1}/${episodes.length} ${episode.title} 완료`); await sleep(3000); } iframe.remove(); console.log('모든 에피소드 다운로드 완료'); } catch (error) { alert('목록 다운로드 중 오류:\n' + error); console.error(error); } } const { isListPage, isEpisodePage } = getListPageInfo(); if (isListPage) { GM_registerMenuCommand('[목록] 전체 에피소드 다운로드', () => downloadAllEpisodes()); console.log('sbxh6Downloader: 목록 페이지 감지됨. Tampermonkey 메뉴에서 실행하세요.'); } else if (isEpisodePage) { GM_registerMenuCommand('현재 에피소드 다운로드', () => downloadCurrentEpisode()); console.log('sbxh6Downloader: 에피소드 페이지 감지됨. Tampermonkey 메뉴에서 실행하세요.'); } else { console.log('sbxh6Downloader: 알 수 없는 페이지 타입'); } })();