Remove tokiDownloader.user.js, replace with tokimonkeyDownader.js
This commit is contained in:
@@ -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 += ` <Page Image="${i}" Type="Story"/>\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
||||||
<Title>${title}</Title>
|
|
||||||
<Series>${series}</Series>
|
|
||||||
<Number>${number}</Number>
|
|
||||||
<Count>-1</Count>
|
|
||||||
<Volume>-1</Volume>
|
|
||||||
<Summary></Summary>
|
|
||||||
<Notes></Notes>
|
|
||||||
<Year>-1</Year>
|
|
||||||
<Month>-1</Month>
|
|
||||||
<Writer>${writer}</Writer>
|
|
||||||
<Penciller></Penciller>
|
|
||||||
<Inker></Inker>
|
|
||||||
<Colorist></Colorist>
|
|
||||||
<Letterer></Letterer>
|
|
||||||
<CoverArtist></CoverArtist>
|
|
||||||
<Editor></Editor>
|
|
||||||
<Publisher></Publisher>
|
|
||||||
<Imprint></Imprint>
|
|
||||||
<Genre></Genre>
|
|
||||||
<Web></Web>
|
|
||||||
<PageCount>${pageCount}</PageCount>
|
|
||||||
<LanguageISO>ko</LanguageISO>
|
|
||||||
<Format></Format>
|
|
||||||
<BlackAndWhite>Unknown</BlackAndWhite>
|
|
||||||
<Manga>Yes</Manga>
|
|
||||||
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
|
|
||||||
</ComicInfo>`;
|
|
||||||
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...
|
|
||||||
})();
|
|
||||||
420
tokimonkeyDownader.js
Normal file
420
tokimonkeyDownader.js
Normal file
@@ -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 += ` <Page Image="${i}" Type="Story"/>\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<Title>${title}</Title>
|
||||||
|
<Series>${series}</Series>
|
||||||
|
<Number>${number}</Number>
|
||||||
|
<Count>-1</Count>
|
||||||
|
<Volume>-1</Volume>
|
||||||
|
<Summary></Summary>
|
||||||
|
<Notes></Notes>
|
||||||
|
<Year>-1</Year>
|
||||||
|
<Month>-1</Month>
|
||||||
|
<Writer>${writer}</Writer>
|
||||||
|
<Penciller></Penciller>
|
||||||
|
<Inker></Inker>
|
||||||
|
<Colorist></Colorist>
|
||||||
|
<Letterer></Letterer>
|
||||||
|
<CoverArtist></CoverArtist>
|
||||||
|
<Editor></Editor>
|
||||||
|
<Publisher></Publisher>
|
||||||
|
<Imprint></Imprint>
|
||||||
|
<Genre></Genre>
|
||||||
|
<Web></Web>
|
||||||
|
<PageCount>${pageCount}</PageCount>
|
||||||
|
<LanguageISO>ko</LanguageISO>
|
||||||
|
<Format></Format>
|
||||||
|
<BlackAndWhite>Unknown</BlackAndWhite>
|
||||||
|
<Manga>Yes</Manga>
|
||||||
|
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
|
||||||
|
</ComicInfo>`;
|
||||||
|
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: 알 수 없는 페이지 타입');
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user