Update viewer image loading and lazy-loading logic
This commit is contained in:
@@ -180,21 +180,83 @@
|
|||||||
return comicInfo;
|
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) {
|
function getEpisodeImages(container) {
|
||||||
const root = container || document;
|
const root = container || document;
|
||||||
const imgs = root.querySelectorAll(
|
// 만화/웹툰 뷰어 영역에서만 이미지 수집
|
||||||
'.vw-imgs img, .vw-imgs--double img, .vw-imgs--single img, ' +
|
const viewerSelectors = [
|
||||||
'.webtoon-page img, .manhwa-page img, ' +
|
'.vw-imgs',
|
||||||
'[class*="vw-imgs"] img'
|
'.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 urls = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
imgs.forEach(img => {
|
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)) {
|
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);
|
seen.add(src);
|
||||||
urls.push(src);
|
urls.push(src);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
@@ -252,41 +314,82 @@
|
|||||||
|
|
||||||
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
|
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
|
||||||
const triggerLazyLoading = async () => {
|
const triggerLazyLoading = async () => {
|
||||||
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
|
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"], .viewer') || document.body;
|
||||||
// 우선 현재 img들 강제 로딩
|
|
||||||
forceLoadImages(viewer);
|
forceLoadImages(viewer);
|
||||||
// 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
|
|
||||||
let prevHeight = 0;
|
let prevHeight = 0;
|
||||||
let stuckCount = 0;
|
let stuckCount = 0;
|
||||||
while (stuckCount < 3) {
|
let maxAttempts = 100; // 더 많은 시도
|
||||||
|
let attemptCount = 0;
|
||||||
|
|
||||||
|
while (stuckCount < 10 && attemptCount < maxAttempts) {
|
||||||
|
attemptCount++;
|
||||||
const maxScroll = Math.max(
|
const maxScroll = Math.max(
|
||||||
document.documentElement.scrollHeight,
|
document.documentElement.scrollHeight,
|
||||||
document.body.scrollHeight, 0
|
document.body.scrollHeight,
|
||||||
|
viewer.scrollHeight,
|
||||||
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log(`[${attemptCount}] 높이: ${maxScroll}px (이전: ${prevHeight}px)`);
|
||||||
|
|
||||||
if (maxScroll === prevHeight) {
|
if (maxScroll === prevHeight) {
|
||||||
stuckCount++;
|
stuckCount++;
|
||||||
|
log(`정체 카운트: ${stuckCount}/10`);
|
||||||
} else {
|
} else {
|
||||||
stuckCount = 0;
|
stuckCount = 0;
|
||||||
|
log(`새 콘텐츠 감지!`);
|
||||||
}
|
}
|
||||||
prevHeight = maxScroll;
|
prevHeight = maxScroll;
|
||||||
// 현재 위치에서 끝까지 step 단위로 스크롤 (느리게)
|
|
||||||
const step = Math.min(300, maxScroll);
|
// 작은 step으로 천천히 스크롤 (모든 이미지 트리거)
|
||||||
for (let s = window.scrollY; s <= maxScroll; s += step) {
|
const step = Math.max(50, Math.min(100, maxScroll / 20)); // 50-100px 범위로 조정
|
||||||
window.scrollTo(0, Math.min(s, maxScroll));
|
let currentPos = window.scrollY;
|
||||||
await sleep(300);
|
|
||||||
|
while (currentPos < maxScroll) {
|
||||||
|
window.scrollTo(0, currentPos);
|
||||||
|
await sleep(50); // 더 빠른 반응
|
||||||
|
currentPos += step;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 정확히 끝까지
|
||||||
window.scrollTo(0, maxScroll);
|
window.scrollTo(0, maxScroll);
|
||||||
// 스크롤 후 추가된 img들도 강제 로딩 (백그라운드 탭 IO 대체)
|
await sleep(150);
|
||||||
|
|
||||||
|
// 중간중간 강제 로딩
|
||||||
forceLoadImages(viewer);
|
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);
|
forceLoadImages(viewer);
|
||||||
await sleep(2000);
|
await sleep(250);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, finalHeight);
|
||||||
await sleep(1000);
|
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 생성 + 다운로드
|
// 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드
|
||||||
const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
|
const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
@@ -315,8 +418,13 @@
|
|||||||
if ((i + 1) % 5 === 0) await sleep(1000);
|
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.file('ComicInfo.xml', comicInfo);
|
||||||
|
|
||||||
|
// ZIP 파일명은 원본 그대로 유지
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
|
const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
|
||||||
downloadZip(blob, filename);
|
downloadZip(blob, filename);
|
||||||
@@ -325,19 +433,31 @@
|
|||||||
|
|
||||||
// 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
|
// 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
|
||||||
const getImagesWithLazyLoad = async () => {
|
const getImagesWithLazyLoad = async () => {
|
||||||
let urls = getEpisodeImages();
|
const viewer = await waitForViewerReady();
|
||||||
|
let urls = getEpisodeImages(viewer);
|
||||||
|
log(`초기 이미지 감지: ${urls.length}개`);
|
||||||
|
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
|
log('이미지 없음 - 0.3초 대기 후 재시도...');
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
await sleep(500);
|
await sleep(300);
|
||||||
urls = getEpisodeImages();
|
urls = getEpisodeImages(viewer);
|
||||||
if (urls.length > 0) break;
|
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}개 → 전체)...`);
|
log(`lazy loading 트리거 (${urls.length}개 → 전체)...`);
|
||||||
await triggerLazyLoading();
|
await triggerLazyLoading();
|
||||||
urls = getEpisodeImages();
|
urls = getEpisodeImages(viewer);
|
||||||
|
log(`스크롤 후 감지: ${urls.length}개`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user