diff --git a/tokiDownloader.user.js b/tokiDownloader.user.js
deleted file mode 100644
index a1acaed..0000000
--- a/tokiDownloader.user.js
+++ /dev/null
@@ -1,509 +0,0 @@
-// ==UserScript==
-// @name tokiDownloader
-// @namespace https://github.com/crossSiteKikyo/tokiDownloader
-// @version 0.0.5
-// @description 일일툰 다운로더
-// @author hehaho
-// @match https://*.com/webtoon/*
-// @match https://*.com/novel/*
-// @match https://*.net/comic/*
-// @match http://*.cprapid.com:*/bbs/board.php*
-// @match https://*.spotv24.com/bbs/board.php*
-// @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
-// @downloadURL https://update.sleazyfork.org/scripts/531932/tokiDownloader.user.js
-// @updateURL https://update.sleazyfork.org/scripts/531932/tokiDownloader.meta.js
-// ==/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);
- }
- };
-
- // 글로벌 이미지 URL 저장소 (네트워크 모니터링용)
- window.capturedImageUrls = [];
-
- // fetch 인터셉트 (모든 페이지에서 실행)
- const originalFetch = window.fetch;
- window.fetch = function(...args) {
- const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
- if (url && (url.includes('pl3040.com') || url.includes('www.pl3040.com')) &&
- (url.includes('.jpg') || url.includes('.png') || url.includes('.webp'))) {
- if (!window.capturedImageUrls.includes(url)) {
- window.capturedImageUrls.push(url);
- }
- }
- return originalFetch.apply(this, args);
- };
-
- // XMLHttpRequest 인터셉트
- const originalOpen = XMLHttpRequest.prototype.open;
- XMLHttpRequest.prototype.open = function(method, url, ...rest) {
- if (url && (url.includes('pl3040.com') || url.includes('www.pl3040.com')) &&
- (url.includes('.jpg') || url.includes('.png') || url.includes('.webp'))) {
- if (!window.capturedImageUrls.includes(url)) {
- window.capturedImageUrls.push(url);
- }
- }
- return originalOpen.apply(this, [method, url, ...rest]);
- };
-
- let site = '뉴토끼'; // 예시
- let protocolDomain = 'https://newtoki350.com'; // 예시
- let isListPage = false; // 목록 페이지 여부
-
- // 현재 url 체크
- const currentURL = document.URL;
- if (currentURL.includes('bo_table=toons')) {
- // cprapid.com 또는 spotv24.com
- if (currentURL.includes('wr_id=')) {
- site = "일일툰"; // 개별 에피소드
- } else {
- site = "일일툰_목록"; // 목록 페이지
- isListPage = true;
- }
- protocolDomain = currentURL.match(/^https?:\/\/[^:/?]+/)[0];
- }
- else {
- // 다운 페이지가 아니라면 리턴. @match가 정규표현식이 아니기 때문에 정확한 host를 매치 할 수 없기 때문.
- return;
- }
-
- function sleep(ms) {
- return new Promise(resolve => {
- setTimeout(() => resolve(), ms);
- })
- }
-
- function extractComicMetadata() {
- let seriesName = '';
- let writerName = '';
-
- const pageTitle = document.querySelector('h1');
- if (pageTitle) {
- const titleText = pageTitle.innerText.trim();
- seriesName = titleText.split('>')[0].trim().replace(/\s*\([^)]*\)$/, '');
- }
-
- // 작가 추출 (작가 라벨 다음의 링크)
- const viewContents = document.querySelectorAll('.view-content');
- for (let elem of viewContents) {
- if (elem.innerText.includes('작가')) {
- const writerLink = elem.querySelector('a');
- if (writerLink) {
- writerName = writerLink.innerText.trim();
- }
- break;
- }
- }
-
- return { seriesName, writerName };
- }
-
- 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;
- }
-
- async function tokiDownload(startIndex, lastIndex) {
- try {
- // 목록 페이지 처리
- if (isListPage) {
- console.clear();
- console.log('목록 페이지 처리 시작');
-
- // 에피소드 목록 추출
- const episodeList = document.querySelector('ul.episode-list');
- if (!episodeList) {
- alert('에피소드 목록을 찾을 수 없습니다.');
- return;
- }
-
- const items = Array.from(episodeList.querySelectorAll('li'));
- console.log(`총 ${items.length}개 에피소드 발견`);
-
- // 각 에피소드의 링크 추출
- const episodes = [];
- items.forEach((li, index) => {
- const button = li.querySelector('button.episode');
- if (button) {
- const onclick = button.getAttribute('onclick') || '';
- const match = onclick.match(/location\.href='([^']+)'/);
- if (match && match[1]) {
- const relativeUrl = match[1];
- const absoluteUrl = new URL(relativeUrl, window.location.href).href;
- const episodeTitle = li.innerText.split('\n')[0].trim();
-
- episodes.push({
- index,
- title: episodeTitle,
- url: absoluteUrl
- });
- }
- }
- });
-
- console.log(`추출된 링크: ${episodes.length}개`);
-
- if (episodes.length === 0) {
- alert('에피소드 링크를 추출할 수 없습니다.');
- return;
- }
-
- // iframe 생성
- const iframe = document.createElement('iframe');
- iframe.width = 600;
- iframe.height = 600;
- document.body.prepend(iframe);
-
- const waitIframeLoad = (url) => {
- return new Promise((resolve) => {
- iframe.addEventListener('load', () => resolve(), { once: true });
- iframe.src = url;
- });
- };
-
- // 각 에피소드 다운로드
- for (let i = 0; i < episodes.length; i++) {
- const episode = episodes[i];
- console.clear();
- console.log(`${i + 1}/${episodes.length} ${episode.title} 처리 중...`);
-
- // iframe에서 페이지 로드
- await waitIframeLoad(episode.url);
-
- // 이미지 로드 대기 (canvas 렌더링 시간)
- console.log('이미지 로드 대기 중...');
- await sleep(3000);
-
- // 스크린샷을 통해 이미지 로드 강제 (canvas 렌더링)
- try {
- const iframeDoc = iframe.contentWindow.document;
- const viewContent = iframeDoc.querySelector('.view-content, [class*="viewer"]');
- if (viewContent) {
- // 스크롤 트리거 (lazy loading 이미지 로드)
- viewContent.scrollTop = 100;
- }
- } catch (e) {
- // iframe CORS 문제 무시
- }
-
- // 추가 대기 (네트워크 요청 완료)
- await sleep(2000);
-
- // 이미지 URL 수집 (전역 변수에서)
- let imageUrls = [];
-
- if (window.capturedImageUrls && window.capturedImageUrls.length > 0) {
- imageUrls = window.capturedImageUrls.slice(); // 복사
- window.capturedImageUrls = []; // 초기화
- }
-
- if (imageUrls.length === 0) {
- console.log(`이미지를 찾을 수 없음 - 스킵`);
- continue;
- }
-
- console.log(`이미지 ${imageUrls.length}개 발견`);
-
- // ZIP 생성
- const zip = new JSZip();
-
- // 이미지 다운로드
- 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 blob = new Blob([response.response], { type: 'image/jpeg' });
- const filename = `image${String(index).padStart(4, '0')}.jpg`;
- 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);
- }
-
- // comicinfo.xml 생성
- const comicInfoContent = generateComicInfo(episode.title, episode.title, '', String(i + 1), imageUrls.length, imageUrls.length);
- zip.file('ComicInfo.xml', comicInfoContent);
-
- // ZIP 다운로드
- const content = await zip.generateAsync({ type: "blob" });
- downloadZip(content, episode.title + '.zip');
-
- console.log(`${i + 1}/${episodes.length} ${episode.title} 완료`);
-
- // 다운로드 간격
- await sleep(3000);
- }
-
- iframe.remove();
- console.log(`모든 에피소드 다운로드 완료`);
- return;
- }
-
- // 리스트들 가져오기
- let list = Array.from(document.querySelector('.list-body').querySelectorAll('li')).reverse();
-
- // 리스트 필터
- // startIndex가 있다면
- if (startIndex) {
- while (true) {
- let num = parseInt(list[0].querySelector('.wr-num').innerText);
- if (num < startIndex)
- list.shift();
- else
- break;
- }
- }
- // lastIndex가 있다면
- if (lastIndex) {
- while (true) {
- let num = parseInt(list.at(-1).querySelector('.wr-num').innerText);
- if (lastIndex < num)
- list.pop();
- else
- break;
- }
- }
-
- // iframe생성
- const iframe = document.createElement('iframe');
- iframe.width = 600;
- iframe.height = 600;
- document.querySelector('.content').prepend(iframe);
- const waitIframeLoad = (url) => {
- return new Promise((resolve) => {
- iframe.addEventListener('load', () => resolve(), { once: true });
- // iframe.addEventListener('DOMContentLoaded', () => resolve());
- iframe.src = url;
- });
- };
-
- if (site === "일일툰") {
- // URL에서 시리즈 ID와 에피소드 ID 추출
- const urlParams = new URLSearchParams(window.location.search);
- const seriesId = urlParams.get('is') || '';
- const episodeId = urlParams.get('wr_id') || '';
-
- if (!seriesId || !episodeId) {
- alert('시리즈 ID 또는 에피소드 ID를 찾을 수 없습니다.');
- return;
- }
-
- // 제목 추출
- const titleElement = document.querySelector('h1');
- const title = titleElement ? titleElement.innerText.trim() : '제목없음';
-
- // 전역 변수에서 이미 캡처된 이미지 URL 가져오기
- let imageUrls = window.capturedImageUrls || [];
-
- console.clear();
- console.log(`일일툰 다운로드 시작`);
- console.log(`현재까지 감지된 이미지: ${imageUrls.length}개`);
-
- // 이미지가 없으면 추가 대기
- if (imageUrls.length === 0) {
- console.log('이미지 로드 대기 중...');
- for (let i = 0; i < 10; i++) {
- await sleep(500);
- imageUrls = window.capturedImageUrls || [];
- console.log(`대기 ${i+1}: ${imageUrls.length}개`);
- if (imageUrls.length > 0) break;
- }
- }
-
- if (imageUrls.length === 0) {
- alert('이미지 URL을 감지하지 못했습니다. 페이지가 완전히 로드되지 않았을 수 있습니다.');
- return;
- }
-
- // ZIP 생성
- const zip = new JSZip();
-
- // 이미지 다운로드 함수
- 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 blob = new Blob([response.response], { type: 'image/jpeg' });
- const filename = `image${String(index).padStart(4, '0')}.jpg`;
- zip.file(filename, blob);
- console.log(`${index + 1}/${total} 다운로드 완료`);
- } else {
- console.log(`이미지 ${index} 다운로드 실패 (${response.status})`);
- }
- resolve();
- },
- onerror: (error) => {
- console.log(`이미지 ${index} 다운로드 오류:`, error);
- 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);
- }
-
- // comicinfo.xml 생성 및 추가
- const comicInfoContent = generateComicInfo(title, title, '', '1', imageUrls.length, imageUrls.length);
- zip.file('ComicInfo.xml', comicInfoContent);
-
- // ZIP 생성 및 다운로드
- const content = await zip.generateAsync({ type: "blob" });
- downloadZip(content, title + ".zip");
-
- console.log(`완료: ${imageUrls.length}개 이미지 다운로드`);
- }
-
- console.log(`모든 회차 다운완료`);
- } catch (error) {
- alert(`tokiDownload 오류발생: ${site}\n${currentURL}\n` + error);
- console.error(error);
- }
- }
-
- // ui 추가
- if (isListPage) {
- // 목록 페이지용 메뉴
- GM_registerMenuCommand('[목록] 전체 다운로드', () => tokiDownload());
- } else {
- // 개별 페이지용 메뉴 (기존)
- GM_registerMenuCommand('전체 다운로드', () => tokiDownload());
- GM_registerMenuCommand('N번째 회차부터', () => {
- const startPageInput = prompt('몇번째 회차부터 저장할까요?', 1);
- tokiDownload(startPageInput);
- });
- GM_registerMenuCommand('N번째 회차부터 N번째 까지', () => {
- const startPageInput = prompt('몇번째 회차부터 저장할까요?', 1);
- const endPageInput = prompt('몇번째 회차까지 저장할까요?', 2);
- tokiDownload(startPageInput, endPageInput);
- });
- }
- // Your code here...
-})();
diff --git a/tokimonkeyDownader.js b/tokimonkeyDownader.js
new file mode 100644
index 0000000..ced174b
--- /dev/null
+++ b/tokimonkeyDownader.js
@@ -0,0 +1,420 @@
+// ==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: 알 수 없는 페이지 타입');
+ }
+})();