diff --git a/tokimonkeyDownader.js b/tokimonkeyDownader.js index 126854b..11839d5 100644 --- a/tokimonkeyDownader.js +++ b/tokimonkeyDownader.js @@ -180,20 +180,82 @@ return comicInfo; } + const waitForViewerReady = async (timeout = 12000) => { + const viewerSelectors = [ + '.vw-imgs', + '.vw-imgs--double', + '.vw-imgs--single', + '.webtoon-page', + '.manhwa-page', + '[class*="vw-imgs"]', + '.viewer', + '[role="main"]' + ]; + const start = Date.now(); + while (Date.now() - start < timeout) { + for (const selector of viewerSelectors) { + const el = document.querySelector(selector); + if (el) return el; + } + const imgs = Array.from(document.querySelectorAll('img')).filter(img => img.naturalWidth >= 120 && img.naturalHeight >= 120); + if (imgs.length > 0) return document; + await sleep(300); + } + return document; + }; + 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 viewerSelectors = [ + '.vw-imgs', + '.vw-imgs--double', + '.vw-imgs--single', + '.webtoon-page', + '.manhwa-page', + '[class*="vw-imgs"]', + '.viewer' + ]; + + let imgs = []; + for (const selector of viewerSelectors) { + try { + const containers = root.querySelectorAll(selector); + if (containers.length > 0) { + containers.forEach(containerEl => { + imgs.push(...Array.from(containerEl.querySelectorAll('img'))); + }); + if (imgs.length > 0) break; + } + } catch (_) {} + } + + // 찾지 못했으면 전체 img에서 필터링 + if (imgs.length === 0) { + imgs = Array.from(root.querySelectorAll('img')).filter(img => { + const rect = img.getBoundingClientRect(); + return rect.height > 50 && rect.width > 50; + }); + } + const urls = []; const seen = new Set(); imgs.forEach(img => { - const src = img.src || img.getAttribute('src') || ''; + const src = img.src || img.getAttribute('src') || img.getAttribute('data-src') || ''; if (src && src.startsWith('http') && !seen.has(src)) { - seen.add(src); - urls.push(src); + const lowerSrc = src.toLowerCase(); + if (!lowerSrc.includes('avatar') && + !lowerSrc.includes('logo') && + !lowerSrc.includes('icon') && + !lowerSrc.includes('banner') && + !lowerSrc.includes('nav') && + !lowerSrc.includes('header') && + !lowerSrc.includes('footer') && + !lowerSrc.includes('.svg') && + !lowerSrc.includes('placeholder')) { + seen.add(src); + urls.push(src); + } } }); return urls; @@ -252,41 +314,82 @@ // lazy loading 트리거: data-src 복사 + loading=eager + 스크롤 const triggerLazyLoading = async () => { - const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document; - // 우선 현재 img들 강제 로딩 + const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"], .viewer') || document.body; forceLoadImages(viewer); - // 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가 + let prevHeight = 0; let stuckCount = 0; - while (stuckCount < 3) { + let maxAttempts = 100; // 더 많은 시도 + let attemptCount = 0; + + while (stuckCount < 10 && attemptCount < maxAttempts) { + attemptCount++; const maxScroll = Math.max( document.documentElement.scrollHeight, - document.body.scrollHeight, 0 + document.body.scrollHeight, + viewer.scrollHeight, + 0 ); + + log(`[${attemptCount}] 높이: ${maxScroll}px (이전: ${prevHeight}px)`); + if (maxScroll === prevHeight) { stuckCount++; + log(`정체 카운트: ${stuckCount}/10`); } else { stuckCount = 0; + log(`새 콘텐츠 감지!`); } 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); + + // 작은 step으로 천천히 스크롤 (모든 이미지 트리거) + const step = Math.max(50, Math.min(100, maxScroll / 20)); // 50-100px 범위로 조정 + let currentPos = window.scrollY; + + while (currentPos < maxScroll) { + window.scrollTo(0, currentPos); + await sleep(50); // 더 빠른 반응 + currentPos += step; } + + // 정확히 끝까지 window.scrollTo(0, maxScroll); - // 스크롤 후 추가된 img들도 강제 로딩 (백그라운드 탭 IO 대체) + await sleep(150); + + // 중간중간 강제 로딩 forceLoadImages(viewer); - await sleep(1500); + await sleep(400); } - // 끝까지 다 내려간 상태에서 여유 대기 + 최종 강제 로딩 + + // 최종 확인: 절대 끝까지 + const finalHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight, + viewer.scrollHeight + ); + log(`최종 높이: ${finalHeight}px - 끝까지 스크롤`); + window.scrollTo(0, finalHeight); + await sleep(250); forceLoadImages(viewer); - await sleep(2000); - window.scrollTo(0, 0); - await sleep(1000); + await sleep(250); + window.scrollTo(0, finalHeight); + await sleep(375); + + log('스크롤 완료'); }; + // 헬퍼: 시리즈명에서 작가 분리 ("시리즈 - 작가" 형식 → {series, writer}) + function extractSeriesAndWriter(fullSeriesName) { + const parts = fullSeriesName.split(' - '); + if (parts.length >= 2) { + return { + series: parts[0].trim(), + writer: parts[1].trim() + }; + } + return { series: fullSeriesName, writer: '' }; + } + // 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드 const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => { const zip = new JSZip(); @@ -315,8 +418,13 @@ if ((i + 1) % 5 === 0) await sleep(1000); } - const comicInfo = generateComicInfo(episodeTitle, seriesName, '', episodeNumber, imageUrls.length, imageUrls.length); + // ComicInfo: 시리즈와 작가 분리, Title은 "시리즈명 + 에피소드제목"으로 조합 + const { series: cleanSeries, writer } = extractSeriesAndWriter(seriesName); + const fullTitle = `${cleanSeries} ${episodeTitle}`; + const comicInfo = generateComicInfo(fullTitle, cleanSeries, writer, episodeNumber, imageUrls.length, imageUrls.length); zip.file('ComicInfo.xml', comicInfo); + + // ZIP 파일명은 원본 그대로 유지 const blob = await zip.generateAsync({ type: "blob" }); const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_'); downloadZip(blob, filename); @@ -325,19 +433,31 @@ // 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거 const getImagesWithLazyLoad = async () => { - let urls = getEpisodeImages(); + const viewer = await waitForViewerReady(); + let urls = getEpisodeImages(viewer); + log(`초기 이미지 감지: ${urls.length}개`); + if (urls.length === 0) { + log('이미지 없음 - 0.3초 대기 후 재시도...'); for (let i = 0; i < 10; i++) { - await sleep(500); - urls = getEpisodeImages(); - if (urls.length > 0) break; + await sleep(300); + urls = getEpisodeImages(viewer); + if (urls.length > 0) { + log(`${i+1}번 시도 후 ${urls.length}개 감지`); + break; + } } } - if (urls.length <= 3) { + + // 실제 만화 이미지가 충분하지 않으면 lazy loading 트리거 + const hasLargeImages = urls.some(src => src.match(/\.(jpe?g|png|webp|gif)(\?|$)/i)); + if (urls.length <= 8 || !hasLargeImages) { log(`lazy loading 트리거 (${urls.length}개 → 전체)...`); await triggerLazyLoading(); - urls = getEpisodeImages(); + urls = getEpisodeImages(viewer); + log(`스크롤 후 감지: ${urls.length}개`); } + return urls; };