抖音小说-笔趣阁(移动)
https://m.douyinxs.com
zpccool (13551) 16小时前 下载:336
小说 小说 笔趣阁 移动
适配 m.douyinxs.com,支持搜索/分类/详情/目录/正文;修复搜索使用未编码POST及DOM API兼容性。
// @name 抖音小说-笔趣阁(移动)
// @uuid douyinxiaoshuobiquge
// @version 1.0.0
// @author AI
// @url https://m.douyinxs.com
// @logo https://m.douyinxs.com/static/ss_wap_bqg/favicon.ico
// @enabled true
// @type novel
// @tags 小说,笔趣阁,移动
// @description 适配 m.douyinxs.com,支持搜索/分类/详情/目录/正文;修复搜索使用未编码POST及DOM API兼容性。
var BASE = "https://m.douyinxs.com";
var CATEGORIES = [
{ name: "全部分类", id: "0" },
{ name: "玄奇", id: "1" },
{ name: "高武", id: "2" },
{ name: "修仙", id: "3" },
{ name: "都市", id: "4" },
{ name: "军事", id: "5" },
{ name: "历史", id: "6" },
{ name: "游体", id: "7" },
{ name: "科幻", id: "8" },
{ name: "恐怖", id: "9" },
{ name: "次元", id: "10" },
{ name: "女生", id: "11" },
];
// ─── 工具函数 ─────────────────────────────────
function trim(s) { return String(s || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim(); }
function toAbs(url) {
if (!url) return "";
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0) return url;
if (url.indexOf("//") === 0) return "https:" + url;
if (url.charAt(0) === "/") return BASE + url;
return BASE + "/" + url;
}
function normalizeBookUrl(url) {
var abs = toAbs(url);
var m = abs.match(/\/bqg\/(\d+)(?:\/|$)/);
return m ? BASE + "/bqg/" + m[1] + "/" : abs;
}
function normalizeChapterUrl(url) {
var abs = toAbs(url);
var m = abs.match(/\/bqg\/(\d+)\/(\d+)(?:_(\d+))?\.html/);
if (!m) return abs;
return BASE + "/bqg/" + m[1] + "/" + m[2] + (m[3] ? ("_" + m[3]) : "") + ".html";
}
function chapterBaseUrl(url) { return normalizeChapterUrl(url).replace(/_(\d+)\.html$/, ".html"); }
function chapterPageUrl(url, pageNo) {
var base = chapterBaseUrl(url);
if (pageNo <= 1) return base;
return base.replace(/\.html$/, "_" + pageNo + ".html");
}
function isKeywordMatched(name, keyword) {
var n = trim(name).toLowerCase().replace(/\s+/g, "").replace(/[^\w\u4e00-\u9fa5]/g, "");
var k = trim(keyword).toLowerCase().replace(/\s+/g, "").replace(/[^\w\u4e00-\u9fa5]/g, "");
if (!k) return false;
if (!n) return false;
return n.indexOf(k) !== -1 || k.indexOf(n) !== -1;
}
function meta(html, prop) {
var reg = new RegExp('<meta[^>]+property=["\']' + prop + '["\'][^>]*content=["\']([^"\']+)["\']', "i");
var m = html.match(reg);
return m ? trim(m[1]) : "";
}
// ─── 搜索解析(兼容 h4 a 结构,使用 legado.dom.select)────
function parseSearchBooks(html) {
var doc = legado.dom.parse(html || "");
var boxes = legado.dom.selectAll(doc, ".bookbox");
var out = [];
var seen = {};
for (var i = 0; i < boxes.length; i++) {
var b = boxes[i];
// 书名和链接:优先 h4 a,回退 .bookname a
var nameEl = legado.dom.select(b, "h4 a");
if (!nameEl) nameEl = legado.dom.select(b, ".bookname a");
if (!nameEl) continue;
var name = trim(legado.dom.text(nameEl));
var bookUrl = normalizeBookUrl(legado.dom.attr(nameEl, "href"));
if (!name || !bookUrl || seen[bookUrl]) continue;
seen[bookUrl] = 1;
// 作者:第一个 .author
var authorEl = legado.dom.select(b, ".author");
var author = trim((authorEl ? legado.dom.text(authorEl) : "")).replace(/^作者[::]\s*/, "");
// 类型:第二个 .author
var authorAll = legado.dom.selectAll(b, ".author");
var kindEl = authorAll.length > 1 ? authorAll[1] : null;
var kind = trim((kindEl ? legado.dom.text(kindEl) : "")).replace(/^类型[::]\s*/, "");
// 封面
var coverEl = legado.dom.select(b, ".bookimg img");
var coverUrl = coverEl ? legado.dom.attr(coverEl, "src") : "";
// 最新章节
var updateEl = legado.dom.select(b, ".update a");
var latest = trim((updateEl ? legado.dom.text(updateEl) : ""));
out.push({
name: name,
author: author,
bookUrl: bookUrl,
tocUrl: bookUrl,
coverUrl: toAbs(coverUrl),
latestChapter: latest,
lastChapter: latest,
kind: kind,
type: kind
});
}
legado.dom.free(doc);
return out;
}
// ─── 搜索主函数(修复版:未编码 POST + GBK 回退)────
async function search(keyword, page) {
var rawKeyword = trim(keyword);
if (!rawKeyword) return [];
legado.log("[search] keyword=" + rawKeyword + " page=" + page);
// 方式1:直接 POST 明文中文(模拟 JSON 书源)
var body = "searchkey=" + rawKeyword;
var html = "";
try {
html = await legado.http.post(
BASE + "/search/",
body,
{
"Content-Type": "application/x-www-form-urlencoded",
"Referer": BASE + "/",
"Cookie": "",
"X-Requested-With": "XMLHttpRequest"
}
);
legado.log("[search][POST-raw] len=" + (html ? html.length : 0));
} catch (e) {
legado.log("[search][POST-raw] failed: " + e);
}
// 方式2:如果失败,尝试 GBK 编码
if (!html || html.length < 500) {
try {
var gbkBody = "searchkey=" + legado.urlEncodeCharset(rawKeyword, "gbk");
html = await legado.http.post(
BASE + "/search/",
gbkBody,
{
"Content-Type": "application/x-www-form-urlencoded",
"Referer": BASE + "/",
"Cookie": "",
"X-Requested-With": "XMLHttpRequest"
}
);
legado.log("[search][POST-gbk] len=" + (html ? html.length : 0));
} catch (e) {
legado.log("[search][POST-gbk] failed: " + e);
}
}
if (html && html.length > 500) {
var books = parseSearchBooks(html);
legado.log("[search] parsed " + books.length + " books");
return books.filter(function(b) {
return isKeywordMatched(b.name, rawKeyword) || isKeywordMatched(b.author, rawKeyword);
});
}
legado.log("[search] all POST attempts returned empty or short content");
return [];
}
// ─── 发现页 (explore) ─────────────────────────
function categoryId(category) {
var c = trim(category || "");
if (!c || c === "全部分类" || c === "0") return "0";
for (var i = 0; i < CATEGORIES.length; i++) {
if (CATEGORIES[i].id === c || CATEGORIES[i].name === c) return CATEGORIES[i].id;
}
return "0";
}
async function explore(page, category) {
if (category === "GETALL") {
var names = [];
for (var i = 0; i < CATEGORIES.length; i++) names.push(CATEGORIES[i].name);
return names;
}
var p = page || 1;
var cid = categoryId(category);
var path = cid === "0" ? (p > 1 ? ("/fenlei/" + p + "/") : "/fenlei/") : ("/fenlei/" + cid + "/" + p + "/");
var html = await legado.http.get(BASE + path);
var doc = legado.dom.parse(html);
var cards = legado.dom.selectAll(doc, ".recommend .hot_sale");
var out = [];
var seen = {};
for (var j = 0; j < cards.length; j++) {
var el = cards[j];
var name = trim(legado.dom.selectText(el, ".title"));
var bookUrl = normalizeBookUrl(legado.dom.selectAttr(el, "a", "href"));
if (!name || !bookUrl || seen[bookUrl]) continue;
seen[bookUrl] = 1;
var author = trim(legado.dom.selectText(el, ".author")).replace(/^作者[::]\s*/, "").replace(/\s*\(.+\)\s*$/, "");
var intro = trim(legado.dom.selectText(el, ".review"));
var kind = cid === "0" ? "" : trim((function () {
for (var k = 0; k < CATEGORIES.length; k++) if (CATEGORIES[k].id === cid) return CATEGORIES[k].name;
return "";
})());
out.push({
name: name,
author: author,
bookUrl: bookUrl,
tocUrl: bookUrl,
intro: intro,
coverUrl: "",
kind: kind,
type: kind,
latestChapter: "",
lastChapter: ""
});
}
legado.dom.free(doc);
return out;
}
// ─── 书籍详情 (bookInfo) ──────────────────────
async function bookInfo(bookUrl) {
var url = normalizeBookUrl(bookUrl);
var html = await legado.http.get(url);
var doc = legado.dom.parse(html);
var name = meta(html, "og:title") || trim(legado.dom.selectText(doc, ".channelHeader .title"));
var author = meta(html, "og:novel:author") || trim(legado.dom.selectText(doc, ".synopsisArea_detail .author")).replace(/^作者[::]\s*/, "");
var kind = meta(html, "og:novel:category") || trim(legado.dom.selectText(doc, ".synopsisArea_detail .sort")).replace(/^类别[::]\s*/, "");
var status = meta(html, "og:novel:status") || trim(legado.dom.selectText(doc, ".synopsisArea_detail p:nth-of-type(2)")).replace(/^状态[::]\s*/, "");
var latest = meta(html, "og:novel:latest_chapter_name");
var intro = trim(legado.dom.selectText(doc, ".synopsisArea .review"));
var cover = meta(html, "og:image") || toAbs(legado.dom.selectAttr(doc, ".synopsisArea_detail img", "src"));
var updateTime = meta(html, "og:novel:update_time");
legado.dom.free(doc);
return {
name: name,
author: author,
bookUrl: url,
tocUrl: url,
coverUrl: cover,
intro: intro + (updateTime ? ("\n\n更新时间:" + updateTime) : ""),
latestChapter: latest,
lastChapter: latest,
kind: kind,
type: kind,
status: status
};
}
// ─── 章节目录 (chapterList) ────────────────────
function extractTocPageUrls(html, currentUrl) {
var map = {};
map[normalizeBookUrl(currentUrl)] = 1;
var reg = /<option[^>]+value=["']([^"']+)["']/gi;
var m;
while ((m = reg.exec(html))) {
var v = m[1];
if (!v || v.indexOf("/bqg/") === -1) continue;
map[normalizeBookUrl(v)] = 1;
}
var arr = [];
for (var k in map) arr.push(k);
return arr;
}
function extractChapterLinks(html) {
var match = html.match(/<h2><a>[^<]*正文[^<]*<\/a><\/h2>[\s\S]*?<div class="directoryArea">([\s\S]*?)<\/div>/i);
var block = match ? match[1] : "";
var out = [];
if (!block) return out;
var reg = /<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
var m;
while ((m = reg.exec(block))) {
var name = trim(m[2].replace(/<[^>]*>/g, ""));
var url = normalizeChapterUrl(m[1]);
if (name && url) out.push({ name: name, url: url });
}
return out;
}
async function chapterList(tocUrl) {
var firstUrl = normalizeBookUrl(tocUrl);
var html0 = await legado.http.get(firstUrl);
var pageUrls = extractTocPageUrls(html0, firstUrl);
var htmls = await Promise.all(pageUrls.map(async function (u) {
try { return await legado.http.get(u); } catch (e) { return ""; }
}));
var chapters = [];
var seen = {};
for (var i = 0; i < htmls.length; i++) {
var list = extractChapterLinks(htmls[i]);
for (var j = 0; j < list.length; j++) {
var u = list[j].url;
if (!u || seen[u]) continue;
seen[u] = 1;
chapters.push({ name: list[j].name, url: u, chapterUrl: u });
}
}
chapters.sort(function (a, b) {
var am = a.url.match(/\/(\d+)(?:_(\d+))?\.html$/);
var bm = b.url.match(/\/(\d+)(?:_(\d+))?\.html$/);
var av = am ? parseInt(am[1], 10) : 0;
var bv = bm ? parseInt(bm[1], 10) : 0;
return av - bv;
});
return chapters;
}
// ─── 正文内容 (chapterContent) ─────────────────
function titlePageCount(html) {
var m = (html || "").match(/<title>[^<]*\((\d+)\s*\/\s*(\d+)\)[^<]*<\/title>/i);
if (!m) return 1;
var n = parseInt(m[2], 10);
return n > 1 ? n : 1;
}
function parseChapterText(html) {
var doc = legado.dom.parse(html || "");
var ps = legado.dom.selectAllTexts(doc, "#chaptercontent p");
var lines = [];
for (var i = 0; i < ps.length; i++) {
var t = trim(ps[i]);
if (!t) continue;
if (/加入书签|章节报错|本章未完|点击下一页/.test(t)) continue;
lines.push(t);
}
legado.dom.free(doc);
return lines.join("\n\n");
}
async function chapterContent(chapterUrl) {
var firstUrl = normalizeChapterUrl(chapterUrl);
var firstHtml = await legado.http.get(firstUrl);
var count = titlePageCount(firstHtml);
var pages = [firstHtml];
if (count > 1) {
var rest = await Promise.all(
(function () {
var arr = [];
for (var i = 2; i <= count; i++) {
arr.push(
legado.http.get(chapterPageUrl(firstUrl, i)).catch(function () { return ""; })
);
}
return arr;
})()
);
for (var j = 0; j < rest.length; j++) if (rest[j]) pages.push(rest[j]);
}
var out = [];
for (var k = 0; k < pages.length; k++) {
var txt = parseChapterText(pages[k]);
if (txt) out.push(txt);
}
return out.join("\n\n");
}
// ─── 兼容旧调用命名 ──────────────────────────
async function toc(tocUrl) { return await chapterList(tocUrl); }
async function content(chapterUrl) { return await chapterContent(chapterUrl); }
// ─── 测试函数 ─────────────────────────────────
async function TEST(type) {
if (type === "__list__") return ["search", "explore", "bookInfo", "chapterList", "chapterContent"];
if (type === "search") {
var r = await search("万族之劫", 1);
return { passed: r && r.length > 0, message: "搜索结果数: " + (r ? r.length : 0) };
}
if (type === "explore") {
var b = await explore(1, "玄奇");
return { passed: b && b.length > 0, message: "发现页结果数: " + (b ? b.length : 0) };
}
if (type === "bookInfo") {
var i = await bookInfo(BASE + "/bqg/223080/");
return { passed: !!(i && i.name), message: "书名: " + (i ? i.name : "") + " 作者: " + (i ? i.author : "") };
}
if (type === "chapterList") {
var c = await chapterList(BASE + "/bqg/223080/");
return { passed: c && c.length > 0, message: "章节数: " + (c ? c.length : 0) };
}
if (type === "chapterContent") {
var t = await chapterContent(BASE + "/bqg/223080/101387016.html");
return { passed: !!t && t.length > 100, message: "正文长度: " + (t ? t.length : 0) };
}
return { passed: false, message: "未知测试类型: " + type };
}