抖音小说-笔趣阁(移动)

https://m.douyinxs.com

zpccool (13551) 16小时前 下载:336

小说 小说 笔趣阁 移动
适配 m.douyinxs.com,支持搜索/分类/详情/目录/正文;修复搜索使用未编码POST及DOM API兼容性。
二维码导入(APP尚未完成该功能)
// @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 };
}
广告