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(); + } })();