diff --git a/tokimonkeyDownader.js b/tokimonkeyDownader.js
index 215c861..2e8cd7b 100644
--- a/tokimonkeyDownader.js
+++ b/tokimonkeyDownader.js
@@ -13,394 +13,446 @@
// @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
+// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
- // 사이트가 console을 모니터링/차단하므로 자체 로거 사용
- const log = (...args) => {
- try { console.log(...args); } catch (_) {}
- };
+ // ---------------------------------------------------------------
+ // Phase 1: IntersectionObserver 후킹 (document-start 에서 즉시 실행)
+ // 백그라운드 탭에서 IO가 동작하지 않아도 observe() 즉시 콜백 실행
+ // ---------------------------------------------------------------
+ const OrigIO = window.IntersectionObserver;
+ if (OrigIO) {
+ const HookedIO = function (callback, options) {
+ const instance = new OrigIO(callback, options);
+ const origObserve = instance.observe.bind(instance);
+ instance.observe = function (target) {
+ origObserve(target);
+ setTimeout(() => {
+ try {
+ callback([{
+ target: target,
+ isIntersecting: true,
+ intersectionRatio: 1,
+ boundingClientRect: target.getBoundingClientRect(),
+ intersectionRect: target.getBoundingClientRect(),
+ rootBounds: null,
+ time: performance.now()
+ }], instance);
+ } catch (_) { }
+ }, 50);
+ };
+ return instance;
+ };
+ HookedIO.prototype = OrigIO.prototype;
+ window.IntersectionObserver = HookedIO;
+ }
- // 메뉴 명령 ID 관리 (중복 등록 방지 → 배지 숫자 증가 방지)
- let menuCmdIds = [];
+ // ---------------------------------------------------------------
+ // Phase 2: DOM 로딩 후 나머지 로직
+ // ---------------------------------------------------------------
+ const main = () => {
+ // 사이트가 console을 모니터링/차단하므로 자체 로거 사용
+ const log = (...args) => {
+ try { console.log(...args); } catch (_) {}
+ };
- const registerMenu = () => {
- menuCmdIds.forEach(id => {
- try { GM_unregisterMenuCommand(id); } catch (_) {}
- });
- menuCmdIds = [];
+ // 메뉴 명령 ID 관리 (중복 등록 방지 → 배지 숫자 증가 방지)
+ let menuCmdIds = [];
- const { isListPage, isEpisodePage } = getListPageInfo();
- if (isListPage) {
- menuCmdIds.push(GM_registerMenuCommand('[목록] 전체 에피소드 다운로드', () => downloadAllEpisodes()));
- } else if (isEpisodePage) {
- menuCmdIds.push(GM_registerMenuCommand('현재 에피소드 다운로드', () => downloadCurrentEpisode()));
+ const registerMenu = () => {
+ menuCmdIds.forEach(id => {
+ try { GM_unregisterMenuCommand(id); } catch (_) {}
+ });
+ menuCmdIds = [];
+
+ const { isListPage, isEpisodePage } = getListPageInfo();
+ if (isListPage) {
+ menuCmdIds.push(GM_registerMenuCommand('[목록] 전체 에피소드 다운로드', () => downloadAllEpisodes()));
+ } else if (isEpisodePage) {
+ menuCmdIds.push(GM_registerMenuCommand('현재 에피소드 다운로드', () => downloadCurrentEpisode()));
+ }
+ };
+
+ 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));
}
- };
- 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);
+ // 자동 연속 다운로드 큐 (sessionStorage 기반)
+ const QUEUE_KEY = 'sbxh6_q';
+ const IDX_KEY = 'sbxh6_i';
+ const SERIES_KEY = 'sbxh6_s';
+ const LIST_KEY = 'sbxh6_list';
+
+ const getQueue = () => {
+ try { return JSON.parse(sessionStorage.getItem(QUEUE_KEY) || 'null'); } catch (_) { return null; }
+ };
+ const getQueueIndex = () => {
+ try { return parseInt(sessionStorage.getItem(IDX_KEY) || '0', 10); } catch (_) { return 0; }
+ };
+ const cleanupQueue = () => {
+ sessionStorage.removeItem(QUEUE_KEY);
+ sessionStorage.removeItem(IDX_KEY);
+ sessionStorage.removeItem(SERIES_KEY);
+ sessionStorage.removeItem(LIST_KEY);
+ };
+
+ 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(container) {
+ const root = container || document;
+ const imgs = root.querySelectorAll(
+ '.vw-imgs img, .vw-imgs--double img, .vw-imgs--single img, ' +
+ '.webtoon-page img, .manhwa-page img, ' +
+ '[class*="vw-imgs"] img'
+ );
+ const urls = [];
+ const seen = new Set();
+ imgs.forEach(img => {
+ const src = img.src || img.getAttribute('src') || '';
+ if (src && src.startsWith('http') && !seen.has(src)) {
+ seen.add(src);
+ urls.push(src);
}
});
- } else {
- const link = document.createElement("a");
- link.href = url;
- link.download = filename;
- link.click();
- URL.revokeObjectURL(url);
+ return urls;
}
- };
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
+ function getSeriesName() {
+ const metaEl = document.querySelector('div.vw-bot-meta strong');
+ if (metaEl) return metaEl.textContent.trim();
- // 자동 연속 다운로드 큐 (sessionStorage 기반)
- const QUEUE_KEY = 'sbxh6_q';
- const IDX_KEY = 'sbxh6_i';
- const SERIES_KEY = 'sbxh6_s';
- const LIST_KEY = 'sbxh6_list';
-
- const getQueue = () => {
- try { return JSON.parse(sessionStorage.getItem(QUEUE_KEY) || 'null'); } catch (_) { return null; }
- };
- const getQueueIndex = () => {
- try { return parseInt(sessionStorage.getItem(IDX_KEY) || '0', 10); } catch (_) { return 0; }
- };
- const cleanupQueue = () => {
- sessionStorage.removeItem(QUEUE_KEY);
- sessionStorage.removeItem(IDX_KEY);
- sessionStorage.removeItem(SERIES_KEY);
- sessionStorage.removeItem(LIST_KEY);
- };
-
- function generateComicInfo(title, series, writer, number, pageCount, imageCount = 0) {
- let pagesXml = '';
- if (imageCount > 0) {
- for (let i = 0; i < imageCount; i++) {
- pagesXml += ` \n`;
+ const titleEl = document.querySelector('h1');
+ if (titleEl && !titleEl.closest('.hero-v2')) {
+ return titleEl.textContent.trim();
}
- }
- const comicInfo = `
-
- ${title}
- ${series}
- ${number}
- -1
- -1
-
-
- -1
- -1
- ${writer}
-
-
-
-
-
-
-
-
-
-
- ${pageCount}
- ko
-
- Unknown
- Yes
-${pagesXml ? ` \n${pagesXml} \n` : ''}
-`;
- return comicInfo;
- }
-
- function getEpisodeImages(container) {
- const root = container || document;
- const imgs = root.querySelectorAll(
- '.vw-imgs img, .vw-imgs--double img, .vw-imgs--single img, ' +
- '.webtoon-page img, .manhwa-page img, ' +
- '[class*="vw-imgs"] img'
- );
- const urls = [];
- const seen = new Set();
- imgs.forEach(img => {
- const src = img.src || img.getAttribute('src') || '';
- if (src && src.startsWith('http') && !seen.has(src)) {
- seen.add(src);
- urls.push(src);
+ const ogTitle = document.querySelector('meta[property="og:title"]');
+ if (ogTitle) {
+ const content = ogTitle.getAttribute('content') || '';
+ return content.split('|')[0].trim() || content;
}
- });
- 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();
+ return document.title.split('|')[0].trim() || '제목없음';
}
- const ogTitle = document.querySelector('meta[property="og:title"]');
- if (ogTitle) {
- const content = ogTitle.getAttribute('content') || '';
- return content.split('|')[0].trim() || content;
- }
+ function getEpisodeTitle() {
+ const metaEl = document.querySelector('div.vw-bot-meta span');
+ if (metaEl) return metaEl.textContent.trim();
- 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 };
- }
-
- // lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
- const triggerLazyLoading = async () => {
- const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
- const imgs = viewer.querySelectorAll('img');
- imgs.forEach(img => {
- img.setAttribute('loading', 'eager');
- ['data-src', 'data-original', 'data-lazy-src', 'data-lazy'].forEach(attr => {
- const val = img.getAttribute(attr);
- if (val && !img.src) img.src = val;
- });
- });
- // 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
- let prevHeight = 0;
- let stuckCount = 0;
- while (stuckCount < 3) {
- const maxScroll = Math.max(
- document.documentElement.scrollHeight,
- document.body.scrollHeight, 0
- );
- if (maxScroll === prevHeight) {
- stuckCount++;
- } else {
- stuckCount = 0;
+ const titleEl = document.querySelector('h1');
+ if (titleEl && !titleEl.closest('.hero-v2')) {
+ return titleEl.textContent.trim();
}
- prevHeight = maxScroll;
- // 현재 위치에서 끝까지 step 단위로 스크롤 (느리게)
- const step = Math.min(300, maxScroll);
- for (let s = window.scrollY; s <= maxScroll; s += step) {
- window.scrollTo(0, Math.min(s, maxScroll));
- await sleep(300);
- }
- window.scrollTo(0, maxScroll);
- await sleep(1500);
+
+ return document.title.split('|')[0].trim() || '제목없음';
}
- // 끝까지 다 내려간 상태에서 여유 대기
- await sleep(2000);
- window.scrollTo(0, 0);
- await sleep(1000);
- };
- // 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드
- const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
- const zip = new JSZip();
- const protocolDomain = window.location.origin;
+ 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;
- for (let i = 0; i < imageUrls.length; i++) {
- const url = imageUrls[i];
- await new Promise((resolve) => {
- GM_xmlhttpRequest({
- method: 'GET', url, responseType: 'arraybuffer',
- headers: {
- 'Referer': protocolDomain,
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
- },
- onload: (resp) => {
- if (resp.status === 200) {
- const ext = url.split('.').pop().split('?')[0] || 'jpg';
- zip.file(`image${String(i).padStart(4, '0')}.${ext}`, new Blob([resp.response], { type: 'image/' + ext }));
- }
- resolve();
- },
- onerror: () => resolve(),
- ontimeout: () => resolve()
+ return { isListPage, isEpisodePage };
+ }
+
+ // 이미지의 data-* 속성을 src로 강제 복사
+ const forceLoadImages = (root) => {
+ const container = root || document;
+ container.querySelectorAll('img').forEach(img => {
+ img.setAttribute('loading', 'eager');
+ ['data-src', 'data-original', 'data-lazy-src', 'data-lazy', 'data-srcset'].forEach(attr => {
+ const val = img.getAttribute(attr);
+ if (val && !img.src) img.src = val;
});
});
- if ((i + 1) % 5 === 0) await sleep(1000);
- }
+ };
- const comicInfo = generateComicInfo(episodeTitle, seriesName, '', episodeNumber, imageUrls.length, imageUrls.length);
- zip.file('ComicInfo.xml', comicInfo);
- const blob = await zip.generateAsync({ type: "blob" });
- const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
- downloadZip(blob, filename);
- log(`저장: ${filename} (${imageUrls.length}개)`);
- };
+ // lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
+ const triggerLazyLoading = async () => {
+ const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
+ // 우선 현재 img들 강제 로딩
+ forceLoadImages(viewer);
+ // 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
+ let prevHeight = 0;
+ let stuckCount = 0;
+ while (stuckCount < 3) {
+ const maxScroll = Math.max(
+ document.documentElement.scrollHeight,
+ document.body.scrollHeight, 0
+ );
+ if (maxScroll === prevHeight) {
+ stuckCount++;
+ } else {
+ stuckCount = 0;
+ }
+ prevHeight = maxScroll;
+ // 현재 위치에서 끝까지 step 단위로 스크롤 (느리게)
+ const step = Math.min(300, maxScroll);
+ for (let s = window.scrollY; s <= maxScroll; s += step) {
+ window.scrollTo(0, Math.min(s, maxScroll));
+ await sleep(300);
+ }
+ window.scrollTo(0, maxScroll);
+ // 스크롤 후 추가된 img들도 강제 로딩 (백그라운드 탭 IO 대체)
+ forceLoadImages(viewer);
+ await sleep(1500);
+ }
+ // 끝까지 다 내려간 상태에서 여유 대기 + 최종 강제 로딩
+ forceLoadImages(viewer);
+ await sleep(2000);
+ window.scrollTo(0, 0);
+ await sleep(1000);
+ };
- // 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
- const getImagesWithLazyLoad = async () => {
- let urls = getEpisodeImages();
- if (urls.length === 0) {
- for (let i = 0; i < 10; i++) {
- await sleep(500);
+ // 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드
+ const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
+ const zip = new JSZip();
+ const protocolDomain = window.location.origin;
+
+ for (let i = 0; i < imageUrls.length; i++) {
+ const url = imageUrls[i];
+ await new Promise((resolve) => {
+ GM_xmlhttpRequest({
+ method: 'GET', url, responseType: 'arraybuffer',
+ headers: {
+ 'Referer': protocolDomain,
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ },
+ onload: (resp) => {
+ if (resp.status === 200) {
+ const ext = url.split('.').pop().split('?')[0] || 'jpg';
+ zip.file(`image${String(i).padStart(4, '0')}.${ext}`, new Blob([resp.response], { type: 'image/' + ext }));
+ }
+ resolve();
+ },
+ onerror: () => resolve(),
+ ontimeout: () => resolve()
+ });
+ });
+ if ((i + 1) % 5 === 0) await sleep(1000);
+ }
+
+ const comicInfo = generateComicInfo(episodeTitle, seriesName, '', episodeNumber, imageUrls.length, imageUrls.length);
+ zip.file('ComicInfo.xml', comicInfo);
+ const blob = await zip.generateAsync({ type: "blob" });
+ const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
+ downloadZip(blob, filename);
+ log(`저장: ${filename} (${imageUrls.length}개)`);
+ };
+
+ // 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
+ const getImagesWithLazyLoad = async () => {
+ let urls = getEpisodeImages();
+ if (urls.length === 0) {
+ for (let i = 0; i < 10; i++) {
+ await sleep(500);
+ urls = getEpisodeImages();
+ if (urls.length > 0) break;
+ }
+ }
+ if (urls.length <= 3) {
+ log(`lazy loading 트리거 (${urls.length}개 → 전체)...`);
+ await triggerLazyLoading();
urls = getEpisodeImages();
- if (urls.length > 0) break;
+ }
+ return urls;
+ };
+
+ async function downloadCurrentEpisode() {
+ try {
+ log('에피소드 다운로드 시작...');
+ const imageUrls = await getImagesWithLazyLoad();
+
+ if (imageUrls.length === 0) {
+ alert('이미지를 찾을 수 없습니다.');
+ return;
+ }
+
+ const seriesName = getSeriesName();
+ const episodeTitle = getEpisodeTitle();
+ log(`시리즈: ${seriesName}, 에피소드: ${episodeTitle}, 이미지: ${imageUrls.length}개`);
+
+ await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, '1');
+ log('완료');
+ } catch (error) {
+ alert('다운로드 중 오류 발생:\n' + error);
+ log(error);
}
}
- if (urls.length <= 3) {
- log(`lazy loading 트리거 (${urls.length}개 → 전체)...`);
- await triggerLazyLoading();
- urls = getEpisodeImages();
- }
- return urls;
- };
- async function downloadCurrentEpisode() {
- try {
- log('에피소드 다운로드 시작...');
- const imageUrls = await getImagesWithLazyLoad();
+ async function downloadAllEpisodes() {
+ log('목록 페이지 처리 시작');
- if (imageUrls.length === 0) {
- alert('이미지를 찾을 수 없습니다.');
- return;
- }
+ const episodeList = document.querySelector('ul.ep-list-v2');
+ if (!episodeList) { alert('에피소드 목록을 찾을 수 없습니다.'); return; }
+
+ const episodes = [];
+ episodeList.querySelectorAll('li.ep-row-v2').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');
+ episodes.push({
+ index,
+ number: epNoEl ? epNoEl.textContent.trim() : String(index + 1),
+ title: epTitleEl ? epTitleEl.textContent.trim() : String(index + 1),
+ url: absoluteUrl
+ });
+ }
+ });
+
+ if (episodes.length === 0) { alert('에피소드 링크를 추출할 수 없습니다.'); return; }
const seriesName = getSeriesName();
- const episodeTitle = getEpisodeTitle();
- log(`시리즈: ${seriesName}, 에피소드: ${episodeTitle}, 이미지: ${imageUrls.length}개`);
+ log(`${episodes.length}개 에피소드를 큐에 저장, 첫 화로 이동합니다...`);
- await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, '1');
- log('완료');
- } catch (error) {
- alert('다운로드 중 오류 발생:\n' + error);
- log(error);
- }
- }
-
- async function downloadAllEpisodes() {
- log('목록 페이지 처리 시작');
-
- const episodeList = document.querySelector('ul.ep-list-v2');
- if (!episodeList) { alert('에피소드 목록을 찾을 수 없습니다.'); return; }
-
- const episodes = [];
- episodeList.querySelectorAll('li.ep-row-v2').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');
- episodes.push({
- index,
- number: epNoEl ? epNoEl.textContent.trim() : String(index + 1),
- title: epTitleEl ? epTitleEl.textContent.trim() : String(index + 1),
- url: absoluteUrl
- });
- }
- });
-
- if (episodes.length === 0) { alert('에피소드 링크를 추출할 수 없습니다.'); return; }
-
- const seriesName = getSeriesName();
- log(`${episodes.length}개 에피소드를 큐에 저장, 첫 화로 이동합니다...`);
-
- // 큐 저장 후 첫 번째 에피소드로 이동
- sessionStorage.setItem(QUEUE_KEY, JSON.stringify(episodes));
- sessionStorage.setItem(SERIES_KEY, seriesName);
- sessionStorage.setItem(IDX_KEY, '0');
- sessionStorage.setItem(LIST_KEY, window.location.href);
- window.location.href = episodes[0].url;
- }
-
- // 자동 큐 처리: sessionStorage에 큐가 있으면 현재 화면을 다운로드 후 다음으로 이동
- const processAutoQueue = async () => {
- const queue = getQueue();
- if (!queue) return;
- const { isEpisodePage } = getListPageInfo();
- if (!isEpisodePage) return; // 목록 페이지에서는 큐 처리 안 함
-
- const idx = getQueueIndex();
- if (idx >= queue.length) { cleanupQueue(); return; }
-
- log(`자동 다운로드: ${idx + 1}/${queue.length} - ${queue[idx].title}`);
- await sleep(7000);
-
- try {
- const imageUrls = await getImagesWithLazyLoad();
- if (imageUrls.length > 0) {
- const seriesName = sessionStorage.getItem(SERIES_KEY) || getSeriesName();
- const episodeTitle = getEpisodeTitle() || queue[idx].title;
- await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, queue[idx].number);
- } else {
- log(`이미지 없음 - 스킵`);
- }
- } catch (e) {
- log('자동 다운로드 오류:', e);
+ // 큐 저장 후 첫 번째 에피소드로 이동
+ sessionStorage.setItem(QUEUE_KEY, JSON.stringify(episodes));
+ sessionStorage.setItem(SERIES_KEY, seriesName);
+ sessionStorage.setItem(IDX_KEY, '0');
+ sessionStorage.setItem(LIST_KEY, window.location.href);
+ window.location.href = episodes[0].url;
}
- const next = idx + 1;
- sessionStorage.setItem(IDX_KEY, String(next));
+ // 자동 큐 처리: sessionStorage에 큐가 있으면 현재 화면을 다운로드 후 다음으로 이동
+ const processAutoQueue = async () => {
+ const queue = getQueue();
+ if (!queue) return;
+ const { isEpisodePage } = getListPageInfo();
+ if (!isEpisodePage) return; // 목록 페이지에서는 큐 처리 안 함
- if (next < queue.length) {
- log(`${next + 1}/${queue.length}번째로 이동...`);
- await sleep(2000);
- window.location.href = queue[next].url;
- } else {
- const listUrl = sessionStorage.getItem(LIST_KEY);
- cleanupQueue();
- log('모든 에피소드 다운로드 완료!');
- if (listUrl) {
- log('목록 페이지로 복귀합니다...');
+ const idx = getQueueIndex();
+ if (idx >= queue.length) { cleanupQueue(); return; }
+
+ log(`자동 다운로드: ${idx + 1}/${queue.length} - ${queue[idx].title}`);
+ await sleep(7000);
+
+ try {
+ const imageUrls = await getImagesWithLazyLoad();
+ if (imageUrls.length > 0) {
+ const seriesName = sessionStorage.getItem(SERIES_KEY) || getSeriesName();
+ const episodeTitle = getEpisodeTitle() || queue[idx].title;
+ await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, queue[idx].number);
+ } else {
+ log(`이미지 없음 - 스킵`);
+ }
+ } catch (e) {
+ log('자동 다운로드 오류:', e);
+ }
+
+ const next = idx + 1;
+ sessionStorage.setItem(IDX_KEY, String(next));
+
+ if (next < queue.length) {
+ log(`${next + 1}/${queue.length}번째로 이동...`);
await sleep(2000);
- window.location.href = listUrl;
+ window.location.href = queue[next].url;
+ } else {
+ const listUrl = sessionStorage.getItem(LIST_KEY);
+ cleanupQueue();
+ log('모든 에피소드 다운로드 완료!');
+ if (listUrl) {
+ log('목록 페이지로 복귀합니다...');
+ await sleep(2000);
+ window.location.href = listUrl;
+ }
}
- }
+ };
+
+ registerMenu();
+
+ // 자동 다운로드 큐 확인 (목록→에피소드 이동 시)
+ setTimeout(processAutoQueue, 1000);
+
+ // Next.js SPA 네비게이션 감지: URL 변경 시 메뉴 재등록
+ const origPushState = history.pushState;
+ const origReplaceState = history.replaceState;
+ history.pushState = function () {
+ origPushState.apply(this, arguments);
+ setTimeout(registerMenu, 500);
+ };
+ history.replaceState = function () {
+ origReplaceState.apply(this, arguments);
+ setTimeout(registerMenu, 500);
+ };
+ window.addEventListener('popstate', () => setTimeout(registerMenu, 500));
+
+ log('sbxh6Downloader loaded');
};
- registerMenu();
-
- // 자동 다운로드 큐 확인 (목록→에피소드 이동 시)
- setTimeout(processAutoQueue, 1000);
-
- // Next.js SPA 네비게이션 감지: URL 변경 시 메뉴 재등록
- const origPushState = history.pushState;
- const origReplaceState = history.replaceState;
- history.pushState = function () {
- origPushState.apply(this, arguments);
- setTimeout(registerMenu, 500);
- };
- history.replaceState = function () {
- origReplaceState.apply(this, arguments);
- setTimeout(registerMenu, 500);
- };
- window.addEventListener('popstate', () => setTimeout(registerMenu, 500));
-
- log('sbxh6Downloader loaded');
+ // DOM 로딩 완료 후 main() 실행
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', main);
+ } else {
+ main();
+ }
})();