downloading novel as txt from 69shuba.com
// ==UserScript==
// @name novel2txt
// @namespace http://tampermonkey.net/
// @version 0.1
// @description downloading novel as txt from 69shuba.com
// @author @gzmonkey
// @match *://www.69shuba.com/*
// @grant none
// @license MIT
// ==/UserScript==
function initButton() {
'use strict';
const button = document.createElement('button');
button.innerText = 'Copy TXT';
button.style.position = 'fixed';
button.style.top = '16px';
button.style.right = '16px';
button.style.padding = '14px 20px';
button.style.fontSize = '18px';
button.style.fontWeight = '700';
button.style.background = '#ffcc00';
button.style.color = '#000';
button.style.border = '2px solid #000';
button.style.borderRadius = '8px';
button.style.zIndex = '2147483647';
button.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.25)';
button.onclick = function() {
console.log('[TM] Button clicked.');
const chapterLinks = Array.from(document.querySelectorAll('#catalog ul li a'));
if (chapterLinks.length === 0) {
console.warn('[TM] No chapter links found with selector:', '#catalog ul li a');
return;
}
const listText = chapterLinks.map((link, idx) => {
const title = (link.textContent || '').trim();
const url = (link.href || '').trim();
return `${idx + 1}. ${title}\n${url}`;
}).join('\n\n');
showEditablePopup(listText, chapterLinks);
};
if (!document.body) {
console.warn('[TM] document.body is not available.');
return;
}
document.body.appendChild(button);
}
function showEditablePopup(listText, chapterLinks) {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.zIndex = '2147483647';
const panel = document.createElement('div');
panel.style.position = 'absolute';
panel.style.top = '50%';
panel.style.left = '50%';
panel.style.transform = 'translate(-50%, -50%)';
panel.style.width = '80vw';
panel.style.maxWidth = '900px';
panel.style.height = '70vh';
panel.style.background = '#fff';
panel.style.borderRadius = '10px';
panel.style.padding = '16px';
panel.style.boxShadow = '0 12px 30px rgba(0, 0, 0, 0.35)';
panel.style.display = 'flex';
panel.style.flexDirection = 'column';
panel.style.gap = '10px';
const title = document.createElement('div');
title.textContent = 'Chapter List (edit to select)';
title.style.fontSize = '18px';
title.style.fontWeight = '700';
const textarea = document.createElement('textarea');
textarea.value = listText;
textarea.style.flex = '1';
textarea.style.width = '100%';
textarea.style.resize = 'none';
textarea.style.fontSize = '14px';
textarea.style.fontFamily = 'monospace';
const status = document.createElement('input');
status.type = 'text';
status.readOnly = true;
status.value = '';
status.style.fontSize = '14px';
status.style.fontFamily = 'monospace';
status.style.border = '1px solid #ccc';
status.style.padding = '4px';
status.style.marginTop = '4px';
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.gap = '8px';
actions.style.justifyContent = 'flex-end';
const startBtn = document.createElement('button');
startBtn.textContent = 'Start Fetch';
startBtn.style.padding = '8px 12px';
const stopBtn = document.createElement('button');
stopBtn.textContent = 'Stop';
stopBtn.style.padding = '8px 12px';
stopBtn.disabled = true;
let stopRequested = false;
startBtn.onclick = () => {
const lines = textarea.value.split('\n').filter(Boolean);
const urls = lines.filter(line => line.startsWith('https://'));
startBtn.disabled = true;
startBtn.textContent = 'Fetching...';
stopBtn.disabled = false;
stopRequested = false;
fetchAndAppend(urls, textarea, status, () => {
startBtn.textContent = 'Done';
stopBtn.disabled = true;
}, () => stopRequested);
};
stopBtn.onclick = () => {
stopRequested = true;
stopBtn.disabled = true;
stopBtn.textContent = 'Stopping...';
textarea.value += '\n[Stopped by user]';
};
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.style.padding = '8px 12px';
closeBtn.onclick = () => overlay.remove();
actions.appendChild(startBtn);
actions.appendChild(stopBtn);
actions.appendChild(closeBtn);
panel.appendChild(title);
panel.appendChild(textarea);
panel.appendChild(status);
panel.appendChild(actions);
overlay.appendChild(panel);
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
overlay.remove();
}
});
document.body.appendChild(overlay);
}
async function fetchAndAppend(urls, textarea, status, doneCallback, shouldStop) {
textarea.value = '';
const maxSize = 3.5 * 1024 * 1024; // 3.5 MB
for (let i = 0; i < urls.length; i++) {
if (shouldStop && shouldStop()) {
textarea.value += '\n[Stopped by user]';
status.value = 'Stopped by user';
console.warn('[TM] Stop requested by user.');
break;
}
const url = urls[i];
try {
status.value = `Fetching ${i + 1}/${urls.length}...`;
const content = await fetchChapterContent(url);
const title = `Chapter ${i + 1}`;
const block = `[${title}]\n${content}\n\n`;
const newSize = new Blob([textarea.value + block]).size;
if (newSize > maxSize) {
textarea.value += `\n[Stopped: size limit reached]`;
status.value = 'Stopped: size limit reached';
console.warn('[TM] Size limit reached, stopping.');
break;
}
textarea.value += block;
textarea.scrollTop = textarea.scrollHeight;
status.value = `Fetched: ${title}`;
console.log('[TM] Fetched:', title);
if (i < urls.length - 1) {
const delayMs = 500 + Math.floor(Math.random() * 2500);
await delay(delayMs);
}
} catch (error) {
console.error('[TM] Fetch failed:', url, error);
textarea.value += `[Fetch failed]\n${url}\n\n`;
status.value = `Fetch failed: Chapter ${i + 1}`;
break;
}
}
if (doneCallback) doneCallback();
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchChapterContent(url) {
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const buffer = await response.arrayBuffer();
const html = new TextDecoder('gbk').decode(buffer);
const doc = new DOMParser().parseFromString(html, 'text/html');
const contentRoot = doc.querySelector('.txtnav');
if (!contentRoot) {
return '';
}
const cleanupSelectors = ['.txtinfo', '#txtright', '.contentadv', '.bottom-ad', '.bottom-ad2', '.page1', 'script'];
cleanupSelectors.forEach((selector) => {
contentRoot.querySelectorAll(selector).forEach((node) => node.remove());
});
const htmlWithBreaks = contentRoot.innerHTML.replace(/<br\s*\/?>/gi, '\n');
const temp = doc.createElement('div');
temp.innerHTML = htmlWithBreaks;
const rawText = temp.textContent || '';
return rawText.replace(/\n{3,}/g, '\n\n').trim();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initButton);
} else {
initButton();
}