kone
ainovel

ainovel

twkan 탬퍼멍키 스크립트

11/05/2025, 14:27:14
정보
1834 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번역] 금단이 항성이라니, 너는 이걸 수선이라고 불러? 1-636 외전2(완)
[선협]
cain99
09/16/25 2632 16
[AI번역] 나는 수선계에서 대기만성한다我在修仙界大器晚成 1-1041
[선협]
cain99
09/16/25 2756 26
골 때리는 시스템과 솔직한 나 1-140장 외 4작품
[일반]
vitrras77
09/16/25 2438 32
[원피스]록스해적단 부선장1-161
[패러디]
brlosnb2744
09/16/25 2748 19
나는 고블린 슬레이어가 아니다 1-158 / 이 던전에는 버섯이 자란다 1-249
[일반]
vitrras77
09/16/25 2456 32
[AI 번역]【무림침륜 (武林沉沦)】(제1부 서장-43 + 제2부 쐐기 1-3 + 1-90 + 번외 (1-2)이말+제3부 1-26)#무협 #그린워싱 #조교_translated
[야설]
greener-straper
09/16/25 9184 34
내 공법이 자동으로 승급한다 每天一门功法圆满 1-1833
[선협]
shakeshack
09/15/25 3616 46
현환(선협에서 시스템으로 무한업그레이드) 1~1538txt
[선협]
esfly123
09/15/25 3565 39
내가대제에게머리를빌렸다/사망무사일기/블랙스톤코드
[일반]
wer890
09/15/25 2370 31
[AI 번역] 장생은 도경심법을 깨닫는 것부터 시작한다(长生从领悟道经心法开始) 1-824 [执笔初心]
[일반]
greener-straper
09/15/25 3916 64
[재업][AI번역] 만약 내가 성인이 된다면, 신선 관리가 나를 불러 말을 기르게 하겠지 001-943장 [선협] + 1 작품
[선협]
vitrras77
09/15/25 2602 21
[해리포터] 마블에 섞인 호그와트 교수 1~285
[패러디]
라스트오리진으로 팔려간 댕라
09/15/25 1999 35
[AI 번역] 풍월대륙(风月大陆) 1-36화10장(완) [端木] [19]
[야설]
greener-straper
09/15/25 3629 47
[원피스]재앙의 그란돈1-372완
[패러디]
brlosnb2744
09/15/25 2373 27
[Ai번역]장생지이 1-717 [완]
[일반]
tunamayo53
09/15/25 2286 23
[AI번역]청매선도 1-992 완 [乌泥]
[일반]
tunamayo53
09/15/25 1767 32
호그와트나는진짜모범마법사1-1008완
[패러디]
wer890
09/15/25 2006 36
고장난유토피아故障乌托邦1~636완
[일반]
wer890
09/15/25 2477 38
모음집
[일반]
vitrras77
09/15/25 7297 65
나루토 2대호카게에입양되고부터시작 1-395
[패러디]
wer890
09/15/25 2786 36
나는 도쿄에서 검도를 가르친다 1~745 완
[일반]
wer890
09/15/25 2097 33
[나루토] 우치하의 절대정의 1-193
[패러디]
金日成綜合大學
09/14/25 2159 20
[AI 번역] 선도의 끝(仙道尽头) 1-148 [怕辣的红椒]
[선협]
greener-straper
09/14/25 2036 17
[유희왕] 누가 그에게 유희왕을 1-1500
[패러디]
金日成綜合大學
09/14/25 1579 18
[나루토]제가 중급 시험을 치르는 모습이 생중계로 방송되었습니다! 1-942
[패러디]
weieilkk22
09/14/25 3441 42
[AI 번역] 도쿄버블라이프 1-1321
[일반]
serv-2004
09/14/25 2759 39
용호도주 1~2271 (완)
[일반]
라스트오리진으로 팔려간 댕라
09/14/25 2636 35
심해여신 1-852(완)
[일반]
trina
09/14/25 3369 30
초신기계사 1-1463화 외전 2화(완)
[일반]
trina
09/14/25 3169 44
[AI 번역] 후궁·대관원기(后宫·大观园记) 1-105 [hmhjhc] [19]
[야설]
greener-straper
09/14/25 2884 27