Update viewer image loading and lazy-loading logic

This commit is contained in:
2026-06-18 01:16:51 +09:00
parent a3845bf429
commit fd5209e1e9

View File

@@ -180,21 +180,83 @@
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)) {
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;
};