From 24976567de6cc4593123e698301dc91e962cf2aa Mon Sep 17 00:00:00 2001 From: user01 Date: Sat, 13 Jun 2026 18:46:20 +0900 Subject: [PATCH] Remove tokiDownloader.user.js, replace with tokimonkeyDownader.js --- tokiDownloader.user.js | 509 ----------------------------------------- tokimonkeyDownader.js | 420 ++++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+), 509 deletions(-) delete mode 100644 tokiDownloader.user.js create mode 100644 tokimonkeyDownader.js 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: 알 수 없는 페이지 타입'); + } +})();