东方小说
https://www.cndfzw.com
ethereal-essence (13554) 16小时前 下载:309
小说 东方小说
东方小说阅读网(cndfzw.com),小说发现、目录与正文解析。
// @uuid 019e120f-9e35-7f1c-a5bd-b820db5a3097
// @name 东方小说
// @version 1.0.5
// @author Ethereal
// @url https://www.cndfzw.com
// @logo https://www.cndfzw.com/favicon.ico
// @enabled true
// @description 东方小说阅读网(cndfzw.com),小说发现、目录与正文解析。
// @type novel
// @tags 东方小说
var BASE = 'https://www.cndfzw.com';
var EXPLORE_CAT = {
玄幻小说: '1',
修真小说: '2',
都市小说: '3',
穿越小说: '4',
网游小说: '5',
言情小说: '6',
科幻小说: '7'
};
var CHAPTER_MAX_PAGES = 40;
/*
搜索:http 链 GET 建会话 → GET 带 q 取 sign → POST /api/search;失败或「太频繁」再走 fetch 取 SID + POST。
common.js:有 Cookie 时 bgt 去尾一位;无 sign 不调 API。接口常空时用备用:/top/*、/quanben/、class/all 与分类。
支持粘贴本站 /novel/分类/书号/ 或 /novel/书号.html 直达。
速度与限流:短间隔 + 全局 POST 间隔;仍遇「太频繁」可调大 SEARCH_MIN_GAP_BETWEEN_POST_MS。
全本/书库列表会轮换,少数书名在 SEARCH_EXACT_TITLE_DIR 中写死正文书路径,避免完全依赖分页扫到。
备用扫描:榜单一次并发;书库/全本/分类列表按 SEARCH_FALLBACK_CONCURRENCY 分批 Promise.all 并发 GET。
*/
/** GET 会话页 / token 页后的短间隔(毫秒),兼顾 Cookie 写入与站点限流。 */
var SEARCH_DELAY_AFTER_GET_MS = 320;
var SEARCH_FALLBACK_MAX_PAGES = 20;
/** 全本列表 /quanben/ 单独页上限(与 class/all 分页节奏相同)。 */
var SEARCH_FALLBACK_QUANBEN_MAX_PAGES = 36;
var SEARCH_FALLBACK_MAX_RESULTS = 30;
var SEARCH_FALLBACK_REQUEST_BUDGET = 96;
/** 备用列表单批并发 GET 数(过大易触发站点限流)。 */
var SEARCH_FALLBACK_CONCURRENCY = 8;
/** true:API 无命中后再扫 class/1~7(更慢,覆盖面更大)。 */
var SEARCH_FALLBACK_SCAN_CATEGORIES = true;
/** 榜单/排行(表格 tr),热门书常在此而不在书库前几页。 */
var SEARCH_FALLBACK_TOP_TABLE_PATHS = [
'/top/lastupdate/',
'/top/monthvisit/',
'/top/1.html'
];
var SEARCH_RETRY_GAP_MS = 5500;
var SEARCH_MAX_ATTEMPTS = 2;
var SEARCH_MIN_GAP_BETWEEN_POST_MS = 2200;
var SEARCH_CONFIG_NS = 'cndfzw';
var SEARCH_CONFIG_LAST_POST = 'lastSearchPostMs';
var SEARCH_BROWSER_UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
/** 精确书名 → 正文目录 URL(须带分类段 /novel/cid/bid/)。列表里未必再出现该书时仍可搜索。 */
var SEARCH_EXACT_TITLE_DIR = {
'\u6597\u7834\u82cd\u7a79': '/novel/8/8884/'
};
var SEARCH_KEYS = [
'vw',
'abw',
'ru',
'jrt',
'van',
'fw',
'cwl',
'gpr',
'uyoo',
'tz',
'euu',
'tsn',
'eju',
'um',
'fp',
'dvm',
'jpk',
'deblkx',
'ht',
'azy',
'sna',
'wqx',
'fpp',
'rup',
'jwj',
'bgt',
'qp',
'yf',
'cw',
'wq',
'sign'
];
function absUrl(href) {
if (!href) {
return '';
}
if (href.indexOf('http://') === 0 || href.indexOf('https://') === 0) {
return href;
}
if (href.indexOf('//') === 0) {
return 'https:' + href;
}
if (href.charAt(0) === '/') {
return BASE + href;
}
return BASE + '/' + href;
}
function sleepMs(ms) {
return new Promise(function (resolve) {
if (typeof setTimeout === 'function') {
setTimeout(resolve, ms);
} else {
resolve();
}
});
}
function searchReadLastPostMs() {
try {
var s = legado.config.read(SEARCH_CONFIG_NS, SEARCH_CONFIG_LAST_POST);
if (!s) {
return 0;
}
var n = parseInt(s, 10);
return isNaN(n) ? 0 : n;
} catch (e0) {
return 0;
}
}
function searchWriteLastPostMs() {
try {
legado.config.write(SEARCH_CONFIG_NS, SEARCH_CONFIG_LAST_POST, String(Date.now()));
} catch (e1) {}
}
async function searchWaitGlobalPostGap() {
var last = searchReadLastPostMs();
if (!last) {
return;
}
var need = SEARCH_MIN_GAP_BETWEEN_POST_MS - (Date.now() - last);
if (need > 0 && need < 28000) {
legado.log('search global gap wait ' + need + 'ms');
await sleepMs(need);
}
}
function extractSidCookieFromFetchResponse(res) {
try {
var raw = '';
if (res && res.headers && typeof res.headers.get === 'function') {
raw = res.headers.get('set-cookie') || res.headers.get('Set-Cookie') || '';
}
if (!raw && res && res.headers && typeof res.headers.forEach === 'function') {
res.headers.forEach(function (v, k) {
if (String(k).toLowerCase() === 'set-cookie') {
raw = raw ? raw + '; ' + v : v;
}
});
}
var m = String(raw).match(/SID=([^;\s,]+)/);
return m ? 'SID=' + m[1] : '';
} catch (e3) {
return '';
}
}
function parseInlineAnti(html) {
var out = {};
var re = /var\s+(\w+)\s*=\s*"([^"]*)"/g;
var m;
while (true) {
m = re.exec(html);
if (!m) {
break;
}
out[m[1]] = m[2];
}
return out;
}
function metaContent(doc, prop) {
var el = legado.dom.select(doc, 'meta[property="' + prop + '"]');
if (!el) {
return '';
}
return legado.dom.attr(el, 'content') || '';
}
function isChapterHref(href) {
return /^\/novel\/\d+\/\d+\/\d+\.html$/.test(href);
}
function chapterIdFromUrl(url) {
var m = String(url).match(/\/(\d+)\.html(?:\?|$|#)/);
if (!m) {
return 0;
}
return parseInt(m[1], 10) || 0;
}
function chapterUrlBase(url) {
var s = String(url).split('#')[0];
var q = s.indexOf('?');
return q >= 0 ? s.substring(0, q) : s;
}
function extractContentParagraphs(doc) {
var ps = legado.dom.selectAll(doc, '#content p');
var lines = [];
var i;
if (ps.length > 0) {
for (i = 0; i < ps.length; i++) {
var line = legado.dom.text(ps[i]).trim();
if (!line) {
continue;
}
if (line.indexOf('东方小说网欢迎您') >= 0) {
continue;
}
if (line.indexOf('强烈推荐:') === 0 || line.indexOf('强烈推荐:') === 0) {
continue;
}
lines.push(line);
}
return lines.join('\n\n');
}
var box = legado.dom.select(doc, '#content');
if (!box) {
return '';
}
legado.dom.remove(box, 'script');
return legado.dom.text(box).trim();
}
function findNextPageHref(doc, baseUrl) {
var bid = chapterIdFromUrl(baseUrl);
if (!bid) {
return '';
}
var links = legado.dom.selectAll(doc, 'a');
for (var j = 0; j < links.length; j++) {
var label = legado.dom.text(links[j]).replace(/\s+/g, '');
if (label.indexOf('下一页') < 0 && label !== '下页') {
continue;
}
var href = legado.dom.attr(links[j], 'href') || '';
if (!href || href.indexOf('javascript:') === 0) {
continue;
}
var abs = absUrl(href);
if (chapterIdFromUrl(abs) !== bid) {
continue;
}
return abs;
}
return '';
}
function stripHtmlTags(s) {
return String(s).replace(/<[^>]+>/g, '').trim();
}
/** 分类/书库列表 dd 内「作者:…」与「更新时间:…」常无换行,作者必须在「更新\s*时间」或「作品分类」前截断。 */
function extractAuthorFromListPText(pText) {
if (!pText) {
return '';
}
var s = String(pText).replace(/\u00a0/g, ' ').trim();
var am = s.match(/作者[::]\s*/);
if (!am || am.index == null) {
return '';
}
var start = am.index + am[0].length;
var rest = s.substring(start);
var endRel = rest.search(/更新\s*时间|作品分类|作品状态/);
var seg = endRel >= 0 ? rest.substring(0, endRel) : rest;
return seg
.replace(/\s+/g, ' ')
.replace(/^[|·\s]+|[|·\s]+$/g, '')
.trim();
}
function pickStr(row, keys) {
for (var i = 0; i < keys.length; i++) {
var v = row[keys[i]];
if (v != null && String(v).trim() !== '') {
return String(v).trim();
}
}
return '';
}
function coverFromNovelListUrl(anyUrl) {
if (!anyUrl) {
return '';
}
var s = String(anyUrl);
var m = s.match(/\/novel\/(\d+)\/(\d+)(?:\/|\.html|\?|#|$)/);
if (!m) {
return '';
}
return BASE + '/img/' + m[1] + '/' + m[2] + '.jpg';
}
/** 粘贴 https://www.cndfzw.com/novel/8/8884/ 或 /novel/8884.html 等本站阅读/介绍地址。 */
function parseCndfzwNovelUserInput(raw) {
var t = String(raw || '').trim();
if (!t) {
return null;
}
t = t.split('#')[0].split('?')[0].trim();
var m = t.match(/^(?:https?:\/\/)?(?:www\.|m\.)?cndfzw\.com(\/novel\/\d+\/\d+)\/?$/i);
if (m) {
var path = m[1];
if (path.charAt(path.length - 1) !== '/') {
path += '/';
}
return { kind: 'index', fetchUrl: BASE + path, bookUrl: BASE + path };
}
m = t.match(/^(?:https?:\/\/)?(?:www\.|m\.)?cndfzw\.com(\/novel\/\d+\.html)$/i);
if (m) {
return { kind: 'intro', fetchUrl: BASE + m[1], bookUrl: '' };
}
if (/^\/novel\/\d+\/\d+\/?$/i.test(t)) {
var norm = t.replace(/\/?$/, '/');
return { kind: 'index', fetchUrl: BASE + norm, bookUrl: BASE + norm };
}
if (/^\/novel\/\d+\.html$/i.test(t)) {
return { kind: 'intro', fetchUrl: BASE + t, bookUrl: '' };
}
return null;
}
function extractReadDirFromIntroDoc(doc) {
var links = legado.dom.selectAll(doc, 'a[href]');
var i;
for (i = 0; i < links.length; i++) {
var href = legado.dom.attr(links[i], 'href') || '';
if (/^\/novel\/\d+\/\d+\/?$/.test(href)) {
return absUrl(href.replace(/\/?$/, '') + '/');
}
}
return '';
}
function parseBookTitleAuthorFromNovelIndexDoc(doc) {
var name = '';
var h1 = legado.dom.select(doc, '#bookinfo h1');
if (!h1) {
h1 = legado.dom.select(doc, '#title h1');
}
if (h1) {
name = legado.dom.text(h1).trim();
}
if (!name) {
var h2a = legado.dom.select(doc, '#title h2 a');
if (h2a) {
name = legado.dom.text(h2a).trim();
}
}
var author = '';
var authEl = legado.dom.select(doc, '#bookinfo address.author');
if (!authEl) {
authEl = legado.dom.select(doc, 'address.author');
}
if (authEl) {
var at = legado.dom.text(authEl).trim();
var am = at.match(/作者[::\s]*([^\s\r\n]+)/);
author = am ? am[1].trim() : at.replace(/^作者[::]\s*/, '').trim();
}
return { name: name, author: author };
}
function latestChapterFromNovelIndexDoc(doc) {
var dds = legado.dom.selectAll(doc, 'dl.book dd');
var di;
for (di = 0; di < dds.length; di++) {
if (legado.dom.text(dds[di]).indexOf('最新章节') >= 0) {
var la = legado.dom.select(dds[di], 'a[href*=".html"]');
if (la) {
return legado.dom.text(la).trim();
}
}
}
return '';
}
function searchExactTitleBookUrl(keyword) {
var k = String(keyword || '').trim();
if (!k || !Object.prototype.hasOwnProperty.call(SEARCH_EXACT_TITLE_DIR, k)) {
return '';
}
var path = SEARCH_EXACT_TITLE_DIR[k];
if (!path || path.indexOf('/novel/') !== 0) {
return '';
}
return BASE + path.replace(/\/?$/, '/');
}
async function searchFromPastedNovelUrl(keyword) {
var p = parseCndfzwNovelUserInput(keyword);
if (!p) {
return [];
}
legado.log('search pasted novel url');
var hdr = {
Referer: BASE + '/',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': SEARCH_BROWSER_UA
};
try {
var bookUrl = '';
var doc = null;
if (p.kind === 'index') {
bookUrl = p.bookUrl.replace(/\/?$/, '/');
var htmlIdx = await legado.http.get(bookUrl, hdr);
doc = legado.dom.parse(htmlIdx);
} else {
var htmlIntro = await legado.http.get(p.fetchUrl, hdr);
var docIntro = legado.dom.parse(htmlIntro);
bookUrl = extractReadDirFromIntroDoc(docIntro);
legado.dom.free(docIntro);
if (!bookUrl) {
return [];
}
bookUrl = bookUrl.replace(/\/?$/, '/');
if (bookUrl.indexOf('http') !== 0) {
bookUrl = absUrl(bookUrl);
}
var htmlRead = await legado.http.get(bookUrl, hdr);
doc = legado.dom.parse(htmlRead);
}
var meta = parseBookTitleAuthorFromNovelIndexDoc(doc);
var lastChapter = latestChapterFromNovelIndexDoc(doc);
legado.dom.free(doc);
var name = meta.name || '(解析书名失败)';
return [
{
name: name,
author: meta.author || '',
bookUrl: bookUrl,
coverUrl: coverFromNovelListUrl(bookUrl),
kind: '本站直达',
lastChapter: lastChapter,
latestChapter: lastChapter
}
];
} catch (ePaste) {
legado.log('search pasted url fail');
return [];
}
}
async function explore(page, category) {
legado.log('explore ' + category + ' p=' + page);
if (category === 'GETALL') {
return [
'玄幻小说',
'修真小说',
'都市小说',
'穿越小说',
'网游小说',
'言情小说',
'科幻小说'
];
}
var cid = EXPLORE_CAT[category];
if (!cid) {
return [];
}
var url = BASE + '/class/' + cid + '/';
if (page > 1) {
url += '?p=' + page;
}
var html = await legado.http.get(url);
var doc = legado.dom.parse(html);
var items = [];
var blocks = legado.dom.selectAll(doc, 'dl.eachitem');
for (var i = 0; i < blocks.length; i++) {
var cap = legado.dom.select(blocks[i], 'a.caption');
if (!cap) {
continue;
}
var name = legado.dom.text(cap).trim();
var bookUrl = absUrl(legado.dom.attr(cap, 'href'));
var img = legado.dom.select(blocks[i], 'dd.img img');
var coverUrl = img ? absUrl(legado.dom.attr(img, 'src')) : '';
var pEl = legado.dom.select(blocks[i], 'dd.text p');
if (!pEl) {
pEl = legado.dom.select(blocks[i], 'dd.text');
}
var pText = pEl ? legado.dom.text(pEl) : '';
var author = extractAuthorFromListPText(pText);
var lastChapter = '';
var kind = category;
var lc = legado.dom.select(blocks[i], 'dd.text p a[style*="5EB4D8"]');
if (lc) {
lastChapter = legado.dom.text(lc).trim();
}
items.push({
name: name,
author: author,
bookUrl: bookUrl,
coverUrl: coverUrl,
kind: kind,
lastChapter: lastChapter,
latestChapter: lastChapter
});
}
return items;
}
async function bookInfo(bookUrl) {
legado.log('bookInfo ' + bookUrl);
var html = await legado.http.get(bookUrl);
var doc = legado.dom.parse(html);
var name = metaContent(doc, 'og:novel:book_name');
var author = metaContent(doc, 'og:novel:author');
var coverUrl = metaContent(doc, 'og:image');
var intro = metaContent(doc, 'og:description');
var kind = metaContent(doc, 'og:novel:category');
var lastChapter = metaContent(doc, 'og:novel:latest_chapter_name');
if (!name) {
var h1 = legado.dom.select(doc, '#bookinfo h1');
name = h1 ? legado.dom.text(h1).trim() : '';
}
return {
name: name,
author: author,
bookUrl: bookUrl,
coverUrl: absUrl(coverUrl),
intro: intro,
kind: kind,
lastChapter: lastChapter,
latestChapter: lastChapter,
tocUrl: bookUrl
};
}
async function chapterList(tocUrl) {
legado.log('chapterList ' + tocUrl);
var html = await legado.http.get(tocUrl);
var doc = legado.dom.parse(html);
var links = legado.dom.selectAll(doc, 'dl.book dd a');
var chapters = [];
var seen = {};
for (var i = 0; i < links.length; i++) {
var href = legado.dom.attr(links[i], 'href') || '';
if (!isChapterHref(href)) {
continue;
}
var url = absUrl(href);
if (seen[url]) {
continue;
}
seen[url] = 1;
chapters.push({
name: legado.dom.text(links[i]).trim(),
url: url
});
}
chapters.sort(function (a, b) {
return chapterIdFromUrl(a.url) - chapterIdFromUrl(b.url);
});
return chapters;
}
async function chapterContent(chapterUrl) {
legado.log('chapterContent ' + chapterUrl);
var base = chapterUrlBase(chapterUrl);
var chunks = [];
var seenNorm = {};
var seenFetch = {};
function mergeChunk(text) {
var n = text.replace(/\s+/g, '');
if (!n) {
return false;
}
if (seenNorm[n]) {
return false;
}
seenNorm[n] = 1;
chunks.push(text);
return true;
}
seenFetch[base] = 1;
var html0 = await legado.http.get(base);
var doc0 = legado.dom.parse(html0);
legado.dom.remove(doc0, '#content script');
var t0 = extractContentParagraphs(doc0);
legado.dom.free(doc0);
if (!mergeChunk(t0)) {
return '';
}
var page;
for (page = 2; page <= CHAPTER_MAX_PAGES; page++) {
var pu = base + '?page=' + page;
if (seenFetch[pu]) {
break;
}
seenFetch[pu] = 1;
var htmlP = await legado.http.get(pu);
var docP = legado.dom.parse(htmlP);
legado.dom.remove(docP, '#content script');
var tP = extractContentParagraphs(docP);
legado.dom.free(docP);
if (!mergeChunk(tP)) {
break;
}
}
var docL = legado.dom.parse(html0);
var walk = findNextPageHref(docL, base);
legado.dom.free(docL);
var guard = 0;
while (walk && guard < CHAPTER_MAX_PAGES) {
guard++;
if (seenFetch[walk]) {
break;
}
seenFetch[walk] = 1;
var htmlW = await legado.http.get(walk);
var docW = legado.dom.parse(htmlW);
legado.dom.remove(docW, '#content script');
var tW = extractContentParagraphs(docW);
legado.dom.free(docW);
if (!mergeChunk(tW)) {
break;
}
var docW2 = legado.dom.parse(htmlW);
walk = findNextPageHref(docW2, base);
legado.dom.free(docW2);
}
return chunks.join('\n\n').trim();
}
function coerceSearchResultArray(obj) {
var out = [];
var k;
for (k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
out.push(obj[k]);
}
}
return out;
}
/** 兼容 data.search 为 []、对象映射或其它字段名。code!==0 时返回 null。 */
function searchApiExtractRows(json) {
if (!json || json.code !== 0) {
return null;
}
var d = json.data;
if (d == null) {
return [];
}
if (Array.isArray(d)) {
return d;
}
if (typeof d !== 'object') {
return [];
}
var keys = ['search', 'list', 'books', 'rows', 'items'];
var ki;
for (ki = 0; ki < keys.length; ki++) {
var key = keys[ki];
if (!Object.prototype.hasOwnProperty.call(d, key)) {
continue;
}
var raw = d[key];
if (raw == null) {
continue;
}
if (Array.isArray(raw)) {
return raw;
}
if (typeof raw === 'object') {
return coerceSearchResultArray(raw);
}
}
return [];
}
function buildSearchFormBody(keyword, vars) {
var parts = [];
parts.push('q=' + encodeURIComponent(keyword));
for (var i = 0; i < SEARCH_KEYS.length; i++) {
var k = SEARCH_KEYS[i];
var v = vars[k];
if (v === undefined || v === null) {
v = '';
}
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
}
return parts.join('&');
}
/** 与 common.js search_before 一致:document.cookie 非空时去掉 bgt 最后一位(有 SID 等 Cookie 时与浏览器一致)。 */
function applySearchBeforeLikeBrowser(vars, hasSidCookie) {
var o = {};
var key;
for (key in vars) {
if (Object.prototype.hasOwnProperty.call(vars, key)) {
o[key] = vars[key];
}
}
if (hasSidCookie && o.bgt && String(o.bgt).length > 0) {
var b = String(o.bgt);
o.bgt = b.substring(0, b.length - 1);
}
return o;
}
function mapSearchApiRow(row) {
var rawBook = pickStr(row, [
'book_list_url',
'bookListUrl',
'book_url',
'bookUrl',
'list_url',
'listUrl',
'url'
]);
var bookUrl = absUrl(rawBook);
var name = stripHtmlTags(pickStr(row, ['title', 'book_name', 'bookName', 'name']));
var rawCover = pickStr(row, [
'cover',
'cover_url',
'coverUrl',
'img',
'imgurl',
'img_url',
'imgUrl',
'book_img',
'bookImg',
'pic',
'thumb',
'image',
'poster'
]);
var coverGuess =
coverFromNovelListUrl(bookUrl) || coverFromNovelListUrl(rawBook);
var coverUrl = (rawCover ? absUrl(rawCover) : '') || coverGuess;
var latest = pickStr(row, [
'latest_chapter_name',
'latestChapterName',
'latest_chapter',
'latestChapter',
'lastchapter',
'lastChapter'
]);
var author = stripHtmlTags(
pickStr(row, [
'author',
'author_name',
'authorName',
'writer',
'pen_name',
'penName',
'book_author',
'bookAuthor',
'auth',
'zuozhe'
])
);
if (!author && row && typeof row === 'object') {
var ua = row.user_author || row.UserAuthor;
if (ua != null && String(ua).trim()) {
author = stripHtmlTags(String(ua));
}
}
return {
name: name,
author: author,
bookUrl: bookUrl,
coverUrl: coverUrl,
kind: row.cate_name || row.cateName || row.kind || '',
lastChapter: latest,
latestChapter: latest
};
}
/** 从长到短尝试前缀,便于「斗破苍穹」无全书时再匹配「斗破」等同站衍生书。 */
function keywordFallbackVariants(raw) {
var s = String(raw || '').trim();
var list = [];
var seen = {};
var t = s;
while (t.length >= 2) {
if (!seen[t]) {
seen[t] = 1;
list.push(t);
}
if (t.length === 2) {
break;
}
t = t.substring(0, t.length - 1);
}
return list;
}
function blockMatchesKeyword(block, kw) {
var cap = legado.dom.select(block, 'a.caption');
if (!cap) {
return null;
}
var name = legado.dom.text(cap).trim();
var pEl = legado.dom.select(block, 'dd.text p');
if (!pEl) {
pEl = legado.dom.select(block, 'dd.text');
}
var pText = pEl ? legado.dom.text(pEl) : '';
var author = extractAuthorFromListPText(pText);
if (name.indexOf(kw) >= 0) {
return { name: name, author: author, cap: cap, pText: pText };
}
if (author && author.indexOf(kw) >= 0) {
return { name: name, author: author, cap: cap, pText: pText };
}
return null;
}
function pushFallbackBlock(out, seen, block, hit) {
var bookUrl = absUrl(legado.dom.attr(hit.cap, 'href'));
if (!bookUrl || seen[bookUrl]) {
return;
}
seen[bookUrl] = 1;
var img = legado.dom.select(block, 'dd.img img');
var coverUrl = img ? absUrl(legado.dom.attr(img, 'src')) : '';
var lc = legado.dom.select(block, 'dd.text p a[style*="5EB4D8"]');
var lastChapter = lc ? legado.dom.text(lc).trim() : '';
var guessCover = coverUrl || coverFromNovelListUrl(bookUrl);
out.push({
name: hit.name,
author: hit.author,
bookUrl: bookUrl,
coverUrl: guessCover,
kind: '在线书库',
lastChapter: lastChapter,
latestChapter: lastChapter
});
}
/** variants 已按从长到短;每条书目取最先命中的更长关键词,同一页只请求一次。 */
function blockMatchLongestVariant(block, variants) {
var vi;
for (vi = 0; vi < variants.length; vi++) {
var hit = blockMatchesKeyword(block, variants[vi]);
if (hit) {
return hit;
}
}
return null;
}
function isNovelBookIndexHref(href) {
return /^\/novel\/\d+\/\d+\/?$/.test(String(href || ''));
}
function topTableRowAuthorGuess(tr) {
var tds = legado.dom.selectAll(tr, 'td');
if (!tds.length) {
return '';
}
var ai;
var bookIdx = -1;
var lj;
for (ai = 0; ai < tds.length; ai++) {
var links = legado.dom.selectAll(tds[ai], 'a[href]');
for (lj = 0; lj < links.length; lj++) {
var href = legado.dom.attr(links[lj], 'href') || '';
if (isNovelBookIndexHref(href)) {
bookIdx = ai;
break;
}
}
if (bookIdx >= 0) {
break;
}
}
if (bookIdx >= 0) {
for (ai = bookIdx + 1; ai < tds.length; ai++) {
var subas = legado.dom.selectAll(tds[ai], 'a');
var tx = legado.dom.text(tds[ai]).trim();
if (!tx || tx.length > 24) {
continue;
}
if (/^\[[^\]]+\]$/.test(tx)) {
continue;
}
if (/^\d+$/.test(tx)) {
continue;
}
if (/^\d{1,2}-\d{1,2}$/.test(tx)) {
continue;
}
if (tx === '完结' || tx === '连载') {
continue;
}
if (subas.length === 0) {
return tx;
}
if (subas.length === 1 && tx.length <= 20) {
return tx;
}
}
}
for (ai = tds.length - 1; ai >= 0; ai--) {
var links2 = legado.dom.selectAll(tds[ai], 'a');
if (links2.length) {
continue;
}
var tx2 = legado.dom.text(tds[ai]).trim();
if (!tx2 || tx2.length > 22) {
continue;
}
if (/^\[[^\]]+\]$/.test(tx2)) {
continue;
}
if (/^\d+$/.test(tx2)) {
continue;
}
if (/^\d{1,2}-\d{1,2}$/.test(tx2)) {
continue;
}
if (tx2 === '完结' || tx2 === '连载') {
continue;
}
return tx2;
}
return '';
}
function tableRowPickBookAnchor(tr) {
var as = legado.dom.selectAll(tr, 'a[href]');
var i;
for (i = 0; i < as.length; i++) {
var href = legado.dom.attr(as[i], 'href') || '';
if (!isNovelBookIndexHref(href)) {
continue;
}
var name = legado.dom.text(as[i]).trim();
if (!name) {
continue;
}
return { el: as[i], href: href, name: name };
}
return null;
}
function tableRowMatchesKeyword(tr, kw) {
var pick = tableRowPickBookAnchor(tr);
if (!pick) {
return null;
}
var author = topTableRowAuthorGuess(tr);
if (pick.name.indexOf(kw) >= 0) {
return { name: pick.name, author: author, cap: pick.el, pText: '', tr: tr };
}
if (author && author.indexOf(kw) >= 0) {
return { name: pick.name, author: author, cap: pick.el, pText: '', tr: tr };
}
return null;
}
function tableRowMatchLongestVariant(tr, variants) {
var vi;
for (vi = 0; vi < variants.length; vi++) {
var hit = tableRowMatchesKeyword(tr, variants[vi]);
if (hit) {
return hit;
}
}
return null;
}
function tableRowChapterTitle(tr) {
if (!tr) {
return '';
}
var candidates = legado.dom.selectAll(tr, 'a[href]');
var ci;
for (ci = 0; ci < candidates.length; ci++) {
var href = legado.dom.attr(candidates[ci], 'href') || '';
if (/^\/novel\/\d+\/\d+\/\d+\.html$/.test(href)) {
return legado.dom.text(candidates[ci]).trim();
}
}
return '';
}
function pushFallbackTableRow(out, seen, hit) {
var bookUrl = absUrl(legado.dom.attr(hit.cap, 'href'));
if (!bookUrl || seen[bookUrl]) {
return;
}
seen[bookUrl] = 1;
var guessCover = coverFromNovelListUrl(bookUrl);
var lastChapter = tableRowChapterTitle(hit.tr);
out.push({
name: hit.name,
author: hit.author || '',
bookUrl: bookUrl,
coverUrl: guessCover,
kind: '榜单/书库',
lastChapter: lastChapter,
latestChapter: lastChapter
});
}
function searchFallbackConsumeTopTableHtml(html, variants, seen, out) {
if (!html) {
return;
}
var doc = legado.dom.parse(html);
var rows = legado.dom.selectAll(doc, 'tr');
var ri;
for (ri = 0; ri < rows.length; ri++) {
var hit = tableRowMatchLongestVariant(rows[ri], variants);
if (!hit) {
continue;
}
pushFallbackTableRow(out, seen, hit);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
}
legado.dom.free(doc);
}
function searchFallbackConsumeEachitemHtml(html, variants, seen, out) {
if (!html) {
return;
}
var doc = legado.dom.parse(html);
var blocks = legado.dom.selectAll(doc, 'dl.eachitem');
var bi;
for (bi = 0; bi < blocks.length; bi++) {
var hit = blockMatchLongestVariant(blocks[bi], variants);
if (!hit) {
continue;
}
pushFallbackBlock(out, seen, blocks[bi], hit);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
}
legado.dom.free(doc);
}
function searchHttpGetSafe(url) {
return legado.http.get(url).catch(function () {
return '';
});
}
async function searchFallbackScanTopTablePaths(variants, seen, out, budgetRef) {
var urls = [];
var pi;
for (pi = 0; pi < SEARCH_FALLBACK_TOP_TABLE_PATHS.length; pi++) {
if (budgetRef.n <= 0 || out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
budgetRef.n--;
urls.push(BASE + SEARCH_FALLBACK_TOP_TABLE_PATHS[pi]);
}
if (urls.length === 0) {
return;
}
var htmls = await Promise.all(
urls.map(function (u) {
return searchHttpGetSafe(u);
})
);
var hi;
for (hi = 0; hi < htmls.length; hi++) {
searchFallbackConsumeTopTableHtml(htmls[hi], variants, seen, out);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
}
}
async function searchFallbackScanListPathMulti(listPath, variants, seen, out, budgetRef, maxPagesOverride) {
var maxPages =
typeof maxPagesOverride === 'number' && maxPagesOverride > 0
? maxPagesOverride
: SEARCH_FALLBACK_MAX_PAGES;
var page = 1;
while (
page <= maxPages &&
budgetRef.n > 0 &&
out.length < SEARCH_FALLBACK_MAX_RESULTS
) {
var room = Math.min(SEARCH_FALLBACK_CONCURRENCY, budgetRef.n, maxPages - page + 1);
if (room <= 0) {
break;
}
var urls = [];
var bi;
for (bi = 0; bi < room; bi++) {
var pg = page + bi;
var url = BASE + listPath;
if (pg > 1) {
url += '?p=' + pg;
}
urls.push(url);
budgetRef.n--;
}
var htmls = await Promise.all(
urls.map(function (u) {
return searchHttpGetSafe(u);
})
);
var ui;
for (ui = 0; ui < htmls.length; ui++) {
searchFallbackConsumeEachitemHtml(htmls[ui], variants, seen, out);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
}
page += room;
}
}
async function searchFallbackScanCategoriesConcurrent(variants, seen, out, budgetRef) {
var cidList = ['1', '2', '3', '4', '5', '6', '7'];
var maxPg = SEARCH_FALLBACK_MAX_PAGES;
var pg;
for (pg = 1; pg <= maxPg; pg++) {
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS || budgetRef.n <= 0) {
break;
}
var urls = [];
var ci;
for (ci = 0; ci < cidList.length; ci++) {
if (budgetRef.n <= 0 || out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
var u = BASE + '/class/' + cidList[ci] + '/';
if (pg > 1) {
u += '?p=' + pg;
}
urls.push(u);
budgetRef.n--;
}
if (urls.length === 0) {
break;
}
var htmls = await Promise.all(
urls.map(function (u) {
return searchHttpGetSafe(u);
})
);
var ui;
for (ui = 0; ui < htmls.length; ui++) {
searchFallbackConsumeEachitemHtml(htmls[ui], variants, seen, out);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
break;
}
}
}
}
async function searchFallbackLibraryByKeyword(keyword) {
var variants = keywordFallbackVariants(keyword);
if (variants.length === 0) {
return [];
}
var out = [];
var seen = {};
var budgetRef = { n: SEARCH_FALLBACK_REQUEST_BUDGET };
await searchFallbackScanTopTablePaths(variants, seen, out, budgetRef);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
return out;
}
await searchFallbackScanListPathMulti(
'/quanben/',
variants,
seen,
out,
budgetRef,
SEARCH_FALLBACK_QUANBEN_MAX_PAGES
);
if (out.length >= SEARCH_FALLBACK_MAX_RESULTS) {
return out;
}
await searchFallbackScanListPathMulti(
'/class/all.html',
variants,
seen,
out,
budgetRef
);
if (out.length > 0) {
legado.log('search fallback ' + out.length);
return out;
}
if (budgetRef.n <= 0 || !SEARCH_FALLBACK_SCAN_CATEGORIES) {
return out;
}
await searchFallbackScanCategoriesConcurrent(variants, seen, out, budgetRef);
if (out.length > 0) {
legado.log('search fallback ' + out.length);
}
return out;
}
async function finalizeSearchRowsFromApi(arr, keyword) {
var out = [];
var j;
for (j = 0; j < arr.length; j++) {
out.push(mapSearchApiRow(arr[j]));
}
if (out.length === 0) {
legado.log('search api empty, fallback');
out = await searchFallbackLibraryByKeyword(keyword);
}
return out;
}
/** 同 legado.http 栈 GET→POST,Cookie 随 Set-Cookie 带到后续请求。 */
async function searchTryHttpPipeline(keyword, searchResultsUrl) {
var hdrBase = {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
Referer: BASE + '/',
'User-Agent': SEARCH_BROWSER_UA
};
try {
await legado.http.get(BASE + '/user/search.html', hdrBase);
} catch (ePrim) {
legado.log('search http prime skip');
}
await sleepMs(SEARCH_DELAY_AFTER_GET_MS);
var htmlQ = '';
try {
htmlQ = await legado.http.get(searchResultsUrl, {
Accept: hdrBase.Accept,
'Accept-Language': hdrBase['Accept-Language'],
Referer: BASE + '/user/search.html',
'User-Agent': SEARCH_BROWSER_UA
});
} catch (ePage) {
legado.log('search http q-page fail');
return { ok: false, reason: 'httppage' };
}
var vars = parseInlineAnti(htmlQ);
if (!vars.sign) {
return { ok: false, reason: 'nosign' };
}
await sleepMs(SEARCH_DELAY_AFTER_GET_MS);
await searchWaitGlobalPostGap();
var hdrPost = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Referer: searchResultsUrl,
'X-Requested-With': 'XMLHttpRequest',
Origin: BASE,
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': SEARCH_BROWSER_UA
};
var varsPost = applySearchBeforeLikeBrowser(vars, true);
var body = buildSearchFormBody(keyword, varsPost);
var jsonText = '';
try {
jsonText = await legado.http.post(BASE + '/api/search', body, hdrPost);
} catch (ePost) {
legado.log('search http post fail');
return { ok: false, reason: 'httppost' };
}
searchWriteLastPostMs();
var json;
try {
json = JSON.parse(jsonText);
} catch (eJson) {
return { ok: false, reason: 'json' };
}
var msg = json && json.msg ? String(json.msg) : '';
if (msg.indexOf('频繁') >= 0) {
return { ok: false, reason: 'frequent' };
}
var extracted = searchApiExtractRows(json);
if (extracted === null) {
return { ok: false, reason: 'api', msg: msg };
}
return { ok: true, rows: extracted };
}
/** fetch 取 SID + http 拉 q 页 + fetch POST(仅作回退)。 */
async function searchTryFetchSidPipeline(keyword, searchResultsUrl) {
var keysPageUrl = BASE + '/user/search.html';
var attempt;
for (attempt = 0; attempt < SEARCH_MAX_ATTEMPTS; attempt++) {
if (attempt > 0) {
legado.log('search fetch retry after rate limit, attempt ' + (attempt + 1));
await sleepMs(SEARCH_RETRY_GAP_MS);
await searchWaitGlobalPostGap();
}
var keysRes = await searchFetchKeysPage(keysPageUrl);
var sidCookie = keysRes.sidCookie;
var htmlQ = '';
try {
htmlQ = await searchFetchTokenHtmlHttp(searchResultsUrl, sidCookie);
} catch (e4) {
legado.log('search q-page http failed');
}
var vars = parseInlineAnti(htmlQ);
if (!vars.sign) {
return { ok: false };
}
if (!sidCookie) {
legado.log('search missing SID (fetch Set-Cookie)');
}
await sleepMs(SEARCH_DELAY_AFTER_GET_MS);
var varsPost = applySearchBeforeLikeBrowser(vars, !!sidCookie);
var body = buildSearchFormBody(keyword, varsPost);
var jsonText = await searchFetchPostApi(body, searchResultsUrl, sidCookie);
searchWriteLastPostMs();
var json;
try {
json = JSON.parse(jsonText);
} catch (e1) {
return { ok: false };
}
var extracted = searchApiExtractRows(json);
if (extracted !== null) {
return { ok: true, rows: extracted };
}
var msg = json && json.msg ? String(json.msg) : '';
legado.log('search api fail ' + msg);
if (msg.indexOf('频繁') < 0) {
return { ok: false };
}
}
return { ok: false };
}
/** fetch 短 URL:只取 SID(响应体可丢弃,绝不用于 POST 的 sign)。 */
async function searchFetchKeysPage(shortKeysUrl) {
var hdr = {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
Referer: BASE + '/',
'User-Agent': SEARCH_BROWSER_UA
};
if (typeof fetch === 'function') {
var res = await fetch(shortKeysUrl, {
headers: hdr,
credentials: 'include'
});
var html = await res.text();
var sidCookie = extractSidCookieFromFetchResponse(res);
return { html: html, sidCookie: sidCookie };
}
var htmlOnly = await legado.http.get(shortKeysUrl, hdr);
return { html: htmlOnly, sidCookie: '' };
}
/** 带完整 q 的搜索结果页:Referer 指向搜索入口页更贴近浏览器。 */
async function searchFetchTokenHtmlHttp(searchResultsUrl, sidCookie) {
var hdr = {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
Referer: BASE + '/user/search.html',
'User-Agent': SEARCH_BROWSER_UA
};
if (sidCookie) {
hdr.Cookie = sidCookie;
}
return await legado.http.get(searchResultsUrl, hdr);
}
async function searchFetchPostApi(body, refererUrl, sidCookie) {
var hdr = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Referer: refererUrl,
'X-Requested-With': 'XMLHttpRequest',
Origin: BASE,
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': SEARCH_BROWSER_UA
};
if (sidCookie) {
hdr.Cookie = sidCookie;
}
if (typeof fetch === 'function') {
var res = await fetch(BASE + '/api/search', {
method: 'POST',
headers: hdr,
body: body,
credentials: 'include'
});
return await res.text();
}
return await legado.http.post(BASE + '/api/search', body, hdr);
}
async function search(keyword, page) {
legado.log('search ' + keyword + ' p=' + page);
if (page > 1) {
return [];
}
var pasted = await searchFromPastedNovelUrl(keyword);
if (pasted.length > 0) {
return pasted;
}
var knownBookUrl = searchExactTitleBookUrl(keyword);
if (knownBookUrl) {
var knownOut = await searchFromPastedNovelUrl(knownBookUrl);
if (knownOut.length > 0) {
return knownOut;
}
}
await searchWaitGlobalPostGap();
var searchResultsUrl =
BASE + '/user/search.html?q=' + encodeURIComponent(keyword);
var httpR = await searchTryHttpPipeline(keyword, searchResultsUrl);
if (!httpR.ok) {
if (httpR.reason === 'nosign') {
return await searchFallbackLibraryByKeyword(keyword);
}
legado.log('search http path not ok (' + httpR.reason + '), try fetch-SID');
await sleepMs(120);
await searchWaitGlobalPostGap();
var fetchR = await searchTryFetchSidPipeline(keyword, searchResultsUrl);
if (fetchR.ok) {
return await finalizeSearchRowsFromApi(fetchR.rows, keyword);
}
return await searchFallbackLibraryByKeyword(keyword);
}
return await finalizeSearchRowsFromApi(httpR.rows, keyword);
}
async function TEST(type) {
if (type === '__list__') {
return ['search', 'explore'];
}
if (type === 'search') {
var r = await search('武道人仙', 1);
if (!r || r.length < 1) {
return { passed: false, message: '搜索无结果' };
}
return { passed: true, message: '搜索返回 ' + r.length + ' 条' };
}
if (type === 'explore') {
var b = await explore(1, '玄幻小说');
if (!b || b.length < 1) {
return { passed: false, message: '发现页为空' };
}
return { passed: true, message: '发现页 ' + b.length + ' 条' };
}
return { passed: false, message: '未知: ' + type };
}