kone
sub_icon

ainovel

101kanshu 소설 받기 스크립트(수정)

2025-11-10 02:53:08
정보
조회 5994 · 좋아요 19

// ==UserScript== // @name 101kanshu Novel Downloader (Background Fetch) // @namespace http://tampermonkey.net/ // @version 5.3 // 비동기 로딩 완료 감지 로직 추가 // @description Downloads novel text from 101kanshu.com by fetching chapters in the background. Handles rate-limiting (429) and forbidden (403) errors with retries. Adds automatic 'LoadMore' functionality with robust loading detection. // @author You // @match https://101kanshu.com/book/* // @grant GM_download // @grant GM_xmlhttpRequest // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function() { 'use strict'; // ... (상수 및 보조 함수는 변경 없음) ... const episodeListSelector = 'div#allchapter ul li a[href*="/txt/"]'; const textToRemove = ''; let isCheckAll = false; let countLabel; const BASE_DELAY_MS = 1500; const INITIAL_PAGE_DELAY_MS = 500; // 비동기 로딩 감지 관련 상수 추가/수정 const POLLING_INTERVAL_MS = 100; // 0.1초마다 상태 확인 const MAX_LOAD_WAIT_MS = 5000; // 최대 5초 동안 로딩 대기 const MAX_RETRIES = 5; const MIN_FILE_SIZE_KB = 1; function log(message) { console.log(`[Tampermonkey: 101kanshu Downloader] ${message}`); } // ... (addGlobalStyle, updateCheckedCount, addCheckboxes, createListPageUI, parseRawHtml 함수는 변경 없음) ... 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); const insertionPoint = $('.mybox h3.mytitle.shuye'); if (insertionPoint.length) { controlsContainer.insertAfter(insertionPoint); } else { $('body').prepend(controlsContainer); } 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(); const chapterTitle = $contentContainer.find('h1').text().trim(); const $txtContent = $contentContainer.find('#txtcontent'); if ($txtContent.length === 0) { $contentContainer.find('h1, .txtinfo, .txtcenter, .txtad, script, br').remove(); return `\n\n${chapterTitle}\n\n[오류: 본문 내용을 찾을 수 없거나 추출에 실패했습니다.]`; } $txtContent.find('.txtad, script').remove(); let chapterContent = $txtContent.html(); chapterContent = chapterContent.replace(//ig, '\n'); chapterContent = chapterContent.replace(/ /g, ' '); chapterContent = chapterContent.replace(/[\u2003\u3000]/g, ' '); 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 `${chapterTitle}\n\n${chapterContent}`; } function startBackgroundDownload() { 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})` : ''})`); GM_xmlhttpRequest({ method: "GET", url: chapter.url, headers: { "Referer": chapter.url, "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) { chapterContent = parseRawHtml(response.responseText); const cleanedContent = chapterContent.replace(new RegExp(textToRemove.replace(/⊥/g, '⊥?'), 'g'), "").replace(/[\uFEFF]/g, '').trim(); if (cleanedContent.length < (MIN_FILE_SIZE_KB * 500)) { errorContent = `[본문 내용이 너무 짧음 (${cleanedContent.length}자), 재시도]`; 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) { errorContent = `[다운로드 실패: HTTP 상태 코드 ${response.status}]`; needsRetry = true; } else { 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); if (response.status === 429 && response.responseHeaders) { const retryAfterHeaderMatch = response.responseHeaders.match(/Retry-After: (\d+)/i); 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.`); setTimeout(() => { processNextChapter(); }, INITIAL_PAGE_DELAY_MS); } // ======================================================================= // 5. 초기화 및 LoadMore 감지 로직 (수정) // ======================================================================= function initializeUI() { // 이 함수에서 체크박스와 UI를 최종적으로 추가합니다. addCheckboxes(); createListPageUI(); log("Chapter list loaded and UI is ready."); } function waitForLoadMoreCompletion(maxAttempts, attempt = 0) { const $loadMoreButton = $('#loadmore'); if (attempt * POLLING_INTERVAL_MS >= MAX_LOAD_WAIT_MS) { log(`Max wait time (${MAX_LOAD_WAIT_MS}ms) reached. Initializing UI.`); initializeUI(); return; } // 1. LoadMore 버튼이 존재하고 숨겨져 있거나, 아예 사라졌는지 확인 // LoadMore 함수가 호출되면 AJAX가 완료된 후 버튼이 사라지거나 display:none이 됩니다. if ($loadMoreButton.length === 0 || $loadMoreButton.css('display') === 'none') { log(`LoadMore button disappeared/hidden after ${attempt * POLLING_INTERVAL_MS}ms. Initializing UI.`); initializeUI(); return; } // 2. 버튼이 아직 남아있고 visible 하다면 (로딩 중이거나 실패) 다시 시도 // (참고: 로딩 중에는 버튼 텍스트가 "正在加載..."로 바뀔 수 있지만, 사라지는 것을 기준으로 합니다.) setTimeout(() => { waitForLoadMoreCompletion(maxAttempts, attempt + 1); }, POLLING_INTERVAL_MS); } $(document).ready(function() { if (window.location.href.includes('/book/') && window.location.href.includes('index.html')) { const $loadMoreButton = $('#loadmore'); if ($loadMoreButton.length > 0 && $loadMoreButton.is(':visible')) { log("LoadMore button found. Triggering automatic click."); // LoadMore 함수를 호출하여 챕터 목록 로딩 시작 if (typeof window.LoadMore === 'function') { window.LoadMore(); } else { $loadMoreButton.trigger('click'); } // 로딩 완료를 기다리는 폴링 시작 waitForLoadMoreCompletion(); } else { // LoadMore 버튼이 없거나 이미 전체 목록이 로드된 경우 log("Chapter list assumed complete. Initializing UI directly."); initializeUI(); } } }); })();




