전에 만든건데 방금해보니 잘되네요.
https://kio.ac/c/dlR4CKA6vYLTT1ZeKuV5Ob
스크립트 다운로드 링크 추가.
// ==UserScript==
// @name 69shuba Novel Downloader (Background Fetch)
// @namespace http://tampermonkey.net/
// @version 4.8 // 버전 업데이트
// @description Downloads novel text from 69shuba.com by fetching chapters in the background. Fixes character encoding (GBK) issue, extracts full content, handles 429 rate-limiting, and 403 forbidden errors.
// @author You
// @match https://www.69shuba.com/book/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function() {
'use strict';
// Selectors and constants
const episodeListSelector = 'ul li a[href*="/txt/"]';
const textToRemove = '最⊥新⊥小⊥说⊥在⊥六⊥9⊥⊥书⊥⊥吧⊥⊥首⊥发!';
let isCheckAll = false;
let countLabel;
// 429 에러 대응 및 딜레이 설정
const BASE_DELAY_MS = 1500; // 요청 사이의 기본 지연 시간 (1.5초)
const INITIAL_PAGE_DELAY_MS = 500; // 페이지 진입 시 초기 지연 시간 (추가 요구사항)
const MAX_RETRIES = 5; // 최대 재시도 횟수
const MIN_FILE_SIZE_KB = 1; // 본문 추출 성공 최소 크기 (1KB)
function log(message) { console.log(`[Tampermonkey: 69shuba Downloader] ${message}`); }
// ... (addGlobalStyle, updateCheckedCount, addCheckboxes, createListPageUI 함수는 변경 없음) ...
function addGlobalStyle() {
if ($('#tampermonkey-shuba-style').length > 0) return;
const style = document.createElement('style');
style.id = 'tampermonkey-shuba-style';
style.innerHTML = `
.tampermonkey-ui-element { display: inline-block; margin-left: 5px; vertical-align: middle; }
#tampermonkey-count-label { position: fixed !important; top: 10px !important; left: 50% !important; transform: translateX(-50%) !important; z-index: 9999 !important; background-color: #007bff !important; color: white !important; padding: 8px 12px !important; border-radius: 4px !important; font-family: sans-serif !important; font-size: 14px !important; box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; pointer-events: none !important; }
#tampermonkey-controls-container { margin-bottom: 10px; padding: 10px; background-color: #f0f0f0; border-radius: 5px; display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
#tampermonkey-download-button-bottom { position: fixed !important; bottom: 20px !important; left: 50% !important; transform: translateX(-50%) !important; z-index: 9998 !important; background-color: #28a745 !important; border: none !important; color: white !important; padding: 15px 32px !important; text-align: center !important; font-size: 16px !important; cursor: pointer !important; border-radius: 8px !important; }
#tampermonkey-download-button-bottom:disabled, #tampermonkey-download-button-top:disabled { background-color: #cccccc !important; cursor: not-allowed !important; }
`;
document.head.appendChild(style);
}
function updateCheckedCount() {
if (!countLabel) return;
const checkedCount = $(episodeListSelector).parent().find('input[type="checkbox"]:checked').length;
countLabel.text(`선택된 항목 = ${checkedCount}개`);
}
function addCheckboxes() {
let episodeIndex = 1;
$(episodeListSelector).each(function() {
const link = $(this);
const listItem = link.parent();
if (link.attr('id') === 'bookcase' || listItem.find('input[type="checkbox"]').length > 0) return;
const checkbox = $('').css('margin-right', '5px');
checkbox.on('change', updateCheckedCount);
link.data({
index: episodeIndex++,
title: link.text().trim(),
url: this.href
});
listItem.prepend(checkbox);
});
updateCheckedCount();
}
function createListPageUI() {
if ($('#tampermonkey-controls-container').length > 0) return;
addGlobalStyle();
countLabel = $('').attr('id', 'tampermonkey-count-label');
$('body').append(countLabel);
const checkAllButton = $('모두 선택').addClass('tampermonkey-ui-element');
const uncheckAllButton = $('모두 해제').addClass('tampermonkey-ui-element');
const downloadButton = $('선택 다운로드').attr('id', 'tampermonkey-download-button-top').addClass('tampermonkey-ui-element');
const rangeInput = $('').addClass('tampermonkey-ui-element');
const checkTBButton = $('범위 선택').addClass('tampermonkey-ui-element');
const uncheckTBButton = $('범위 해제').addClass('tampermonkey-ui-element');
const betweenChkButton = $('선택 사이').addClass('tampermonkey-ui-element');
checkAllButton.on('click', function() {
isCheckAll = !isCheckAll;
$(episodeListSelector).parent().find('input[type="checkbox"]').prop('checked', isCheckAll);
$(this).text(isCheckAll ? '모두 해제' : '모두 선택');
updateCheckedCount();
});
uncheckAllButton.on('click', function() {
$(episodeListSelector).parent().find('input[type="checkbox"]').prop('checked', false);
isCheckAll = false;
checkAllButton.text('모두 선택');
updateCheckedCount();
});
downloadButton.on('click', startBackgroundDownload);
function handleRangeAction(shouldCheck, start, end) {
if (start === undefined || end === undefined) {
const rangeString = rangeInput.val();
const range = rangeString.split('-').map(s => parseInt(s.trim(), 10));
if (range.length < 1 || isNaN(range[0])) return;
start = range.length > 1 && !isNaN(range[1]) ? Math.min(...range) : range[0];
end = range.length > 1 && !isNaN(range[1]) ? Math.max(...range) : range[0];
}
$(episodeListSelector).filter((i, el) => {
const index = $(el).data('index');
return index >= start && index <= end;
}).parent().find('input[type="checkbox"]').prop('checked', shouldCheck);
updateCheckedCount();
}
checkTBButton.on('click', () => handleRangeAction(true));
uncheckTBButton.on('click', () => handleRangeAction(false));
betweenChkButton.on('click', function() {
const checkedIndexes = $(episodeListSelector).parent().find('input:checked').siblings('a').map((i, el) => $(el).data('index')).get();
if (checkedIndexes.length < 2) {
alert("범위를 지정하려면 최소 두 개의 체크박스를 선택해주세요.");
return;
}
handleRangeAction(true, Math.min(...checkedIndexes), Math.max(...checkedIndexes));
});
const controlsContainer = $('
').attr('id', 'tampermonkey-controls-container');
controlsContainer.append(checkAllButton, uncheckAllButton, downloadButton, rangeInput, checkTBButton, uncheckTBButton, betweenChkButton);
controlsContainer.insertBefore($(episodeListSelector).first().closest('ul'));
const downloadButtonBottom = $('선택 다운로드').attr('id', 'tampermonkey-download-button-bottom');
downloadButtonBottom.on('click', startBackgroundDownload);
$('body').append(downloadButtonBottom);
}
/**
* 원시 HTML 응답에서 소설 본문 텍스트를 파싱하여 추출합니다.
*/
function parseRawHtml(rawHtml) {
const $tempDiv = $('
').html(rawHtml);
const $txtNav = $tempDiv.find('.txtnav');
if ($txtNav.length === 0) {
log("Error: '.txtnav' area not found in fetched HTML.");
return "[오류: 본문 영역을 찾을 수 없음]";
}
const $contentContainer = $txtNav.clone();
// 불필요한 요소들을 제거
$contentContainer.find('h1.hide720, .txtinfo, #txtright, .bottom-ad, script').remove();
let chapterContent = $contentContainer.html();
//
태그를 줄 바꿈 문자로 변환
chapterContent = chapterContent.replace(//ig, '\n');
// 특수 공백 문자 대체
chapterContent = chapterContent.replace(/ /g, ' ');
chapterContent = chapterContent.replace(/[\u2003\u3000]/g, ' ');
// 남아있는 HTML 태그를 모두 제거
chapterContent = chapterContent.replace(/<[^>]*>/g, '');
// 공백 정규화
chapterContent = chapterContent.split('\n').map(line => line.trim()).join('\n');
chapterContent = chapterContent.replace(/(\n\s*){3,}/g, '\n\n');
chapterContent = chapterContent.trim();
return chapterContent;
}
function startBackgroundDownload() {
// 기존 chapterToDownload 대신, 재시도 횟수를 저장할 구조를 사용합니다.
const chaptersQueue = $(episodeListSelector).parent().find('input:checked').siblings('a').map((i, el) => {
return {
...$(el).data(),
retryCount: 0 // 재시도 횟수 추가
};
}).get();
if (chaptersQueue.length === 0) return alert("다운로드할 에피소드를 선택해주세요.");
let processedCount = 0;
const totalCount = chaptersQueue.length;
const downloadButtons = $('#tampermonkey-download-button-top, #tampermonkey-download-button-bottom');
downloadButtons.prop('disabled', true);
function processNextChapter() {
if (chaptersQueue.length === 0) {
log("All chapters processed.");
downloadButtons.prop('disabled', false).text('다운로드 완료!');
setTimeout(() => downloadButtons.text('선택 다운로드'), 5000);
return;
}
const chapter = chaptersQueue.shift();
// 현재 진행 상황 업데이트
const currentProgress = totalCount - chaptersQueue.length;
downloadButtons.text(`다운로드 중... (${currentProgress}/${totalCount}${chapter.retryCount > 0 ? ` (재시도 ${chapter.retryCount}/${MAX_RETRIES})` : ''})`);
// Referer 헤더 설정을 위해 base URL 추출
const baseUrl = chapter.url.split('/').slice(0, 3).join('/');
GM_xmlhttpRequest({
method: "GET",
url: chapter.url,
overrideMimeType: "text/html; charset=gbk",
headers: {
// 403 에러 회피를 위해 Referer와 User-Agent 설정
"Referer": baseUrl + '/',
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
onload: function(response) {
let delay = BASE_DELAY_MS; // 기본 지연 시간
let needsRetry = false;
let chapterContent = '';
let errorContent = '';
const filename = `${chapter.index}.${chapter.title.replace(/[<>:"/\\|?*]/g, '_')}.txt`;
if (response.status === 200) {
// 1. 200 성공 처리
chapterContent = parseRawHtml(response.responseText);
const cleanedContent = chapterContent.replace(new RegExp(textToRemove.replace(/⊥/g, '⊥?'), 'g'), "").replace(/[\uFEFF]/g, '').trim();
// 2. 파일 크기 검사 (1KB 미만이면 재시도)
// 주의: encodeURIComponent로 인코딩된 바이트 크기는 실제 파일 크기와 다를 수 있으므로, 대략적인 길이로 판단합니다.
// 1KB = 1024 bytes. UTF-8 한글 1글자는 3바이트. 1024 / 3 = 약 340글자.
if (cleanedContent.length < (MIN_FILE_SIZE_KB * 500)) {
errorContent = `[본문 내용이 짧음, 재시도]`;
needsRetry = true;
} else {
// 최종 다운로드 트리거
processedCount++;
log(`[${processedCount}/${totalCount}] Fetched ${chapter.title} successfully.`);
GM_download({
url: 'data:text/plain;charset=utf-8,' + encodeURIComponent(cleanedContent),
name: filename,
onload: () => log(`Successfully triggered download for: ${filename}`),
onerror: (err) => log(`Error downloading ${filename}: ${JSON.stringify(err)}`)
});
}
} else if (response.status === 429 || response.status === 403) {
// 3. 429 또는 403 에러 처리
errorContent = `[다운로드 실패: HTTP 상태 코드 ${response.status}]`;
needsRetry = true;
} else {
// 4. 기타 HTTP 에러 처리 (404, 500 등)
processedCount++;
log(`Failed to fetch ${chapter.title}, Status: ${response.status}`);
errorContent = `[다운로드 실패: HTTP 상태 코드 ${response.status}]`;
GM_download({
url: 'data:text/plain;charset=utf-8,' + encodeURIComponent(errorContent),
name: filename
});
}
// 재시도 로직 실행
if (needsRetry) {
if (chapter.retryCount < MAX_RETRIES) {
chapter.retryCount++;
// 지수 백오프 적용
delay = BASE_DELAY_MS * Math.pow(2, chapter.retryCount);
// 서버의 Retry-After 헤더 확인 (429 에러 시)
if (response.status === 429) {
const retryAfterHeaderMatch = response.responseHeaders ? response.responseHeaders.match(/Retry-After: (\d+)/i) : null;
if (retryAfterHeaderMatch) {
const serverDelay = parseInt(retryAfterHeaderMatch[1], 10) * 1000;
delay = Math.max(delay, serverDelay);
}
}
log(`[RETRY] ${errorContent} for ${chapter.title}. Retrying in ${delay / 1000}s. Attempt ${chapter.retryCount}/${MAX_RETRIES}`);
chaptersQueue.unshift(chapter); // 큐의 맨 앞에 다시 넣어 재시도
} else {
// 최대 재시도 횟수 초과
processedCount++;
log(`[FATAL] Max retries reached for ${chapter.title}. Skipping.`);
const finalErrorContent = `[다운로드 실패: ${errorContent}, 최대 재시도 횟수 ${MAX_RETRIES}회 초과]`;
GM_download({
url: 'data:text/plain;charset=utf-8,' + encodeURIComponent(finalErrorContent),
name: filename
});
}
}
// 다음 챕터 처리 (계산된 지연 시간 적용)
setTimeout(processNextChapter, delay);
},
onerror: function(error) {
// 네트워크 오류 처리
processedCount++;
log(`Error fetching ${chapter.title} (Network Error): ${JSON.stringify(error)}`);
setTimeout(processNextChapter, BASE_DELAY_MS);
}
});
}
log(`Starting background download for ${totalCount} individual chapters with ${BASE_DELAY_MS/1000}s base delay.`);
// 500ms 초기 딜레이 후 다운로드 시작
setTimeout(() => {
processNextChapter();
}, INITIAL_PAGE_DELAY_MS);
}
$(document).ready(function() {
if (window.location.href.includes('/book/') && !window.location.href.includes('/txt/')) {
addCheckboxes();
createListPageUI();
}
});
})();
12
공지
[필독] 임시 채널 규칙 & 신문고
Alria
25.09.08
71898
22
[필독] 임시 채널 규칙 & 신문고
공지
Alria
25.09.08 71898 22
공지
번역 정보글 및 팁 모음
Alria
25.11.03
42363
11
번역 정보글 및 팁 모음
공지
Alria
25.11.03 42363 11
공지
혹시 모를 상황을 대비한 대피소 구축 완료
Alria
05.05
8522
70
혹시 모를 상황을 대비한 대피소 구축 완료
공지
Alria
05.05 8522 70
공지
번역 요청탭의 요청 방식 수정 안내
Alria
25.12.29
39122
8
번역 요청탭의 요청 방식 수정 안내
공지
Alria
25.12.29 39122 8
공지
부관리자 모집 공지
Alria
05.05
5871
19
부관리자 모집 공지
공지
Alria
05.05 5871 19
일반
봇치가 메인인 작품이 뭐가 있을까요?
catcoco
04.03
7061
0
봇치가 메인인 작품이 뭐가 있을까요?
일반
catcoco
04.03 7061 0
잡담
페어리테일 용어집 만들고 싶었는대 존나 많아!
yasking
04.03
7235
0
페어리테일 용어집 만들고 싶었는대 존나 많아!
잡담
yasking
04.03 7235 0
질문
일상물에서 조금 비일상이 섞이는 느낌에 패러디는 없나?
youwh2002
04.03
9863
0
일상물에서 조금 비일상이 섞이는 느낌에 패러디는 없나?
질문
youwh2002
04.03 9863 0
[선협]
나에게 투자하면 무조건 대박, 선녀들이 미친 듯이 달려든다 1~494
777zzzabc1633
04.02
12805
33
나에게 투자하면 무조건 대박, 선녀들이 미친 듯이 달려든다 1~494
[선협]
777zzzabc1633
04.02 12805 33
잡담
아침에 잠깐 신비복소 봤는데
naru00
04.02
6402
0
아침에 잠깐 신비복소 봤는데
잡담
naru00
04.02 6402 0
후기
선협 기방 여기서 처음 올라온 거 읽고
오믈릿
04.02
9402
7
선협 기방 여기서 처음 올라온 거 읽고
후기
오믈릿
04.02 9402 7
후기
명일방주 패러디 후기, 나는 명일방주에서 시스템이 주는 선택지를 선택한다 & 명일방주 종려 템플릿 각성, 테라에서의 노후 생활
bebee
04.02
9216
6
명일방주 패러디 후기, 나는 명일방주에서 시스템이 주는 선택지를 선택한다 & 명일방주 종려 템플릿 각성, 테라에서의 노후 생활
후기
bebee
04.02 9216 6
일반
야마다 료 이 씨발련 진짜
sjw7whch8
04.02
6137
-8
야마다 료 이 씨발련 진짜
일반
sjw7whch8
04.02 6137 -8
잡담
크아아아ㅏㅏㅏㅏㅏㅏㅏㄱ 누가 욕구불만인거야는 언제 갱신되는가아아아!! 번역하고싶어ㅓㅓㅓ!!
명일방주너무좋아광인
04.02
6272
2
크아아아ㅏㅏㅏㅏㅏㅏㅏㄱ 누가 욕구불만인거야는 언제 갱신되는가아아아!! 번역하고싶어ㅓㅓㅓ!!
잡담
명일방주너무좋아광인
04.02 6272 2
후기
1987 나의 시대 후기
arjioooo
04.02
9896
4
1987 나의 시대 후기
후기
arjioooo
04.02 9896 4
[일반]
음식남녀(饮食男女) 1~850
ascr156
04.02
16566
40
음식남녀(饮食男女) 1~850
[일반]
ascr156
04.02 16566 40
번역 요청
无限神职 무한 신직 요청
7gpt7
04.02
9305
5
无限神职 무한 신직 요청
번역 요청
7gpt7
04.02 9305 5
잡담
성진지주는 번역이 goat라 더 재밌는듯
collectersss
04.02
7374
7
성진지주는 번역이 goat라 더 재밌는듯
잡담
collectersss
04.02 7374 7
[패러디]
[젠존제]엔비를 줍고 싶었는데, 트위기를 주웠다绝区零:本想捡安比,结果是崔姬 1-281 (완)
hyun6871
04.02
15103
46
[젠존제]엔비를 줍고 싶었는데, 트위기를 주웠다绝区零:本想捡安比,结果是崔姬 1-281 (완)
[패러디]
hyun6871
04.02 15103 46
복구 요청
데스노트코난 완결 복구 요청해요~
vhzkf
04.02
9916
0
데스노트코난 완결 복구 요청해요~
복구 요청
vhzkf
04.02 9916 0
잡담
자주 나오는 궤이 3대장
oo12345
04.02
8188
1
자주 나오는 궤이 3대장
잡담
oo12345
04.02 8188 1
잡담
선협세계에서 기방열기는 진짜 꼭 봐야한다고 생각함
greener-straper
04.02
10216
15
선협세계에서 기방열기는 진짜 꼭 봐야한다고 생각함
잡담
greener-straper
04.02 10216 15
[야설]
[AI 번역]선협 세계에서 기방 열기(在仙侠世界开妓院) 001~212 연재중 #선협 #하렘 #NTR #피폐
greener-straper
04.02
16532
47
[AI 번역]선협 세계에서 기방 열기(在仙侠世界开妓院) 001~212 연재중 #선협 #하렘 #NTR #피폐
[야설]
greener-straper
04.02 16532 47
잡담
그냥 눈팅이 많은거임?
greener-straper
04.02
9117
0
그냥 눈팅이 많은거임?
잡담
greener-straper
04.02 9117 0
[일반]
스피드 쌀먹 공유 모음 20260402
tassdar
04.02
18963
94
스피드 쌀먹 공유 모음 20260402
[일반]
tassdar
04.02 18963 94
잡담
신제 패러디 천존대용품 주인공 호감인 점
basileus31
04.02
9357
5
신제 패러디 천존대용품 주인공 호감인 점
잡담
basileus31
04.02 9357 5
[일반]
스피드 쌀먹 공유 모음 20260402
tassdar
04.02
18702
94
스피드 쌀먹 공유 모음 20260402
[일반]
tassdar
04.02 18702 94
잡담
지금 누가 조회수 봇이라도 돌리는 건가요...?
whenevereven
04.02
11321
5
지금 누가 조회수 봇이라도 돌리는 건가요...?
잡담
whenevereven
04.02 11321 5
잡담
극스포) 나루토 벌레공주 이거 읽으신 분 있나요? 결말이 이해가 좀 안되는데
sodlfmawls
04.02
9682
1
극스포) 나루토 벌레공주 이거 읽으신 분 있나요? 결말이 이해가 좀 안되는데
잡담
sodlfmawls
04.02 9682 1
후기
[우마무스메]트레이너 때려쳐! 후기
whenevereven
04.02
13486
2
[우마무스메]트레이너 때려쳐! 후기
후기
whenevereven
04.02 13486 2
잡담
뱅드림 토가와 아빠 연기설은 은근 많이나오네
하이바라
04.02
10881
1
뱅드림 토가와 아빠 연기설은 은근 많이나오네
잡담
하이바라
04.02 10881 1
[복구]
민국문호궐기 복구 가능할까요?
bohige
04.02
13489
-11
민국문호궐기 복구 가능할까요?
[복구]
bohige
04.02 13489 -11
[패러디]
신비의제왕-시간을가르는나의정체성 520화 완결
syuranian
04.02
17407
27
신비의제왕-시간을가르는나의정체성 520화 완결
[패러디]
syuranian
04.02 17407 27
일반
고전인 왜 소설 없어졌어? 4월 4일까지라했는뎆
2131244
04.02
9972
0
고전인 왜 소설 없어졌어? 4월 4일까지라했는뎆
일반
2131244
04.02 9972 0
일반
중국 주인공들은 하나같이 이쁘고 잘생긴듯
캔따거
04.02
9499
3
중국 주인공들은 하나같이 이쁘고 잘생긴듯
일반
캔따거
04.02 9499 3
s/ainovel
• 5,169명 구독중AI로 번역된 해외 소설 및 해외 패러디 소설 서브입니다.
2025-09-08 09:40:53 생성
삭제된 댓글입니다.