전에 만든건데 방금해보니 잘되네요.
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
71859
22
[필독] 임시 채널 규칙 & 신문고
공지
Alria
25.09.08 71859 22
공지
번역 정보글 및 팁 모음
Alria
25.11.03
42327
11
번역 정보글 및 팁 모음
공지
Alria
25.11.03 42327 11
공지
혹시 모를 상황을 대비한 대피소 구축 완료
Alria
05.05
8308
70
혹시 모를 상황을 대비한 대피소 구축 완료
공지
Alria
05.05 8308 70
공지
번역 요청탭의 요청 방식 수정 안내
Alria
25.12.29
38992
8
번역 요청탭의 요청 방식 수정 안내
공지
Alria
25.12.29 38992 8
공지
부관리자 모집 공지
Alria
05.05
5684
19
부관리자 모집 공지
공지
Alria
05.05 5684 19
정보
ai소설 보다가 등장인물 이름이 자꾸 바뀐다거나
poiuytrewq
01.03
8407
6
ai소설 보다가 등장인물 이름이 자꾸 바뀐다거나
정보
poiuytrewq
01.03 8407 6
정보
패러디 본것들 주관적인 평점겸 작품 추천 구걸
아스파가
01.03
9125
16
패러디 본것들 주관적인 평점겸 작품 추천 구걸
정보
아스파가
01.03 9125 16
정보
러시아 참독사이트 찾았다
paldus
01.01
8195
13
러시아 참독사이트 찾았다
정보
paldus
01.01 8195 13
정보
3.0 flash 누락 많으시면 프롬프트로 존나게 가스라이팅 돌리세요
naxlamas
25.12.26
9594
21
3.0 flash 누락 많으시면 프롬프트로 존나게 가스라이팅 돌리세요
정보
naxlamas
25.12.26 9594 21
정보
雷子 뜻이
sankhara
25.12.22
7090
1
雷子 뜻이
정보
sankhara
25.12.22 7090 1
정보
저도 소소한 base64팁입니다.
koronabi3154
25.12.19
8147
13
저도 소소한 base64팁입니다.
정보
koronabi3154
25.12.19 8147 13
정보
고유명사 찐빠 거르는 프롬프트 연구
greener-straper
25.12.18
9984
17
고유명사 찐빠 거르는 프롬프트 연구
정보
greener-straper
25.12.18 9984 17
정보
콜로모 프롬프트 공유
Sorkim
25.12.08
12136
15
콜로모 프롬프트 공유
정보
Sorkim
25.12.08 12136 15
정보
사이버펑크 2071 공식용어 바꾸기 팁(셀프검수)
parksangyong
25.12.07
11794
7
사이버펑크 2071 공식용어 바꾸기 팁(셀프검수)
정보
parksangyong
25.12.07 11794 7
정보
콜로모 번역기 사용중에 에러 뜨면
sankhara
25.12.06
9336
1
콜로모 번역기 사용중에 에러 뜨면
정보
sankhara
25.12.06 9336 1
정보
용어집을 좀 더 강력하게 적용시키는 법
74839
25.12.06
7996
8
용어집을 좀 더 강력하게 적용시키는 법
정보
74839
25.12.06 7996 8
정보
패러디 프롬프트에는 반드시 들어가야 할 것
74839
25.12.05
3519
7
패러디 프롬프트에는 반드시 들어가야 할 것
정보
74839
25.12.05 3519 7
정보
웹소설 분석용 템플릿 프롬프트 (수정)
부캐
25.12.05
9739
7
웹소설 분석용 템플릿 프롬프트 (수정)
정보
부캐
25.12.05 9739 7
정보
패러디 관련 정보 공유해요
t0po2
25.12.01
4436
14
패러디 관련 정보 공유해요
정보
t0po2
25.12.01 4436 14
정보
69shuba 탬퍼몽키 업데이트
kone1133
25.11.26
8266
17
69shuba 탬퍼몽키 업데이트
정보
kone1133
25.11.26 8266 17
정보
gemini blocked contents. 금지 컨텐츠 번역하는 프롬프트
kone1133
25.11.25
4374
20
gemini blocked contents. 금지 컨텐츠 번역하는 프롬프트
정보
kone1133
25.11.25 4374 20
정보
지금 내가 쓰는 배치번역기 잼민이 2.5pro용 프롬프트
naxlamas
25.11.23
7310
28
지금 내가 쓰는 배치번역기 잼민이 2.5pro용 프롬프트
정보
naxlamas
25.11.23 7310 28
정보
중뽕 제거 프롬프트 연구
greener-straper
25.11.14
3215
13
중뽕 제거 프롬프트 연구
정보
greener-straper
25.11.14 3215 13
정보
Xbanxia 소설 다운로드 스크립트 템퍼멍키
kone1133
25.11.12
1911
8
Xbanxia 소설 다운로드 스크립트 템퍼멍키
정보
kone1133
25.11.12 1911 8
정보
프롬프트에서 한국 음역규칙에 따라 적용하라 , 라는 어구는 수정하는게 좋음
sonic001
25.11.12
3198
6
프롬프트에서 한국 음역규칙에 따라 적용하라 , 라는 어구는 수정하는게 좋음
정보
sonic001
25.11.12 3198 6
정보
코랩도 좋은데 gemini-cli 도 한번 써보셈 되게 쉬움
greener-straper
25.11.12
3137
8
코랩도 좋은데 gemini-cli 도 한번 써보셈 되게 쉬움
정보
greener-straper
25.11.12 3137 8
정보
번역시 한자 오류 줄이는 방법
darksun
25.11.12
2919
7
번역시 한자 오류 줄이는 방법
정보
darksun
25.11.12 2919 7
정보
코딩을 아예 몰라도 코딩을 하는 방법 with Gemini. 이미지 多
74839
25.11.11
5247
23
코딩을 아예 몰라도 코딩을 하는 방법 with Gemini. 이미지 多
정보
74839
25.11.11 5247 23
정보
101kanshu 소설 받기 스크립트(수정)
kone1133
25.11.10
5996
19
101kanshu 소설 받기 스크립트(수정)
정보
kone1133
25.11.10 5996 19
정보
twkan 탬퍼멍키 스크립트
kone1133
25.11.05
1652
12
twkan 탬퍼멍키 스크립트
정보
kone1133
25.11.05 1652 12
정보
wordfix2팁
kone1133
25.11.05
779
12
wordfix2팁
정보
kone1133
25.11.05 779 12
정보
69shuba 탬퍼몽키
kone1133
25.11.05
7133
12
69shuba 탬퍼몽키
정보
kone1133
25.11.05 7133 12
정보
Base64 디코딩 아이콘 추가 스크립트.
kone1133
25.11.05
900
10
Base64 디코딩 아이콘 추가 스크립트.
정보
kone1133
25.11.05 900 10
정보
제가 쓰는 검수용 파이썬 스크립트.
kone1133
25.11.03
2997
20
제가 쓰는 검수용 파이썬 스크립트.
정보
kone1133
25.11.03 2997 20
정보
소개글/ 리뷰글 바로 딸깍 완성하는법 (검도제일선)
LG
25.11.03
5508
7
소개글/ 리뷰글 바로 딸깍 완성하는법 (검도제일선)
정보
LG
25.11.03 5508 7
s/ainovel
• 5,169명 구독중AI로 번역된 해외 소설 및 해외 패러디 소설 서브입니다.
2025-09-08 09:40:53 생성
삭제된 댓글입니다.