// ==UserScript== // @name 轻小说文库+ // @namespace Wenku8+ // @version 0.9.2 // @description 章节批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存 // @author PY-DNG // @match http*://www.wenku8.net/* // @connect wenku8.com // @connect wenku8.net // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_info // @noframes // @downloadURL none // ==/UserScript== // 记录 // 阅读API:http://dl.wenku8.com/pack.php?aid=2478&vid=92914 // 回帖API:https://www.wenku8.net/modules/article/reviewshow.php?rid=209631&aid=2751 (function() { 'use strict'; // CONSTS const HTML_DOWNLOAD_CONTENER = '
\n
\n《{BOOKNAME}》小说TXT简繁全本下载\n
\n
'; const HTML_DOWNLOAD_LINKS = '
\n简体(G)(载点一 \n载点二\n\n简体(U)(载点一 \n载点二\n\n繁体(U)(载点一 \n载点二\n
'; const HTML_DOWNLOAD_BOARD = '[轻小说文库+] 为您提供《{BOOKNAME}》的TXT简繁全本下载!
由此产生的一切法律及其他问题均由脚本用户承担
—— PY-DNG'; const CSS_DOWNLOAD = '.even {display: grid; grid-template-columns: repeat(3, 1fr); text-align: center;} .dlink {text-align: center;}'; const CSS_COMMON = '.plusbtn {color: rgb(0, 160, 0);} .plusbtn:hover {color: rgb(0, 100, 0);} .plusbtn:focus {color: rgb(0, 100, 0);}'; const CSS_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)'; const CLASSNAME_BUTTON = 'plusbtn'; const TEXT_TIP_COPY = '双击复制'; const TEXT_TIP_SERVERCHANGE = '点击切换线路'; const TEXT_GUI_DOWNLOAD_IMAGE = '下载图片'; const TEXT_GUI_DOWNLOAD_TEXT = '下载本章'; const TEXT_GUI_DOWNLOAD_REVIEW = '[下载本帖(共A页)]'; const TEXT_GUI_DOWNLOADING_REVIEW = '[下载中...(C/A)]'; const TEXT_GUI_DOWNLOADFINISH_REVIEW = '[下载完毕]'; const TEXT_GUI_DOWNLOADING = ' 下载中...'; const REG_GUI_DOWNLOADING = new RegExp(TEXT_GUI_DOWNLOADING + '$'); const TEXT_GUI_DOWNLOADED = ' (下载完毕)'; const REG_GUI_DOWNLOADED = new RegExp(TEXT_GUI_DOWNLOADED.replaceAll(/([\(\)])+/g, '\\$1') + '$'); const TEXT_GUI_DOWNLOADING_ALL = '下载中...(C/A)'; const TEXT_GUI_DOWNLOADED_ALL = '下载图片(已完成)'; /* \t ┌┬┐┌─┐┏┳┓┏━┓╭─╮ ├┼┤│┼│┣╋┫┃╋┃│╳│ └┴┘└─┘┗┻┛┗━┛╰─╯ ╲╱╭╮ ╱╲╰╯ */ /* **output format: Review Name.txt** ** 轻小说文库-帖子 [ID: reviewid] ** title ** 保存自: reviewlink ** 保存时间: savetime ** By scriptname Ver. version, author authorname ** ** ────────────────────────────── ** [用户: username userid] ** 用户名: username ** 用户ID: userid ** 加入日期: 1970-01-01 ** 用户链接: userlink ** 最早出现: 1楼 ** ────────────────────────────── ** ... ** ────────────────────────────── ** [#1 2021-04-26 17:53:49] [username userid] ** ────────────────────────────── ** content - line 1 ** content - line 2 ** content - line 3 ** ────────────────────────────── ** ** ────────────────────────────── ** [#2 2021-04-26 19:28:08] [username userid] ** ────────────────────────────── ** content - line 1 ** content - line 2 ** content - line 3 ** ────────────────────────────── ** ** ... ** ** ** [THE END] */ const TEXT_SPLIT_LINE_CHAR = '━'; const TEXT_SPLIT_LINE = TEXT_SPLIT_LINE_CHAR.repeat(20) const TEXT_OUTPUT_REVIEW_HEAD = '轻小说文库-帖子 [ID: {RWID}]\n{RWTT}\n保存自: {RWLK}\n保存时间: {SVTM}\nBy {SCNM} Ver. {VRSN}, author {ATNM}' const TEXT_OUTPUT_REVIEW_USER = '{LNSPLT}\n[用户: {USERNM} {USERID}]\n用户名: {USERNM}\n用户ID: {USERID}\n加入日期: {USERJT}\n用户链接: {USERLK}\n最早出现: {USERFL}楼\n{LNSPLT}' const TEXT_OUTPUT_REVIEW_FLOOR = '{LNSPLT}\n[#{RPNUMB} {RPTIME}] [{USERNM} {USERID}]\n{LNSPLT}\n{RPTEXT}\n{LNSPLT}'; const TEXT_OUTPUT_REVIEW_END = '\n[THE END]'; /** DoLog相关函数改自 Ocrosoft 的 Pixiv Previewer * [GitHub] Ocrosoft: https://github.com/Ocrosoft/ * [GreasyFork] Ocrosoft: https://greasyfork.org/zh-CN/users/63073 * [GreasyFork] Pixiv Previewer: https://greasyfork.org/zh-CN/scripts/30766 * [GitHub] Pixiv Previewer: https://github.com/Ocrosoft/PixivPreviewer **/ let LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, Elements: 5, }; let g_logCount = 0; let g_logLevel = LogLevel.Success; function DoLog(level, msgOrElement, isElement=false) { if (level <= g_logLevel) { let prefix = '%c'; let param = ''; if (level == LogLevel.Error) { prefix += '[Error]'; param = 'color:#ff0000'; } else if (level == LogLevel.Success) { prefix += '[Success]'; param = 'color:#00aa00'; } else if (level == LogLevel.Warning) { prefix += '[Warning]'; param = 'color:#ffa500'; } else if (level == LogLevel.Info) { prefix += '[Info]'; param = 'color:#888888'; } else if (level == LogLevel.Elements) { prefix += 'Elements'; param = 'color:#000000'; } if (level != LogLevel.Elements && !isElement) { console.log(prefix + msgOrElement, param); } else { console.log(msgOrElement); } if (++g_logCount > 512) { console.clear(); g_logCount = 0; } } } // Common actions addStyle(CSS_COMMON); // Get tab url api part const API = window.location.href.replace(/https?:\/\/www\.wenku8\.net\//, '').replace(/\?.*/, '') .replace(/^book\/\d+\.html?/, 'book').replace(/novel\/(\d+\/?)+\.html?$/, 'novel'); switch (API) { // Dwonload page case 'modules/article/packshow.php': pageDownload(); break; case 'modules/article/reviews.php': case 'modules/article/reviewshow.php': pageReview(); break; // Index page case 'index.php': pageIndex(); break; // Book page case 'book': pageBook(); break; // Novel page case 'novel': pageNovel(); break; // Other pages default: DoLog(LogLevel.Info, API); } // Book page add-on function pageBook() { const bookIdText = location.href.match(/\/(\d+)\.htm/)[1]; const bookNameElement = document.querySelector('#content > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(1) > span:nth-child(1) > b:nth-child(1)'); const bookName = bookNameElement.innerText; const authorNameElement = document.querySelector('#content > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2)'); const authorName = authorNameElement.innerText.substr(authorNameElement.innerText.indexOf(':') + 1); const downloadEnabled = document.querySelector('#content > div:nth-child(1) > div > fieldset:nth-child(1) > legend:nth-child(1) > b:nth-child(1)') !== null; const commentArea = document.querySelector('#pcontent'); const commentForm = document.querySelector('form[action^="https://www.wenku8.net/modules/article/reviews.php"]'); const commentSbmt = document.querySelector('td > input[name="Submit"]'); // Ctrl+Enter comment submit commentSbmt.value = '发表书评(Ctrl+Enter)'; commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em'; commentSbmt.style.height= 'auto'; commentArea.addEventListener('keydown', function() { let keycode = event.keyCode; if (keycode === 13 && event.ctrlKey && !event.altKey) { commentForm.submit(); } }) // Provide book & author name doubleclick copy if (tipshow && tiphide) { // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse bookNameElement.addEventListener('mouseover', function() {tipshow(TEXT_TIP_COPY);}); bookNameElement.addEventListener('mouseout' , tiphide); authorNameElement.addEventListener('mouseover', function() {tipshow(TEXT_TIP_COPY);}); authorNameElement.addEventListener('mouseout' , tiphide); } else { bookNameElement.title = TEXT_TIP_COPY; authorNameElement.title = TEXT_TIP_COPY; } bookNameElement.addEventListener('dblclick', function() {copyText(bookName);}); authorNameElement.addEventListener('dblclick', function() {copyText(authorName);}); // Provide txtfull download for book which download is disabled if (!downloadEnabled) { // Append download html model const modelContainer = document.createElement('div'); document.querySelector('#content div').appendChild(modelContainer); modelContainer.outerHTML = HTML_DOWNLOAD_CONTENER.replaceAll('{BOOKNAME}', bookName); //document.querySelector('#content div').innerHTML += HTML_DOWNLOAD_CONTENER.replaceAll('{BOOKNAME}', bookName); document.querySelector('#content div').lastChild.querySelector('fieldset').innerHTML += HTML_DOWNLOAD_LINKS.replaceAll('{BOOKID}', bookIdText); // Append CSS addStyle(CSS_DOWNLOAD); // Write textboard let textBoard = document.querySelector('#content > div:nth-child(1) > table:nth-child(4) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > span:nth-child(1) > b:nth-child(2)'); textBoard.innerHTML = HTML_DOWNLOAD_BOARD.replaceAll('{BOOKNAME}', bookName); textBoard.style.color = 'green'; } } // Review page add-on function pageReview() { // Release title area first if (document.querySelector('td > input[name="Submit"]')) { const table = document.querySelector('form>table'); const titleText = table.innerHTML.match(//)[0]; const titleHTML = titleText.replace(/^$/, ''); table.innerHTML = table.innerHTML.replace(titleText, titleHTML); } const commentArea = document.querySelector('#pcontent'); const commentForm = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]'); const commentSbmt = document.querySelector('td > input[name="Submit"]'); const commenttitl = document.querySelector('#ptitle'); // Ctrl+Enter comment submit if (commentSbmt) { commentSbmt.value = '发表书评(Ctrl+Enter)'; commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em'; commentSbmt.style.height= 'auto'; commentArea.addEventListener('keydown', hotkeyReply); commenttitl.addEventListener('keydown', hotkeyReply); } function hotkeyReply() { let keycode = event.keyCode; if (keycode === 13 && event.ctrlKey && !event.altKey) { commentForm.submit(); } } // ## Save whole post ## // GUI const pageCountText = document.querySelector('#pagelink>.last').href.match(/page=(\d+)/)[1]; const main = document.querySelector('#content'); const headBars = main.querySelectorAll('tr>td[align]'); headBars[0].width = '80%'; headBars[1].width = '20%'; const saveBtn = document.createElement('span'); saveBtn.innerText = TEXT_GUI_DOWNLOAD_REVIEW.replaceAll('A', pageCountText); saveBtn.classList.add(CLASSNAME_BUTTON); saveBtn.addEventListener('click', downloadWholePost); headBars[1].appendChild(saveBtn); /* // Testing getAllPages(function(data) { const txt = joinTXT(data); DoLog(LogLevel.Success, txt); }); */ // ## Function: Get data from page document or join it into the given data variable ## function getDataFromPage(document, data) { let i; // Get Floors; avatars uses for element locating const main = document.querySelector('#content'); const avatars = main.querySelectorAll('table div img.avatar'); // init data, floors and users if need let floors = {}, users = {}; if (data) { floors = data.floors; users = data.users; } else { data = {}; initData(data, floors, users); } for (i = 0; i < avatars.length; i++) { const floor = newFloor(floors, avatars, i); const elements = getFloorElements(floor); const reply = getFloorReply(floor); const user = getFloorUser(floor); appendFloor(floors, floor); } return data; function initData(data, floors, users) { // data vars data.floors = floors; floors.data = data; data.users = users; users.data = data; // review info data.link = location.href; data.id = location.href.match(/rid=(\d+)/) ? Number(location.href.match(/rid=(\d+)/)[1]) : 0; data.page = location.href.match(/page=(\d+)/) ? Number(location.href.match(/page=(\d+)/)[1]) : 1; data.title = main.querySelector('th strong').innerText; return data; } function newFloor(floors, avatars, i) { const floor = {}; floor.avatar = avatars[i]; floor.floors = floors; return floor; } function getFloorElements(floor) { const elements = {}; floor.elements = elements; elements.avatar = floor.avatar; elements.table = elements.avatar.parentElement.parentElement.parentElement.parentElement.parentElement; elements.tr = elements.table.querySelector('tr'); elements.tdUser = elements.table.querySelector('td.odd'); elements.tdReply = elements.table.querySelector('td.even'); elements.divUser = elements.tdUser.querySelector('div'); elements.aUser = elements.divUser.querySelector('a'); elements.attr = elements.tdReply.querySelector('div a').parentElement; elements.time = elements.attr.childNodes[0]; elements.number = elements.attr.childNodes[1]; elements.title = elements.tdReply.querySelector('div>strong'); elements.content = elements.tdReply.querySelector('hr+div'); return elements; } function getFloorReply(floor) { const elements = floor.elements; const reply = {}; floor.reply = reply; reply.time = elements.time.nodeValue.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0]; reply.number = Number(elements.number.innerText.match(/\d+/)[0]); reply.value = elements.content.innerText; reply.title = elements.title.innerText; return reply; } function getFloorUser(floor) { const elements = floor.elements; const user = {}; floor.user = user; user.id = elements.aUser.href.match(/uid=(\d+)/)[1]; user.name = elements.aUser.innerText; user.avatar = elements.avatar.src; user.link = elements.aUser.href; user.jointime = elements.divUser.innerText.match(/\d{4}-\d{2}-\d{2}/)[0]; const data = floor.floors.data; const users = data.users; if (!users.hasOwnProperty(user.id)) { users[user.id] = user; user.floors = [floor]; } else { const uFloors = users[user.id].floors; uFloors.push(floor); sortUserFloors(uFloors); } return user; } function sortUserFloors(uFloors) { uFloors.sort(function(F1, F2) { return F1.reply.number > F2.reply.number; }) } function appendFloor(floors, floor) { floors[floor.reply.number-1] = floor; } } // ## Function: Get pages and parse each pages to a data, returns data ## // callback(data, gotcount, finished) is called when xhr and parsing completed function getAllPages(callback) { let i, data, gotcount = 0; const ridMatcher = /rid=(\d+)/, pageMatcher = /page=(\d+)/; const lastpageUrl = document.querySelector('#pagelink>.last').href; const rid = Number(lastpageUrl.match(ridMatcher)[1]); const pageCount = Number(lastpageUrl.match(pageMatcher)[1]); const curPageNum = location.href.match(pageMatcher) ? Number(location.href.match(pageMatcher)[1]) : 1; for (i = 1; i <= pageCount; i++) { const url = lastpageUrl.replace(pageMatcher, 'page='+String(i)); DoLog(LogLevel.Info, 'getting page ' + String(i) + ', url=\'' + url + '\''); getDocument(url, joinPageData, callback); } function joinPageData(pageDocument, callback) { data = getDataFromPage(pageDocument, data); gotcount++; // log DoLog(LogLevel.Info, 'got ' + String(gotcount) + ' pages.'); if (gotcount === pageCount) { DoLog(LogLevel.Success, 'All pages xhr and parsing completed.'); DoLog(LogLevel.Success, data, true); } // callback if (callback) {callback(data, gotcount, gotcount === pageCount);}; } } // Function output function joinTXT(data, noSpliter=true) { const floors = data.floors; const users = data.users; // HEAD META DATA const saveTime = getTime(); const head = TEXT_OUTPUT_REVIEW_HEAD .replaceAll('{RWID}', data.id).replaceAll('{RWTT}', data.title).replaceAll('{RWLK}', data.link) .replaceAll('{SVTM}', saveTime).replaceAll('{SCNM}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version).replaceAll('{ATNM}', GM_info.script.author); // join userinfos let userText = ''; for (const [pname, user] of Object.entries(users)) { if (!isNumeric(pname)) {continue;}; userText += TEXT_OUTPUT_REVIEW_USER .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{USERNM}', user.name) .replaceAll('{USERID}', user.id).replaceAll('{USERJT}', user.jointime) .replaceAll('{USERLK}', user.link).replaceAll('{USERFL}', user.floors[0].reply.number); userText += '\n'.repeat(2); } // join floors let floorText = ''; for (const [pname, floor] of Object.entries(floors)) { if (!isNumeric(pname)) {continue;}; const avatar = floor.avatar; const elements = floor.elements; const user = floor.user; const reply = floor.reply; floorText += TEXT_OUTPUT_REVIEW_FLOOR .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{RPNUMB}', String(reply.number)) .replaceAll('{RPTIME}', reply.time).replaceAll('{USERNM}', user.name) .replaceAll('{USERID}', user.id).replaceAll('{RPTEXT}', reply.value); floorText += '\n'.repeat(2); } // End const foot = TEXT_OUTPUT_REVIEW_END; // return const txt = head + '\n'.repeat(2) + userText + '\n'.repeat(2) + floorText + '\n'.repeat(2) + foot; return txt; } // ## Function: Download the whole post ## function downloadWholePost() { // Continues only if not working if (downloadWholePost.working) {return;}; downloadWholePost.working = true; // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW .replaceAll('C', '0').replaceAll('A', pageCountText); // go work! getAllPages(function(data, gotCount, finished) { // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW .replaceAll('C', String(gotCount)).replaceAll('A', pageCountText); // Stop here if not completed if (!finished) {return;}; // Join text const TXT = joinTXT(data); // Download const blob = new Blob([TXT],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const name = '文库贴 - ' + String(data.id) + '.txt'; const a = document.createElement('a'); a.href = url; a.download = name; a.click(); // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADFINISH_REVIEW; // Work finish downloadWholePost.working = false; }) } } // Novel page add-on function pageNovel() { const title = document.querySelector('#title').textContent; const isImagePage = title.includes('插图') || title.includes('插圖'); const rightButtonDiv = document.querySelector('#linkright'); const rightButtons = rightButtonDiv.childNodes; let dlCompleted = 0; // number of completed download tasks let dlAllCount = 0; // number of all download tasks let dlAllRunning = false; // whether there is downloadAllImages running // append control buttons let i; let spliter, button = rightButtonDiv.querySelector('a').cloneNode(); for (i = 0; i < rightButtons.length; i++) { if (rightButtons[i].textContent.includes('|')) { spliter = rightButtons[i].cloneNode(); } } // Attributes & Display config let allImages, buttonText; let clickFunc; if (isImagePage) { buttonText = TEXT_GUI_DOWNLOAD_IMAGE; clickFunc = function() {downloadAllImages();}; } else { buttonText = TEXT_GUI_DOWNLOAD_TEXT; clickFunc = function() {downloadText();}; } button.href = 'javascript:void(0);'; button.target = ''; button.innerText = buttonText; button.style.color = '#00BB00'; button.addEventListener('click', clickFunc); rightButtonDiv.insertBefore(spliter, rightButtonDiv.lastChild); rightButtonDiv.insertBefore(button, rightButtonDiv.lastChild); rightButtonDiv.style.width = '500px'; // Prevent URL.revokeObjectURL in script 轻小说文库下载 const Ori_revokeObjectURL = URL.revokeObjectURL; URL.revokeObjectURL = function(arg) { if (typeof(arg) === 'string' && arg.substr(0, 5) === 'blob:') {return false;}; return Ori_revokeObjectURL(arg); } function downloadText() { const contentEle = document.querySelector('#content'); let content = contentEle.innerText//.replaceAll('\n', '\r\n'); if (content.length === 0) { return false; } // Clear spaces content = content.split('\n'); for (let i = 0; i < content.length; i++) { content[i] = content[i].trim(); } content = content.join('\r\n'); // Download const blob = new Blob([content],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const name = title + '.txt'; const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = name; a.click(); } function downloadAllImages() { if (dlAllRunning) { return false; } allImages = document.querySelectorAll('#content > div.divimage img'); dlAllCount = allImages.length; dlCompleted = 0; dlAllRunning = true; // Display button.innerText = TEXT_GUI_DOWNLOADING_ALL.replace('C', '0').replace('A', String(dlAllCount)); rightButtonDiv.style.width = '550px'; // Download const numLen = String(dlAllCount).length; for (let i = 0; i < dlAllCount; i++) { const imageName = title + '_' + fillNumber(i+1, numLen) + '.jpg'; const url = allImages[i].src; if (allImages[i].src.substr(0,5) === 'blob:') { const image = new Image(); image.onload = function() { saveBlobToFile(toImageFormatURL(image, 1), imageName); dlIncrease(button); } image.src = url; } else { download(url, imageName, button); } } } // File download function function download(url, name, displayElement) { // Check if (!url || !name) { return false; } // xmlHTTPRequest GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(request) { // DataURL let objURL = URL.createObjectURL(request.response); // toImageFormatURL const image = new Image(); image.src = objURL; image.onload = function() { //image.style.display = 'none'; //document.body.appendChild(image); const formatURL = toImageFormatURL(image, 1); //document.body.removeChild(image); saveBlobToFile(formatURL, name); dlIncrease(displayElement); }; } }) return true; } // Increase dlCompleted and judge dlAllRunning function dlIncrease(displayElement) { // Task count decrease dlCompleted++; if (dlCompleted === dlAllCount) { dlAllRunning = false; } // Display if (displayElement) { displayElement.innerText = TEXT_GUI_DOWNLOADING_ALL .replace('C', String(dlCompleted)).replace('A', String(dlAllCount)); if (!dlAllRunning) { displayElement.innerText = TEXT_GUI_DOWNLOADED_ALL; rightButtonDiv.style.width = '550px'; } } } // Blob url file saving function function saveBlobToFile(blobURL, name) { // Create const a = document.createElement('a'); a.style.display = 'none'; a.href = blobURL; a.download = name; a.click(); } // Image format changing function function toImageFormatURL(image, format) { if (typeof(format) === 'number') {format = ['image/jpeg', 'image/png', 'image/webp'][format-1]} const cvs = document.createElement('canvas'); cvs.width = image.width; cvs.height = image.height; const ctx = cvs.getContext('2d'); ctx.drawImage(image, 0, 0); return cvs.toDataURL(format); } } // Index page add-on function pageIndex() { } // Download page add-on function pageDownload() { let i; let dlCount = 0; // number of active download tasks let dlAllRunning = false; // whether there is downloadAll running /* ******************* GUI ******************* */ // Create left operation GUI let downloadGUI = document.querySelectorAll('#left div.block')[1].cloneNode(true); // Rename title downloadGUI.querySelector('.blocktitle .txt').innerHTML = '下载全部章节'; // Remove content downloadGUI.removeChild(downloadGUI.querySelector('.blockcontent')); // Create operation ul list let optionButtonsForm = document.querySelector('#left div.block div.blockcontent div ul[style]').cloneNode(true); // Reset lis const NAMES = ['本地简体(G)', '本地简体(U)', '本地繁体(U)', '地址二简体(G)', '地址二简体(U)', '地址二繁体(U)']; let lis = optionButtonsForm.querySelectorAll('li'); let li = lis[0].cloneNode(true); let newli; li.querySelector('a').href = 'javascript:void(0);'; li.querySelector('a').className = ''; li.querySelector('a').classList.add(CLASSNAME_BUTTON); li.querySelector('a').innerHTML = '默认按钮文本'; for (i = 0; i < 6; i++) { // If li exist, remove it if (lis[i]) { optionButtonsForm.removeChild(lis[i]); }; // Create a new one newli = li.cloneNode(true); // Modify name newli.querySelector('a').innerHTML = NAMES[i]; // Mark i newli.i = i; // Append it optionButtonsForm.appendChild(newli); // Add event listener newli.addEventListener('click', function() { // i refers to its current value in loop by marking on the li element downloadAll(this.i); }) } // Create a container let blockcontent = document.createElement('div'); blockcontent.classList.add('blockcontent'); blockcontent.style.paddingLeft = '10px'; // Append ul blockcontent.appendChild(optionButtonsForm); // Append container downloadGUI.appendChild(blockcontent); // Append GUI document.querySelector('#left').appendChild(downloadGUI); // Servers GUI let servers = document.querySelectorAll('#content>b'); let serverEles = []; for (i = 0; i < servers.length; i++) { if (servers[i].innerText.includes('wenku8.com')) { serverEles.push(servers[i]); } } for (i = 0; i < serverEles.length; i++) { serverEles[i].classList.add(CLASSNAME_BUTTON); serverEles[i].addEventListener('click', function() {changeAllServers(this.innerText);}); if (tipshow && tiphide) { // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse serverEles[i].addEventListener('mouseover', function() {tipshow(TEXT_TIP_SERVERCHANGE);}); serverEles[i].addEventListener('mouseout' , tiphide); } else { serverEles[i].title = TEXT_TIP_SERVERCHANGE; } } /* ******************* Code ******************* */ // Change all server elements function changeAllServers(server) { let i; const allA = document.querySelectorAll('.even a'); for (i = 0; i < allA.length; i++) { changeServer(server, allA[i]); } } // Change server for an element function changeServer(server, element) { if (!element.href) {return false;}; element.href = element.href.replace(/\/\/dl\d?\.wenku8\.com\//g, '//' + server + '/'); } // Get novel name const novelName = document.querySelector('html body div.main div#centerm div#content table.grid caption a').innerText; let downloadAll = function(type) { // Check: only download while no download active tasks currently if (dlAllRunning) { return false; } dlAllRunning = true; // GUI display downloadGUI.querySelector('.blocktitle .txt').innerHTML = TEXT_GUI_DOWNLOADING; // Name customize let NAME = novelName + ' {j}.'; let allNames = getAllNames(); if (window.location.href.indexOf('txt') != -1) { NAME += 'txt'; } else { NAME += document.querySelector('html body div.main div#centerm div#content table.grid tbody tr td.even a').innerText.replace(/[^\w]+/, '').toLowerCase(); } let i,j = 0; const allA = document.querySelectorAll('.even a'); for (i = type; i < allA.length; i = i + 6) { /*GM_download({ url: allA[i].href, name: NAME.replace('{j}', (window.location.href.indexOf('txtfull') === -1 ? allNames[j] : '')) });*/ download( allA[i].href, NAME.replace('{j}', (window.location.href.indexOf('txtfull') === -1 ? allNames[j] : '')), allA[i].parentElement.parentElement.querySelector('td.odd') ) j += 1; } downloadGUI.querySelector('.blocktitle .txt').innerHTML = '下载全部章节'; } function getAllNames() { let all = document.querySelectorAll('.grid tbody tr .odd'); let names = []; for (let i = 0; i < all.length; i++) { names[i] = all[i].innerText.replace(REG_GUI_DOWNLOADED, '').replace(REG_GUI_DOWNLOADING,''); } return names; } // File download function function download(url, name, displayElement) { // Check if (!url || !name) { return false; } // dl task count increase dlCount++; // Display let text = ''; if (displayElement) { if (displayElement.innerText) {text = displayElement.innerText.replace(REG_GUI_DOWNLOADED, '').replace(REG_GUI_DOWNLOADING,'');}; displayElement.innerText = text + TEXT_GUI_DOWNLOADING; } // xmlHTTPRequest GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(request) { // DataURL let objURL = URL.createObjectURL(request.response); // Create const a = document.createElement('a'); a.style.display = 'none'; a.href = objURL; a.download = name; a.click(); // Task count decrease dlCount--; if (dlCount === 0) { dlAllRunning = false; } // Display if (displayElement) { displayElement.innerText = TEXT_GUI_DOWNLOADED.replace(/^ /, ''); if (text) {displayElement.innerText = text + TEXT_GUI_DOWNLOADED;}; } } }) return true; } } // Download and parse a url page into a html document(dom). // when xhr onload: callback.apply([dom, args]) function getDocument(url, callback, args) { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { const htmlblob = response.response; const reader = new FileReader(); reader.onload = function(e) { const htmlText = reader.result; const dom = new DOMParser().parseFromString(htmlText, 'text/html'); args = [dom].concat(args); callback.apply(null, args); //callback(dom, htmlText); } reader.readAsText(htmlblob, 'GBK'); /* 注意!原来这里只是使用了DOMParser,DOMParser不像iframe加载Document一样拥有完整的上下文并执行所有element的功能, ** 只是按照HTML格式进行解析,所以在文库页面的GBK编码下仍然会按照UTF-8编码进行解析,导致中文乱码。 ** 所以处理dom时不要使用ASC-II字符集以外的字符! ** ** 注:现在使用了FileReader来以GBK编码解析htmlText,故编码问题已经解决,可以正常使用任何字符 */ } }) } // Get a time text like 1970-01-01 00:00:00 function getTime(dateSpliter='-', timeSpliter=':') { const d = new Date(); const fulltime = fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) + ' ' + fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2); return fulltime; } // Fill number text to certain length with '0' function fillNumber(number, length) { let str = String(number); for (let i = str.length; i < length; i++) { str = '0' + str; } return str; } // Judge whether the str is a number function isNumeric(str) { const result = Number(str); return !isNaN(result) && str !== ''; } // Append a style text to document() with a