kone
ainovel

ainovel

twkan 탬퍼멍키 스크립트

11/05/2025, 14:27:14
정보
1844 views · 12 likes

// ==UserScript== // @name twkan Novel Downloader (Background Fetch) // @namespace http://tampermonkey.net/ // @version 4.13 // 버전 업데이트 (제목 추가) // @description Downloads novel text from twkan.com by fetching chapters in the background. Handles encoding issues, extracts full content, handles rate-limiting, and 403 forbidden errors. // @author You // @match https://twkan.com/book/* // @grant GM_download // @grant GM_xmlhttpRequest // @require https://code.jquery.com/jquery-3.6.0.min.js // @run-at document-end // ==/UserScript== (function() { 'use strict'; // Selectors and constants const episodeListSelector = 'div#allchapter ul li a'; const textToRemovePattern = /本書由.{0,20}\.com全網首發|本文將在網站同步更新。|喜歡的給個收藏給個推薦票,不喜歡的也別噴|(本章完)/ig; let isCheckAll = false; let countLabel; const BASE_DELAY_MS = 1500; const INITIAL_PAGE_DELAY_MS = 500; const MAX_RETRIES = 5; const MIN_FILE_SIZE_KB = 1; function log(message) { console.log(`[Tampermonkey: twkan Downloader] ${message}`); } // ======================================================= // UI 및 체크박스 함수 // ======================================================= 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).closest('li').find('input[type="checkbox"]:checked').length; countLabel.text(`선택된 항목 = ${checkedCount}개`); } function addCheckboxes() { let episodeIndex = 1; const $chapterLinks = $(episodeListSelector); log(`addCheckboxes called. Found ${$chapterLinks.length} chapter links.`); $chapterLinks.each(function() { const link = $(this); const listItem = link.closest('li'); if (listItem.find('input[type="checkbox"]').length > 0) { return; } const dataNum = listItem.data('num'); const index = dataNum !== undefined ? parseInt(dataNum, 10) : episodeIndex; const checkbox = $('').css('margin-right', '5px'); checkbox.on('change', updateCheckedCount); link.data({ index: index, title: link.text().trim(), url: this.href }); episodeIndex++; 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).closest('li').find('input[type="checkbox"]').prop('checked', isCheckAll); $(this).text(isCheckAll ? '모두 해제' : '모두 선택'); updateCheckedCount(); }); uncheckAllButton.on('click', function() { $(episodeListSelector).closest('li').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; }).closest('li').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).closest('li').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'); const chapterListContainer = $('.catalog').has('#allchapter'); if (chapterListContainer.length) { controlsContainer.append(checkAllButton, uncheckAllButton, downloadButton, rangeInput, checkTBButton, uncheckTBButton, betweenChkButton); controlsContainer.insertBefore(chapterListContainer); } 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); // 1. 제목 추출 (div.txtnav 내의 h1) const $titleElement = $tempDiv.find('.txtnav h1').first(); let chapterTitle = $titleElement.length ? $titleElement.text().trim() : ""; // 2. 본문 컨테이너 추출 const $contentContainer = $tempDiv.find('div#txtcontent0'); if ($contentContainer.length === 0) { log("Error: 'div#txtcontent0' area not found in fetched HTML."); return (chapterTitle ? chapterTitle + "\n\n" : "") + "[오류: 본문 영역을 찾을 수 없음]"; } let chapterContent = $contentContainer.html(); // 불필요한 HTML 요소 제거 (광고 스크립트 등) $contentContainer.find('script, .adsbygoogle').remove(); //
태그를 줄 바꿈 문자로 변환 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.replace(textToRemovePattern, ''); chapterContent = chapterContent.trim(); // 3. 제목과 본문을 합칩니다. if (chapterTitle) { return chapterTitle + "\n\n" + chapterContent; } return chapterContent; } function startBackgroundDownload() { const chaptersQueue = $(episodeListSelector).closest('li').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})` : ''})`); const baseUrl = chapter.url.split('/').slice(0, 3).join('/'); GM_xmlhttpRequest({ method: "GET", url: chapter.url, headers: { "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) { chapterContent = parseRawHtml(response.responseText); const cleanedContent = chapterContent.replace(/[\uFEFF]/g, '').trim(); 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) { 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) { 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.`); setTimeout(() => { processNextChapter(); }, INITIAL_PAGE_DELAY_MS); } /** * 웹페이지 스코프에 랩핑 로직을 주입합니다. */ function injectLoadMoreWrapper() { document.addEventListener('tampermonkey:chapters_loaded', function() { log("Custom event received. Re-running addCheckboxes."); setTimeout(addCheckboxes, 100); }); const script = document.createElement('script'); script.textContent = ` (function() { if (typeof LoadMore === 'function') { const originalLoadMore = LoadMore; LoadMore = function() { originalLoadMore.apply(this, arguments); setTimeout(function() { const event = new CustomEvent('tampermonkey:chapters_loaded'); document.dispatchEvent(event); }, 800); }; } })(); `; (document.body || document.head || document.documentElement).appendChild(script); script.remove(); } function initUI() { if (!window.location.href.match(/\/book\/\d+(\/index\.html)?$/)) { return; } injectLoadMoreWrapper(); addCheckboxes(); createListPageUI(); const $loadMoreButton = $('#loadmore'); if ($loadMoreButton.length) { log("LoadMore button found. Automatically clicking to expand all chapters."); $loadMoreButton.hide(); if ($loadMoreButton[0].click) { $loadMoreButton[0].click(); } else { $loadMoreButton.trigger('click'); } } } $(document).ready(function() { initUI(); }); })();