정식 사이트 외에 사이버펑크 2071 최신이 가장 먼저 올라오는 것 같더라고요.

이 사이트는 123화까지 있고 69shuba는 117까지 밖에 없고...


(수정) 가끔 체크박스가 안떠서 수정했습니다.


19
댓글 6개
댓글 쓰기
25.11.10
감사합니다, 수고하셨습니다!!
25.11.10
오 새로운 사이트 알아가네요
25.11.10
감사합니당
25.11.10
오 여기 업데이트 빠르네요. 감사합니다
25.11.10
여긴 69shuba 같다
25.11.15
방금 쓰는데 오늘 무슨 제한 패치를 했네요. 1분에 10화만 받을 수 있게 서버쪽에서 제한을 걸었나 봅니다. 딜레이를 5.5초로 해놓고 받아야 합니다.
0 / 500
💥 26년 1분기, [AINOVEL] 순위 발표 (일부 줄거리 포함)
정보
코네
04.01 24250 59
53dushu에서 파일 찾으시는 분은
정보
naxlamas
03.11 10380 26
누더기 3.2 기반 번역 프롬프트
정보
greener-straper
02.09 9697 15
중국소설공유방 링크
정보
1atchup98fy
02.06 11015 21
패러디 본것들 주관적인 평점겸 작품 추천 구걸
정보
아스파가
01.03 9123 16
3.0 flash 누락 많으시면 프롬프트로 존나게 가스라이팅 돌리세요
정보
naxlamas
25.12.26 9593 21
고유명사 찐빠 거르는 프롬프트 연구
정보
greener-straper
25.12.18 9984 17
콜로모 프롬프트 공유
정보
Sorkim
25.12.08 12135 15
69shuba 탬퍼몽키 업데이트
정보
kone1133
25.11.26 8264 17
gemini blocked contents. 금지 컨텐츠 번역하는 프롬프트
정보
kone1133
25.11.25 4374 20
지금 내가 쓰는 배치번역기 잼민이 2.5pro용 프롬프트
정보
naxlamas
25.11.23 7308 28
코딩을 아예 몰라도 코딩을 하는 방법 with Gemini. 이미지 多
정보
74839
25.11.11 5245 23
101kanshu 소설 받기 스크립트(수정)
정보
kone1133
25.11.10 5994 19
제가 쓰는 검수용 파이썬 스크립트.
정보
kone1133
25.11.03 2997 20
!!! ㅡ Ai번역 너도 할 수 있다 (하는법)ㅡ !!!
정보
Ghost
25.11.02 8681 34
중국 소설 txt 다운로드 가능한 곳.
정보
cncjs2222
25.10.27 3886 15
사이버펑크2071,2075 용어집
정보
kone1133
25.10.21 4133 15
중국 소설 사이트 모음
정보
naxlamas
25.09.30 13181 41
AI소설 갤에 올리는 소설 번역 하는 법 정리
정보
dlatldyd123
25.09.10 8609 30