// ==UserScript==
// @name sbxh6Downloader
// @namespace https://github.com/crossSiteKikyo/tokiDownloader
// @version 0.0.1
// @description sbxh6.com (뉴토끼) 만화 다운로더
// @author hehaho
// @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: 알 수 없는 페이지 타입');
}
})();