Initial commit - tokiDownloader.user.js
This commit is contained in:
568
Old_Scripts/11toonDownloader.user copy.js
Normal file
568
Old_Scripts/11toonDownloader.user copy.js
Normal file
@@ -0,0 +1,568 @@
|
||||
// ==UserScript==
|
||||
// @name 11toonDownloader
|
||||
// @namespace https://github.com/crossSiteKikyo/tokiDownloader
|
||||
// @version 0.0.6
|
||||
// @description 일일툰 전용 다운로더
|
||||
// @author hehaho
|
||||
// @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
|
||||
// ==/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);
|
||||
}
|
||||
};
|
||||
|
||||
// 글로벌 이미지 저장소 (네트워크 모니터링용)
|
||||
window.capturedImageUrls = [];
|
||||
window.capturedImageBlobs = [];
|
||||
|
||||
// fetch 인터셉트 (이미지 Blob 직접 캡처)
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
||||
const isImageUrl = url && (url.includes('.jpg') || url.includes('.png') || url.includes('.webp') || url.includes('/image') || url.includes('/photo')) &&
|
||||
!url.includes('logo') && !url.includes('banner') && !url.includes('icon');
|
||||
|
||||
if (isImageUrl && !window.capturedImageUrls.includes(url)) {
|
||||
window.capturedImageUrls.push(url);
|
||||
}
|
||||
|
||||
const result = originalFetch.apply(this, args);
|
||||
|
||||
// Blob 응답 캡처 (Canvas Tainted 우회)
|
||||
if (isImageUrl && result instanceof Promise) {
|
||||
result.then(response => {
|
||||
if (response.ok && (response.headers.get('content-type')?.includes('image') || url.match(/\.(jpg|jpeg|png|webp)$/i))) {
|
||||
const clonedResponse = response.clone();
|
||||
clonedResponse.blob().then(blob => {
|
||||
if (blob.size > 100 && !window.capturedImageBlobs.some(b => b.size === blob.size)) {
|
||||
window.capturedImageBlobs.push(blob);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
return response;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// XMLHttpRequest 인터셉트 (arraybuffer 응답 캡처)
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
||||
this._captureUrl = url;
|
||||
return originalOpen.apply(this, [method, url, ...rest]);
|
||||
};
|
||||
XMLHttpRequest.prototype.send = function(...args) {
|
||||
const xhr = this;
|
||||
const originalOnreadystatechange = xhr.onreadystatechange;
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
const url = xhr._captureUrl;
|
||||
const isImageUrl = url && (url.includes('.jpg') || url.includes('.png') || url.includes('.webp') || url.includes('/image') || url.includes('/photo')) &&
|
||||
!url.includes('logo') && !url.includes('banner') && !url.includes('icon');
|
||||
|
||||
if (isImageUrl && xhr.response instanceof ArrayBuffer) {
|
||||
try {
|
||||
const blob = new Blob([xhr.response], { type: xhr.getResponseHeader('content-type') || 'image/jpeg' });
|
||||
if (!window.capturedImageBlobs.some(b => b.size === blob.size)) {
|
||||
window.capturedImageBlobs.push(blob);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (originalOnreadystatechange) {
|
||||
return originalOnreadystatechange.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
return originalSend.apply(this, args);
|
||||
};
|
||||
|
||||
let site = '';
|
||||
let protocolDomain = '';
|
||||
let isListPage = false;
|
||||
|
||||
// 현재 url 체크
|
||||
const currentURL = document.URL;
|
||||
if (currentURL.includes('bo_table=toons')) {
|
||||
if (currentURL.includes('wr_id=')) {
|
||||
site = "일일툰";
|
||||
} else {
|
||||
site = "일일툰_목록";
|
||||
isListPage = true;
|
||||
}
|
||||
protocolDomain = currentURL.match(/^https?:\/\/[^:/?]+/)[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const 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`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<?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>
|
||||
<PageCount>${pageCount}</PageCount>
|
||||
<LanguageISO>ko</LanguageISO>
|
||||
<Manga>Yes</Manga>
|
||||
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}</ComicInfo>`;
|
||||
};
|
||||
|
||||
// DOM에서 이미지 URL 추출 헬퍼 함수
|
||||
const extractImagesFromDom = (targetDocument, protocolDomain) => {
|
||||
let imageUrls = [];
|
||||
|
||||
// 1. img 태그 탐색
|
||||
const contentDiv = targetDocument.querySelector('#bo_v_con') || targetDocument.querySelector('.view-content') || targetDocument.querySelector('#toon_img') || targetDocument.querySelector('.board-view');
|
||||
let imgElements = Array.from(targetDocument.querySelectorAll('#bo_v_con img, .view-content img, .view-wrap img, .toon-img img, .item-image img, .board-view img, #toon_img img'));
|
||||
|
||||
if (contentDiv && imgElements.length === 0) {
|
||||
imgElements = Array.from(contentDiv.querySelectorAll('img'));
|
||||
}
|
||||
|
||||
imgElements.forEach(img => {
|
||||
let src = img.src || img.getAttribute('data-src') || img.getAttribute('data-original');
|
||||
for (let attr of img.attributes) {
|
||||
if (attr.name.startsWith('data-') && (attr.value.includes('http') || attr.value.includes('/data/'))) {
|
||||
src = attr.value;
|
||||
}
|
||||
}
|
||||
if (src && !src.includes('gif') && !src.includes('logo') && !src.includes('banner')) {
|
||||
if (src.startsWith('//')) src = 'https:' + src;
|
||||
else if (src.startsWith('/')) src = protocolDomain + src;
|
||||
imageUrls.push(src);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. css background-image 탐색
|
||||
const divElements = Array.from(targetDocument.querySelectorAll('#bo_v_con div, .view-content div, .toon-img div'));
|
||||
divElements.forEach(div => {
|
||||
const bg = div.style.backgroundImage || window.getComputedStyle(div).backgroundImage;
|
||||
if (bg && bg !== 'none') {
|
||||
const match = bg.match(/url\(["']?(.*?)["']?\)/);
|
||||
if (match && match[1] && !match[1].includes('gif')) {
|
||||
let src = match[1];
|
||||
if (src.startsWith('//')) src = 'https:' + src;
|
||||
else if (src.startsWith('/')) src = protocolDomain + src;
|
||||
imageUrls.push(src);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Canvas 탐색
|
||||
if (imageUrls.length === 0) {
|
||||
const canvasElements = Array.from(targetDocument.querySelectorAll('canvas'));
|
||||
canvasElements.forEach(canvas => {
|
||||
try {
|
||||
const dataUrl = canvas.toDataURL('image/jpeg');
|
||||
if (dataUrl && dataUrl.length > 1000) {
|
||||
imageUrls.push(dataUrl);
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. HTML 원문 정규식 탐색 (Fallback)
|
||||
if (imageUrls.length === 0) {
|
||||
const html = contentDiv ? contentDiv.innerHTML : targetDocument.documentElement.innerHTML;
|
||||
|
||||
const matches1 = html.match(/https?:\/\/[^"'\s>]*\.(jpg|jpeg|png|webp)[^"'\s>]*/gi) || [];
|
||||
const matches2 = html.match(/(?:\/data\/file\/[a-zA-Z0-9_/-]+|img[0-9]*\.[a-zA-Z0-9-]+\.[a-zA-Z]+\/data\/file\/[a-zA-Z0-9_/-]+)\.(jpg|jpeg|png|webp)/gi) || [];
|
||||
const matches3 = html.match(/\/toki[^"'\s>]+\.(jpg|jpeg|png|webp)/gi) || [];
|
||||
const matches4 = html.match(/(?:data-src|data-original)=["']([^"']+)["']/gi) || [];
|
||||
|
||||
let allMatches = [...matches1, ...matches2, ...matches3];
|
||||
matches4.forEach(m => {
|
||||
const urlMatch = m.match(/=["']([^"']+)["']/);
|
||||
if (urlMatch && urlMatch[1] && !urlMatch[1].includes('gif') && !urlMatch[1].includes('logo') && !urlMatch[1].includes('banner')) {
|
||||
allMatches.push(urlMatch[1]);
|
||||
}
|
||||
});
|
||||
|
||||
if (allMatches.length > 0) {
|
||||
imageUrls = imageUrls.concat(allMatches.map(url => {
|
||||
if (url.startsWith('//')) return 'https:' + url;
|
||||
if (url.startsWith('/')) return protocolDomain + url;
|
||||
if (!url.startsWith('http')) return protocolDomain + '/' + url;
|
||||
return url;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 제거 및 최종 필터링
|
||||
imageUrls = [...new Set(imageUrls)].filter(url => {
|
||||
if (url.startsWith('data:image')) return true;
|
||||
return !url.includes('logo') && !url.includes('banner') && !url.includes('icon') && !url.includes('no_img') && !url.includes('profile') && !url.includes('blank') && url.length > 25;
|
||||
});
|
||||
|
||||
return imageUrls;
|
||||
};
|
||||
|
||||
// 이미지 다운로드 및 압축 헬퍼 함수
|
||||
const processAndDownloadZip = async (title, imageUrls, imageBlobs) => {
|
||||
const zip = new JSZip();
|
||||
|
||||
// 1. Blob 사용 시 (가장 우선)
|
||||
if (imageBlobs && imageBlobs.length > 0) {
|
||||
console.log(`[다운로드] Blob 이미지 ${imageBlobs.length}개 사용`);
|
||||
imageBlobs.forEach((blob, i) => {
|
||||
const filename = `image${String(i).padStart(4, '0')}.jpg`;
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
const comicInfoContent = generateComicInfo(title, title, '', '1', imageBlobs.length, imageBlobs.length);
|
||||
zip.file('ComicInfo.xml', comicInfoContent);
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
downloadZip(content, `${title}.zip`);
|
||||
console.log(`[완료] ${title} 다운로드 완료`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. URL 사용 시
|
||||
if (!imageUrls || imageUrls.length === 0) {
|
||||
console.warn(`[스킵] ${title} - 이미지를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[다운로드] URL 이미지 ${imageUrls.length}개 사용`);
|
||||
const downloadImageTask = (url, index) => {
|
||||
return new Promise((resolve) => {
|
||||
const filename = `image${String(index).padStart(4, '0')}.jpg`;
|
||||
|
||||
if (url.startsWith('data:image')) {
|
||||
try {
|
||||
const base64Data = url.split(',')[1];
|
||||
zip.file(filename, base64Data, {base64: true});
|
||||
} catch(e) {}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
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' });
|
||||
zip.file(filename, blob);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
onerror: () => resolve(),
|
||||
ontimeout: () => resolve()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 5개씩 동시 다운로드
|
||||
let promiseList = [];
|
||||
for (let j = 0; j < imageUrls.length; j++) {
|
||||
promiseList.push(downloadImageTask(imageUrls[j], j));
|
||||
if (promiseList.length >= 5) {
|
||||
await Promise.all(promiseList);
|
||||
promiseList = [];
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
if (promiseList.length > 0) {
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
const comicInfoContent = generateComicInfo(title, title, '', '1', imageUrls.length, imageUrls.length);
|
||||
zip.file('ComicInfo.xml', comicInfoContent);
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
downloadZip(content, `${title}.zip`);
|
||||
console.log(`[완료] ${title} 다운로드 완료`);
|
||||
};
|
||||
|
||||
// 에피소드 하나 다운로드 처리 (목록에서 연속 다운로드 시 사용)
|
||||
async function downloadEpisode(episodeTitle, url) {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
console.log(`[진행 중] ${episodeTitle}`);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.border = '5px solid red';
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
if (iframeDoc) {
|
||||
const script = iframeDoc.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.innerHTML = `
|
||||
window.capturedImageUrls = [];
|
||||
window.capturedImageBlobs = [];
|
||||
const isTargetImage = (u) => u && typeof u === 'string' &&
|
||||
(u.includes('.jpg') || u.includes('.png') || u.includes('.webp') || u.includes('/image') || u.includes('/photo')) &&
|
||||
!u.includes('logo') && !u.includes('banner') && !u.includes('icon');
|
||||
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
const u = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
||||
if (isTargetImage(u)) {
|
||||
if (!window.capturedImageUrls.includes(u)) window.capturedImageUrls.push(u);
|
||||
const result = origFetch.apply(this, args);
|
||||
if (result instanceof Promise) {
|
||||
result.then(response => {
|
||||
if (response.ok) {
|
||||
response.clone().blob().then(blob => {
|
||||
if (blob.size > 100 && !window.capturedImageBlobs.some(b => b.size === blob.size)) {
|
||||
window.capturedImageBlobs.push(blob);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
return response;
|
||||
}).catch(() => {});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return origFetch.apply(this, args);
|
||||
};
|
||||
|
||||
const origOpen = XMLHttpRequest.prototype.open;
|
||||
const origSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, u, ...rest) {
|
||||
this._captureUrl = u;
|
||||
return origOpen.apply(this, [method, u, ...rest]);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(...args) {
|
||||
const xhr = this;
|
||||
const origOnreadystatechange = xhr.onreadystatechange;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
const u = xhr._captureUrl;
|
||||
if (isTargetImage(u) && xhr.response instanceof ArrayBuffer) {
|
||||
try {
|
||||
const blob = new Blob([xhr.response], { type: xhr.getResponseHeader('content-type') || 'image/jpeg' });
|
||||
if (blob.size > 100 && !window.capturedImageBlobs.some(b => b.size === blob.size)) {
|
||||
window.capturedImageBlobs.push(blob);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
if (origOnreadystatechange) return origOnreadystatechange.apply(this, arguments);
|
||||
};
|
||||
return origSend.apply(this, args);
|
||||
};
|
||||
`;
|
||||
iframeDoc.head.appendChild(script);
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
await new Promise((res) => {
|
||||
iframe.addEventListener('load', () => res(), { once: true });
|
||||
iframe.src = url;
|
||||
});
|
||||
|
||||
await sleep(3000);
|
||||
|
||||
const iframeWindow = iframe.contentWindow;
|
||||
const iframeDocument = iframeWindow.document;
|
||||
|
||||
let lastScroll = -1;
|
||||
let scrollAttempts = 0;
|
||||
while (scrollAttempts < 50) {
|
||||
iframeWindow.scrollBy(0, window.innerHeight || 800);
|
||||
await sleep(300);
|
||||
if (iframeWindow.scrollY === lastScroll || iframeWindow.scrollY + iframeWindow.innerHeight >= iframeDocument.documentElement.scrollHeight) {
|
||||
break;
|
||||
}
|
||||
lastScroll = iframeWindow.scrollY;
|
||||
scrollAttempts++;
|
||||
}
|
||||
|
||||
await sleep(1500);
|
||||
|
||||
let imageBlobs = [];
|
||||
if (iframeWindow && iframeWindow.capturedImageBlobs && iframeWindow.capturedImageBlobs.length > 0) {
|
||||
imageBlobs = [...iframeWindow.capturedImageBlobs];
|
||||
} else if (window.capturedImageBlobs && window.capturedImageBlobs.length > 0) {
|
||||
imageBlobs = [...window.capturedImageBlobs];
|
||||
}
|
||||
|
||||
let imageUrls = [];
|
||||
if (imageBlobs.length === 0) {
|
||||
imageUrls = extractImagesFromDom(iframeDocument, protocolDomain);
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
if (iframeWindow && iframeWindow.capturedImageUrls && iframeWindow.capturedImageUrls.length > 0) {
|
||||
imageUrls = [...iframeWindow.capturedImageUrls];
|
||||
} else if (window.capturedImageUrls && window.capturedImageUrls.length > 0) {
|
||||
imageUrls = [...window.capturedImageUrls];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iframe.remove();
|
||||
|
||||
await processAndDownloadZip(episodeTitle, imageUrls, imageBlobs);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
console.error(`[오류] ${episodeTitle}:`, err);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tokiDownload() {
|
||||
try {
|
||||
if (isListPage) {
|
||||
console.clear();
|
||||
console.log('목록 페이지 처리 시작');
|
||||
|
||||
const episodeList = document.querySelector('ul.episode-list') || document.querySelector('.list-body') || document.querySelector('.board-list') || document.querySelector('.list-container') || document.querySelector('#bo_list');
|
||||
if (!episodeList) {
|
||||
alert('에피소드 목록을 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = Array.from(episodeList.querySelectorAll('li, tr, .list-item, .item, .list-row'));
|
||||
const episodes = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const button = item.querySelector('button[onclick*="location.href"]');
|
||||
const link = item.querySelector('a[href*="wr_id="]');
|
||||
|
||||
if (button) {
|
||||
const onclick = button.getAttribute('onclick') || '';
|
||||
const match = onclick.match(/location\.href=['"]([^'"]+)['"]/);
|
||||
if (match && match[1]) {
|
||||
const absoluteUrl = new URL(match[1], window.location.href).href;
|
||||
const episodeTitle = item.innerText.split('\n')[0].trim();
|
||||
if (episodeTitle && absoluteUrl) {
|
||||
episodes.push({ title: episodeTitle, url: absoluteUrl });
|
||||
}
|
||||
}
|
||||
} else if (link && link.href) {
|
||||
let clone = link.cloneNode(true);
|
||||
clone.querySelectorAll('span, img, i, b, strong').forEach(s => s.remove());
|
||||
let episodeTitle = clone.innerText.trim();
|
||||
if (!episodeTitle) episodeTitle = link.innerText.trim();
|
||||
if (episodeTitle && link.href) {
|
||||
episodes.push({ title: episodeTitle, url: link.href });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueEpisodes = [];
|
||||
const urlSet = new Set();
|
||||
episodes.forEach(ep => {
|
||||
const wrIdMatch = ep.url.match(/wr_id=([0-9]+)/);
|
||||
const uniqueKey = wrIdMatch ? wrIdMatch[1] : ep.url;
|
||||
if (!urlSet.has(uniqueKey)) {
|
||||
urlSet.add(uniqueKey);
|
||||
uniqueEpisodes.push(ep);
|
||||
}
|
||||
});
|
||||
|
||||
uniqueEpisodes.reverse();
|
||||
|
||||
if (uniqueEpisodes.length === 0) {
|
||||
alert('에피소드 링크를 추출할 수 없습니다. (구조가 변경되었을 수 있습니다.)');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < uniqueEpisodes.length; i++) {
|
||||
await downloadEpisode(uniqueEpisodes[i].title, uniqueEpisodes[i].url);
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
alert('모든 에피소드 다운로드가 완료되었습니다.');
|
||||
|
||||
} else {
|
||||
const titleElement = document.querySelector('h1') || document.querySelector('.view-title');
|
||||
const title = titleElement ? titleElement.innerText.trim() : '제목없음';
|
||||
|
||||
console.clear();
|
||||
console.log(`단일 다운로드 시작: ${title}`);
|
||||
|
||||
let imageBlobs = window.capturedImageBlobs || [];
|
||||
let imageUrls = [];
|
||||
|
||||
if (imageBlobs.length === 0) {
|
||||
imageUrls = extractImagesFromDom(document, protocolDomain);
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
imageUrls = window.capturedImageUrls || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (imageBlobs.length === 0 && (!imageUrls || imageUrls.length === 0)) {
|
||||
alert('이미지를 감지하지 못했습니다. 페이지가 완전히 로드되지 않았을 수 있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
await processAndDownloadZip(title, imageUrls, imageBlobs);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`오류발생: \n` + error);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isListPage) {
|
||||
GM_registerMenuCommand('[일일툰] 전체 에피소드 다운로드', () => tokiDownload());
|
||||
} else {
|
||||
GM_registerMenuCommand('[일일툰] 현재 에피소드 다운로드', () => tokiDownload());
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user