// ==UserScript==
// @name 轻小说文库+
// @namespace Wenku8+
// @version 1.0.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_getValue
// @grant GM_setValue
// @grant GM_info
// @noframes
// ==/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
// 查人API:http://www.wenku8.com/modules/article/reviewslist.php?keyword=136877
(function() {
'use strict';
// CONSTS
const NUMBER_MAX_XHR = 10;
const NUMBER_LOGSUCCESS_AFTER = NUMBER_MAX_XHR * 2;
const KEY_COMMENT_DRAFTS = 'comment-drafts';
const KEY_DRAFT_VERSION = 'version';
const VALUE_DRAFT_VERSION = '0.1';
const HTML_DOWNLOAD_CONTENER = '<div id="dctn" style=\"margin:0px auto;overflow:hidden;\">\n<fieldset style=\"width:820px;height:35px;margin:0px auto;padding:0px;\">\n<legend><b>《{BOOKNAME}》小说TXT简繁全本下载</b></legend>\n</fieldset>\n</div>';
const HTML_DOWNLOAD_LINKS = '<div class="even">\n<span>简体(G)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=txt&id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=txt&id={BOOKID}&fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n\n<span>简体(U)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=utf8&id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=utf8&id={BOOKID}&fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n\n<span>繁体(U)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=big5&id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=big5&id={BOOKID}&fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n </div>';
const HTML_DOWNLOAD_BOARD = '[轻小说文库+] 为您提供《{BOOKNAME}》的TXT简繁全本下载!</br>由此产生的一切法律及其他问题均由脚本用户承担</br>—— PY-DNG';
const CSS_DOWNLOAD = '.even {display: grid; grid-template-columns: repeat(3, 1fr); text-align: center;} .dlink {text-align: center;}';
const CSS_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)';
const CSS_COMMON = '.plusbtn {color: rgb(0, 160, 0);} .plusbtn:hover {color: rgb(0, 100, 0);} .plusbtn:focus {color: 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_WAITING = ' 等待中...'; const REG_GUI_WAITING = new RegExp(TEXT_GUI_WAITING + '$');
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 = '下载图片(已完成)';
const TEXT_GUI_AUTOSAVE = '(您输入的内容已保存到书评草稿中)';
const TEXT_GUI_AUTOSAVE_CLEAR = '(草稿为空)';
const TEXT_GUI_AUTOSAVE_RESTORE = '(已从书评草稿中恢复了您上次编辑的内容)';
// Emoji smiles
const SmList =
[{text:"/:O",id:"1",alt:"惊讶"}, {text:"/:~",id:"2",alt:"撇嘴"}, {text:"/:*",id:"3",alt:"色色"},
{text:"/:|",id:"4",alt:"发呆"}, {text:"/8-)",id:"5",alt:"得意"}, {text:"/:LL",id:"6",alt:"流泪"},
{text:"/:$",id:"7",alt:"害羞"}, {text:"/:X",id:"8",alt:"闭嘴"}, {text:"/:Z",id:"9",alt:"睡觉"},
{text:"/:`(",id:"10",alt:"大哭"}, {text:"/:-",id:"11",alt:"尴尬"}, {text:"/:@",id:"12",alt:"发怒"},
{text:"/:P",id:"13",alt:"调皮"}, {text:"/:D",id:"14",alt:"呲牙"}, {text:"/:)",id:"15",alt:"微笑"},
{text:"/:(",id:"16",alt:"难过"}, {text:"/:+",id:"17",alt:"耍酷"}, {text:"/:#",id:"18",alt:"禁言"},
{text:"/:Q",id:"19",alt:"抓狂"}, {text:"/:T",id:"20",alt:"呕吐"}]
/* \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://gf.qytechs.cn/zh-CN/users/63073
* [GreasyFork] Pixiv Previewer: https://gf.qytechs.cn/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 = LogLevel.Info, 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);
GMXHRHook(NUMBER_MAX_XHR);
// 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;
// ReviewList page
case 'modules/article/reviews.php':
areaReply();
break;
// Review page
case 'modules/article/reviewshow.php':
areaReply();
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
areaReply();
// 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';
}
}
// Reply area add-on
function areaReply() {
/* ## Release title area ## */
if (document.querySelector('td > input[name="Submit"]') && !document.querySelector('#ptitle')) {
const table = document.querySelector('form>table');
const titleText = table.innerHTML.match(/<!--[\s\S]+id="ptitle"[\s\S]+-->/)[0];
const titleHTML = titleText.replace(/^<!--\s*/, '').replace(/\s*-->$/, '');
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');
const commentbttm = commentSbmt.parentElement;
/* ## 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);
}
/* ## Comment auto-save ## */
// GUI
const asTip = document.createElement('span');
commentbttm.appendChild(asTip);
// Review-Page: Same rid, same savekey - 'rid123456'
// Book-Page & Book-Review-List-Page: Same bookid, same savekey - 'bid1234'
let commentData = {
rid : getUrlArgv('rid', Number),
aid : getUrlArgv('aid', Number),
bid : location.href.match(/\/book\/(\d+).htm/) ? Number(location.href.match(/\/book\/(\d+).htm/)[1]) : 0,
page : getUrlArgv('page', Number, 1)
}
commentData.key = commentData.rid ? 'rid' + String(commentData.rid) : 'bid' + String(commentData.bid);
restoreDraft();
const events = ['focus', 'blur', 'mousedown', 'keydown', 'keyup'];
const eventEles = [commentArea, commenttitl];
for (const eventEle of eventEles) {
for (const event of events) {
eventEle.addEventListener(event, saveDraft);
}
}
function saveDraft() {
const content = commentArea.value;
const title = commenttitl.value;
if (!content && !title) {
clearDraft();
return;
} else if (commentData.content === content && commentData.title === title) {
return;
}
commentData.content = content;
commentData.title = title;
const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});
allCData[commentData.key] = commentData;
allCData[KEY_DRAFT_VERSION] = VALUE_DRAFT_VERSION;
GM_setValue(KEY_COMMENT_DRAFTS, allCData);
asTip.innerHTML = TEXT_GUI_AUTOSAVE;
}
function restoreDraft() {
const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});
if (!allCData[commentData.key]) {return false;};
commentData = allCData[commentData.key];
commenttitl.value = commentData.title;
commentArea.value = commentData.content;
asTip.innerHTML = TEXT_GUI_AUTOSAVE_RESTORE;
return true;
}
function clearDraft() {
const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});
if (!allCData[commentData.key]) {return false;};
allCData[commentData.key] = undefined;
GM_setValue(KEY_COMMENT_DRAFTS, allCData);
asTip.innerHTML = TEXT_GUI_AUTOSAVE_CLEAR;
return true;
}
function hotkeyReply() {
let keycode = event.keyCode;
if (keycode === 13 && event.ctrlKey && !event.altKey) {
// Do not submit directly like this; we need to submit with onsubmit executed
//commentForm.submit();
commentSbmt.click();
}
}
function submitHook() {
const onsubmit = commentForm.onsubmit;
commentForm.onsubmit = onsubmitForm;
function onsubmitForm(e) {
clearDraft();
return onsubmit ? onsubmit() : function() {return true;};
}
}
}
// Review page add-on
function pageReview() {
// ## 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);
addQuoteBtns();
function addQuoteBtns() {
// Get content textarea
const pcontent = document.querySelector('#pcontent');
const form = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]');
// Get floor elements
const avatars = main.querySelectorAll('table div img.avatar');
for (const avatar of avatars) {
// do not insert the button as the first childnode. page saving function uses the first childnode as the time element.
const table = avatar.parentElement.parentElement.parentElement.parentElement.parentElement;
const numberEle = table.querySelector('td.even div a');
const attr = numberEle.parentElement;
const btn = createQuoteBtn(attr);
const spliter = document.createTextNode(' | ');
attr.insertBefore(spliter, numberEle);
attr.insertBefore(btn, spliter);
}
function createQuoteBtn() {
const btn = document.createElement('span');
btn.classList.add(CLASSNAME_BUTTON);
btn.addEventListener('click', quoteThisFloor);
btn.innerHTML = '引用';
return btn;
function quoteThisFloor() {
// In DOM Events, <this> keyword points to the Event Element.
const numberEle = this.parentElement.querySelector('a[name]');
const numberText = numberEle.innerText;
const url = numberEle.href;
const contentEle = this.parentElement.parentElement.querySelector('hr+div');
const content = getFloorContent(contentEle);
const insertPosition = pcontent.selectionEnd;
const text = pcontent.value;
const leftText = text.substr(0, insertPosition);
const rightText = text.substr(insertPosition);
/* ## Create insert value ## */
let insertValue = '[url=U]N[/url] [quote]Q[/quote]';
insertValue = insertValue.replace('U', url).replace('N', numberText).replace('Q', content);
// if not at the beginning of a line then insert a whitespace before the link
insertValue = ((leftText.length === 0 || /[\r\n]$/.test(leftText)) ? '' : ' ') + insertValue;
// if not at the end of a line then insert a whitespace after the link
insertValue += (rightText.length === 0 || /^[\r\n]/.test(leftText)) ? '' : ' ';
pcontent.value = leftText + insertValue + rightText;
const position = insertPosition + (pcontent.value.length - text.length);
form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
}
function getFloorContent(contentEle) {
const subNodes = contentEle.childNodes;
let content = '', subContent = '', size = '', color = '';
for (const node of subNodes) {
const type = node.nodeName;
switch (type) {
case '#text':
content += node.data;
break;
case 'IMG':
content += '[img]S[/img]'.replace('S', node.src);
break;
case 'A':
content += '[url=U]T[/url]'.replace('U', node.href).replace('T', node.innerText);
break;
case 'BR':
// no need to add \n, because \n will be preserved in #text nodes
//content += '\n';
break;
case 'DIV':
subContent = getFloorContent(node);
if (node.classList.contains('jieqiQuote')) {
subContent = '[quote]C[/quote]'.replace('C', subContent);
} else if (node.classList.contains('jieqiCode')) {
subContent = '[code]C[/code]'.replace('C', subContent);
}
content += subContent;
break;
case 'SPAN':
case 'B':
case 'I':
case 'DEL':
case 'CODE':
case 'PRE':
subContent = getFloorContent(node);
content += subContent;
break;
/*
case 'SPAN':
subContent = getFloorContent(node);
size = node.style.fontSize.match(/\d+/) ? node.style.fontSize.match(/\d+/)[0] : '';
color = node.style.color.match(/rgb\((\d+), ?(\d+), ?(\d+)\)/);
break;
*/
}
}
return content;
}
}
}
/*
// 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;
DoLog(LogLevel.Info, document, true);
// 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 = getUrlArgv('rid', Number, 0);
data.page = getUrlArgv('page', Number, 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.querySelector('a[name]');
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));
getDocument(url, joinPageData, callback);
}
function joinPageData(pageDocument, callback) {
data = getDataFromPage(pageDocument, data);
gotcount++;
// log
const level = gotcount % NUMBER_LOGSUCCESS_AFTER ? LogLevel.Info : LogLevel.Success;
DoLog(level, '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',
onloadstart : function() {
DoLog(LogLevel.Info, 'Downloading ' + name + ' from ' + url);
},
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 <a>
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,'')
.replace(REG_GUI_WAITING, '');
}
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,'')
.replace(REG_GUI_WAITING, '');
displayElement.innerText = text + TEXT_GUI_WAITING;
}
}
// xmlHTTPRequest
GM_xmlhttpRequest({
method :'GET',
url :url,
responseType : 'blob',
onloadstart : function() {
DoLog(LogLevel.Info, 'Downloading ' + name + ' from ' + url);
// Display
if (displayElement) {
displayElement.innerText = text + TEXT_GUI_DOWNLOADING;
}
},
onload: function(request) {
// DataURL
let objURL = URL.createObjectURL(request.response);
// Create <a>
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;
}
}
// GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
// Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
// (If the request is invalid, such as url === '', will return false and will NOT make this request)
// If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
// Requires: function delItem(){...} & function uniqueIDMaker(){...}
function GMXHRHook(maxXHR=5) {
const GM_XHR = GM_xmlhttpRequest;
const getID = uniqueIDMaker();
let todoList = [], ongoingList = [];
GM_xmlhttpRequest = safeGMxhr;
function safeGMxhr() {
// Get an id for this request, arrange a request object for it.
const id = getID();
const request = {id: id, args: arguments, aborter: null};
// Deal onload function first
dealEndingEvents(request);
/* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
// Stop invalid requests
if (!validCheck(request)) {
return false;
}
*/
// Judge if we could start the request now or later?
todoList.push(request);
checkXHR();
return makeAbortFunc(id);
// Decrease activeXHRCount while GM_XHR onload;
function dealEndingEvents(request) {
const e = request.args[0];
// onload event
const oriOnload = e.onload;
e.onload = function() {
reqFinish(request.id);
checkXHR();
oriOnload ? oriOnload.apply(null, arguments) : function() {};
}
// onerror event
const oriOnerror = e.onerror;
e.onerror = function() {
reqFinish(request.id);
checkXHR();
oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
}
// ontimeout event
const oriOntimeout = e.ontimeout;
e.ontimeout = function() {
reqFinish(request.id);
checkXHR();
oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
}
// onabort event
const oriOnabort = e.onabort;
e.onabort = function() {
reqFinish(request.id);
checkXHR();
oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
}
}
// Check if the request is invalid
function validCheck(request) {
const e = request.args[0];
if (!e.url) {
return false;
}
return true;
}
// Call a XHR from todoList and push the request object to ongoingList if called
function checkXHR() {
if (ongoingList.length >= maxXHR) {return false;};
if (todoList.length === 0) {return false;};
const req = todoList.shift();
const reqArgs = req.args;
const aborter = GM_XHR.apply(null, reqArgs);
req.aborter = aborter;
ongoingList.push(req);
return req;
}
// Make a function that aborts a certain request
function makeAbortFunc(id) {
return function() {
let i;
// Check if the request haven't been called
for (i = 0; i < todoList.length; i++) {
const req = todoList[i];
if (req.id === id) {
// found this request: haven't been called
delItem(todoList, i);
return true;
}
}
// Check if the request is running now
for (i = 0; i < ongoingList.length; i++) {
const req = todoList[i];
if (req.id === id) {
// found this request: running now
req.aborter();
reqFinish(id);
checkXHR();
}
}
// Oh no, this request is already finished...
return false;
}
}
// Remove a certain request from ongoingList
function reqFinish(id) {
let i;
for (i = 0; i < ongoingList.length; i++) {
const req = ongoingList[i];
if (req.id === id) {
ongoingList = delItem(ongoingList, i);
return true;
}
}
return false;
}
}
}
// 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',
onloadstart : function() {
DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
},
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 url argument from lacation.href
// also recieve a function to deal the matched string
// returns defultValue if name not found
function getUrlArgv(name, dealFunc=(function(a) {return a;}), defultValue=null) {
const url = location.href;
const matcher = new RegExp(name + '=([^&]+)');
const result = url.match(matcher);
const argv = result ? dealFunc(result[1]) : defultValue;
return argv;
}
// 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 !== '';
}
// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
function delItem(arr, delIndex) {
arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
return arr;
}
// Makes a function that returns a unique ID number each time
function uniqueIDMaker() {
let id = 0;
return makeID;
function makeID() {
id++;
return id;
}
}
// Append a style text to document(<head>) with a <style> element
function addStyle(css) {
document.head.appendChild(document.createElement("style")).textContent = css;
}
// Copy text to clipboard (needs to be called in an user event)
function copyText(text) {
// Create a new textarea for copying
const newInput = document.createElement('textarea');
document.body.appendChild(newInput);
newInput.value = text;
newInput.select();
document.execCommand('copy');
document.body.removeChild(newInput);
}
})();