12
7 comments
11/05/25
정말 감사합니다 이것도 아래에 올리신것처럼 정렬해주셔서 올려주실수 있으실까요?
11/05/25 Edited 11/05/25
그냥 입력해도 됩니다. 아니면 코드를 보려면 긁어서 ai한테 정렬해달라고 하면 바로 해줄걸요.
11/05/25
아래 69shuba ai한테 정렬 부탁해서 하니까 되네요! 이것도 해보겠습니다
11/05/25
이것도 ai한테 정렬시키니 되네요! 정말 감사합니다
11/06/25
감사합니다
11/25/25
이거 지금은 되는거 맞음? 실행 버튼이 안보이는데.
11/26/25
방금해봤는데 잘됩니다. 전체 목록이 보이게 버튼 눌러서 들어가야함.
[AI 번역]모애응향 (暮霭凝香) snow_xefd (설범) [완결] #설범 #무협 #하렘 #조교
[야설]
greener-straper
12/06/25 7542 54
스피드 쌀먹 공유 모음 20251206
[일반]
tassdar
12/06/25 6612 80
[금서목록] 어떤 크로스오버의 하느님의 오른쪽 자리 1-897 완결
[복구]
라스트오리진으로 팔려간 댕라
12/06/25 3020 32
펌 두개
일반
python3
12/06/25 8637 38
개그 광인의 충격요법 1-785+
[일반]
kone1133
12/05/25 3315 19
스피드 쌀먹 공유 모음 20251205
[일반]
tassdar
12/05/25 7480 105
(위에 걸로 받으세요) 수선은 의외로 과학적인 면이 있다 1-1114(완결)
[일반]
kone1133
12/05/25 7783 41
우마무스메 프리티 더비 용어집
용어집
hyun6871
12/05/25 9518 16
[펌] 초성마문에 숨어 인재로 행세하다 (苟在初圣魔门当人材) 1-1054장
[선협]
ufiber
12/04/25 7126 40
스피드 쌀먹 공유 모음
[일반]
tassdar
12/04/25 9220 86
환생어촌 1-1646
[일반]
kone1133
12/04/25 12183 33
[우마무스메]우마무스메: 내가 클구리 고유기까지 알려줘야해? (완결)
[패러디]
hyun6871
12/04/25 5567 20
[실지주]나는 인생 시뮬레이터를 가지고 있다 1-630(완)
[패러디]
weieilkk22
12/04/25 8029 43
[AI 번역]최면안경(催眠眼镜)_17화_완결
[야설]
greener-straper
12/04/25 9441 41
[AI 번역]파란만장한 세월 峥嵘岁月 (1-55_1부완_2부_1화_연재중) #도시 #NTR #연재
[야설]
greener-straper
12/04/25 6281 32
스피드 쌀먹 공유 모음
[일반]
tassdar
12/03/25 6159 83
퍼온거 모음집
일반
talsoul
12/03/25 7446 114
어느 과학의 도시 명탐정 1-1188
[패러디]
kone1133
12/03/25 3517 27
[우마무스메]트레센에서 전력으로 농땡이친다人在特雷森,专职摆烂 (완결)
[패러디]
hyun6871
12/03/25 3732 48
검도지로 재번역본 1-2340 완
[선협]
asdf4132
12/03/25 5170 81
[실지주] 저는 당신들의 보상이 필요할 뿐입니다 1~684
[패러디]
라스트오리진으로 팔려간 댕라
12/03/25 4019 47
미국, 가로채기부터 시작되는 자유로운 인생 1-144
[패러디]
kone1133
12/03/25 4086 36
블루 스타 온라인 1-1258
[일반]
kone1133
12/03/25 3646 31
사이버펑크 2071 1-137
[패러디]
kone1133
12/03/25 3931 29
[학사신공] 집돌이 수선전 1-263
[패러디]
kone1133
12/03/25 3147 26
복구 + 직접
[선협]
sdl25
12/02/25 9870 48
내 애인의 어머니는 사실 현역 J컵 AV 여배우로, 딸의 남자친구와 하고 싶어 안달 난 암컷이었다 1~2 권.epub 복구
[복구]
soluna9902
12/02/25 2854 15
[타입문+다중]아호게 왕부터 시작되는 공개처형 1~1386(완)
[복구]
aabbp
12/02/25 5060 42
드림마스터 갱신 외 코난 나루토 복구
[패러디]
poiuytrewq
12/02/25 4461 32
코난 나는 사이코패스가 아니다 1~4243(완)
[패러디]
aabbp
12/02/25 4937 66