// ==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... })();