三五中文网

http://www.xkushu.org

ethereal-essence (13554) 16小时前 下载:321

小说
三五中文网 (xkushu.org) 小说书源,支持搜索/分类/详情/章节目录/正文;兼容 async 宿主 API(Legado Tauri 书源规范)
二维码导入(APP尚未完成该功能)
// @uuid        019e1211-35ec-7c83-b819-d6fc68076f4d
// @name        三五中文网
// @version     1.2.0
// @author     Ethereal
// @url         http://www.xkushu.org
// @logo        http://www.xkushu.org/favicon.ico
// @type        novel
// @enabled     true
// @description 三五中文网 (xkushu.org) 小说书源,支持搜索/分类/详情/章节目录/正文;兼容 async 宿主 API(Legado Tauri 书源规范)

var BASE = 'http://www.xkushu.org';

/**
 * Tauri 端多为同步返回字符串;鸿蒙等宿主可能返回 Promise,或对异常输入返回非字符串。
 * 统一在此收敛为 string,避免后续 .replace / .split 报错。
 */
async function htmlDecodeSafe(str) {
  if (str == null || str === '') {
    return '';
  }
  var s = String(str);
  if (!legado.htmlDecode) {
    return s;
  }
  var r = legado.htmlDecode(s);
  if (r != null && typeof r.then === 'function') {
    r = await r;
  }
  return String(r == null ? '' : r);
}

var CATEGORIES = [
  '玄幻奇幻',
  '修真武侠',
  '都市言情',
  '历史军事',
  '同人名著',
  '游戏竞技',
  '科幻灵异',
  '耽美动漫',
];

/**
 * 补全为绝对地址;二参 base 为书籍目录页 URL(以 / 结尾)时,用于相对章节如 1.html
 * 不依赖全局 URL 构造器(部分 Legado/鸿蒙 运行时不提供 URL)
 */
function absUrl(href, base) {
  if (!href) {
    return '';
  }
  if (href.indexOf('http://') === 0 || href.indexOf('https://') === 0) {
    return href;
  }
  if (href.indexOf('//') === 0) {
    if (base && base.indexOf('https:') === 0) {
      return 'https:' + href;
    }
    return 'http:' + href;
  }
  if (!base) {
    if (href.charAt(0) === '/') {
      return BASE + href;
    }
    return BASE + '/' + href.replace(/^\//, '');
  }
  if (base.indexOf('http://') !== 0 && base.indexOf('https://') !== 0) {
    base = absUrl(base, null);
  }
  if (href.charAt(0) === '/') {
    var hostM = base.match(/^(https?:\/\/[^/]+)/i);
    if (hostM) {
      return hostM[1] + href;
    }
    return BASE + href;
  }
  if (base.charAt(base.length - 1) !== '/') {
    base = base + '/';
  }
  return base + href;
}

function normalizeIndexUrl(url) {
  if (!url) {
    return url;
  }
  var u = String(url).trim();
  if (u.indexOf('http://') !== 0 && u.indexOf('https://') !== 0) {
    u = absUrl(u);
  }
  if (u.charAt(u.length - 1) !== '/') {
    u = u + '/';
  }
  return u;
}

function pickMeta(html, property) {
  var r = new RegExp('property="' + property + '"\\s+content="([^"]*)"', 'i');
  var m = html.match(r);
  return m ? m[1] : '';
}

function pickTitle(html) {
  var m = html.match(/<title>([^<]+)<\/title>/i);
  return m ? m[1] : '';
}

function pickMetaName(html, name) {
  var r = new RegExp('name="' + name + '"\\s+content="([^"]*)"', 'i');
  var m = html.match(r);
  return m ? m[1] : '';
}

/**
 * /zwwinfo/{分卷}/{id}.htm 通常不带 og:novel:*,但正文表格与「内容简介」段仍有完整字段;
 * 解析后可避免再 GET 体积很大的目录页 /35zwhtml/.../(详情页提速关键)。
 */
function parseZwwinfoDetail(html) {
  var out = { name: '', author: '', kind: '', intro: '' };
  if (!html) {
    return out;
  }
  var h1m = html.match(/<a[^>]*href="[^"]*35zwhtml\/[^"]*"[^>]*>\s*<h1>([^<]+)<\/h1>\s*<\/a>/i);
  if (!h1m) {
    h1m = html.match(/<h1>([^<]+)<\/h1>/i);
  }
  if (h1m) {
    out.name = h1m[1].replace(/^\s+|\s+$/g, '');
  }
  var km = html.match(/类(?:&nbsp;|\s)+别[::]\s*([^<]+)<\/td>/i);
  if (km) {
    out.kind = km[1].replace(/&nbsp;/g, ' ').replace(/^\s+|\s+$/g, '');
  }
  var am = html.match(/作(?:&nbsp;|\s)+者[::][^<]*<a[^>]+>([^<]+)<\/a>/i);
  if (!am) {
    am = html.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/i);
  }
  if (am) {
    out.author = am[1].replace(/^\s+|\s+$/g, '');
  }
  var im = html.match(/内容简介:<\/span>([\s\S]*?)<span\s+class="hottext">/i);
  if (im) {
    var raw = im[1];
    raw = raw.replace(/<br\s*\/?>/gi, '\n');
    raw = raw.replace(/<[^>]+>/g, '');
    raw = raw.replace(/&nbsp;/gi, ' ');
    out.intro = raw.replace(/^\s+|\s+$/g, '');
  }
  return out;
}

/**
 * 简介文本归一化:处理字面 \n/\r\n 并压缩空白行
 */
async function normalizeIntro(text) {
  if (!text) {
    return '';
  }
  var t = await htmlDecodeSafe(String(text));
  t = t.replace(/\\r\\n/g, '\n');
  t = t.replace(/\\n/g, '\n');
  t = t.replace(/\r\n/g, '\n');
  t = t.replace(/\r/g, '\n');
  t = t.replace(/[ \t]+\n/g, '\n');
  t = t.replace(/\n{3,}/g, '\n\n');
  return t.replace(/^\s+|\s+$/g, '');
}

/** 由 /35zwhtml/分卷/书id/ 推断封面,与书站 og:image 规则一致 */
function coverUrlFrom35Path(frag) {
  if (!frag) {
    return '';
  }
  var bid = String(frag).match(/\/35zwhtml\/(\d+)\/(\d+)\//);
  if (!bid) {
    return '';
  }
  return absUrl(
    '/35zwimage/' + bid[1] + '/' + bid[2] + '/' + bid[2] + 's.jpg',
  );
}

/** 从书库/搜索结果的 grid 表格解析书籍列表 */
function parseGridBookRows(html) {
  var re = new RegExp(
    '<td class="odd"><a href="(/35zwhtml/\\d+/\\d+/)">([^<]+)</a></td>\\s*' +
    '<td class="even">[\\s\\S]*?</td>\\s*' +
    '<td class="odd">([^<]+)</td>',
    'gi',
  );
  var list = [];
  var m;
  re.lastIndex = 0;
  while (true) {
    m = re.exec(html);
    if (!m) {
      break;
    }
    var path = m[1];
    var bookUrl = absUrl(path);
    var cov = coverUrlFrom35Path(path);
    list.push({
      name: m[2],
      author: m[3].replace(/^\s+|\s+$/g, ''),
      bookUrl: bookUrl,
      coverUrl: cov,
      cover: cov,
    });
  }
  return list;
}

/**
 * 搜索(POST 到 /modules/article/search.php)
 * @param keyword
 * @param page  页码,从 1 开始(站点若无分页则始终只有一页)
 */
async function search(keyword, page) {
  legado.log('[三五中文网] search keyword=' + String(keyword) + ', page=' + String(page));
  var p = page;
  if (!p || p < 1) {
    p = 1;
  }
  var body =
    'searchtype=articlename&searchkey=' + encodeURIComponent(keyword) + '&page=' + encodeURIComponent(String(p));
  var html = await legado.http.post(
    BASE + '/modules/article/search.php',
    body,
    { 'Content-Type': 'application/x-www-form-urlencoded' },
  );
  return parseGridBookRows(html);
}

/**
 * 分类/发现 /zwwsort{1-8}/0/{page}.htm
 * category === 'GETALL' 时须返回 string[] 分类名(与 Legado Tauri 发现页协议一致,见官方文档 discover)
 * @param page     页码,从 1 开始
 * @param category 分类名、数字 1-8,或 'GETALL'(仅取分类列表,不发请求)
 */
var SORT_ID_MAP = {
  玄幻奇幻: 1,
  修真武侠: 2,
  都市言情: 3,
  历史军事: 4,
  同人名著: 5,
  游戏竞技: 6,
  科幻灵异: 7,
  耽美动漫: 8,
};

async function explore(page, category) {
  legado.log('[三五中文网] explore page=' + String(page) + ', category=' + String(category));
  if (category === 'GETALL') {
    return CATEGORIES.slice();
  }
  var pg = page;
  if (!pg || pg < 1) {
    pg = 1;
  }
  var sortId = 1;
  if (category !== null && category !== undefined && String(category) !== '') {
    var c = String(category);
    if (SORT_ID_MAP[c] !== undefined) {
      sortId = SORT_ID_MAP[c];
    } else {
      var n = parseInt(c, 10);
      if (!isNaN(n) && n >= 1 && n <= 8) {
        sortId = n;
      } else {
        return CATEGORIES.slice();
      }
    }
  }
  var url = BASE + '/zwwsort' + String(sortId) + '/0/' + String(pg) + '.htm';
  var html = await legado.http.get(url);
  return parseGridBookRows(html);
}

/**
 * 书籍详情
 * 为了加速:优先请求体积更小的 /zwwinfo/{cat}/{id}.htm,再回退到目录页 /35zwhtml/.../
 * @param bookUrl 书籍页 URL,以 / 结尾
 */
async function bookInfo(bookUrl) {
  legado.log('[三五中文网] bookInfo url=' + String(bookUrl));
  if (
    bookUrl &&
    typeof bookUrl === 'object' &&
    typeof bookUrl.length === 'number' &&
    bookUrl.length > 0 &&
    typeof bookUrl !== 'string'
  ) {
    bookUrl = bookUrl[0];
  }
  var u = normalizeIndexUrl(bookUrl);
  var bid = u.match(/\/35zwhtml\/(\d+)\/(\d+)\//);
  var infoHtml = '';
  if (bid) {
    // 详情简介页比目录页小很多,优先用它
    var infoUrl = BASE + '/zwwinfo/' + bid[1] + '/' + bid[2] + '.htm';
    infoHtml = await legado.http.get(infoUrl);
  }

  // 优先从 infoHtml 取;不足再回退到 tocHtml
  var name = '';
  var author = '';
  var kind = '';
  var intro = '';
  var cover = '';

  if (infoHtml) {
    var zw = parseZwwinfoDetail(infoHtml);
    name = pickMeta(infoHtml, 'og:novel:book_name');
    if (!name) {
      name = zw.name;
    }
    if (!name) {
      var t = pickTitle(infoHtml);
      if (t) {
        // e.g. 斗破苍穹最新章节-天蚕土豆-三五中文网(优先 h1,避免书名带「最新章节」)
        name = t.split('-')[0];
      }
    }
    author = pickMeta(infoHtml, 'og:novel:author');
    if (!author) {
      author = zw.author;
    }
    if (!author) {
      var am = infoHtml.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/);
      if (am) {
        author = am[1];
      }
    }
    kind = pickMeta(infoHtml, 'og:novel:category');
    if (!kind) {
      kind = zw.kind;
    }
    intro = pickMeta(infoHtml, 'og:description');
    if (!intro) {
      intro = zw.intro;
    }
    if (!intro) {
      intro = pickMetaName(infoHtml, 'description');
    }
    intro = await normalizeIntro(intro);
  }

  // 封面可直接按路径推断(最快),否则再从页面 meta 取
  if (bid) {
    cover = absUrl('/35zwimage/' + bid[1] + '/' + bid[2] + '/' + bid[2] + 's.jpg');
  }

  if (!name || !author || !kind || !intro) {
    // 回退读取目录页(较大,但字段最全)
    var tocHtml = await legado.http.get(u);
    if (!name) {
      name = pickMeta(tocHtml, 'og:novel:book_name');
      if (!name) {
        var h1m = tocHtml.match(/<div id="title">\s*<h1>([^<]+)<\/h1>/i);
        if (h1m) {
          name = h1m[1].replace(/全文阅读$/, '');
        } else {
          name = pickTitle(tocHtml);
        }
      }
    }
    if (!author) {
      author = pickMeta(tocHtml, 'og:novel:author');
      if (!author) {
        var am2 = tocHtml.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/);
        if (am2) {
          author = am2[1];
        }
      }
    }
    if (!kind) {
      kind = pickMeta(tocHtml, 'og:novel:category');
    }
    if (!intro) {
      intro = await normalizeIntro(pickMeta(tocHtml, 'og:description'));
    }
    if (!cover) {
      cover = pickMeta(tocHtml, 'og:image');
      if (cover) {
        cover = absUrl(cover);
      }
    }
  }

  return {
    name: name,
    author: author,
    kind: kind,
    intro: intro,
    bookUrl: u,
    tocUrl: u,
    coverUrl: cover,
  };
}

/**
 * 从 &lt;a&gt; 开标签属性里取 href、title(忽略顺序;兼容仅有正文无 title)
 */
function parseChapterAOpen(tagOpen) {
  var hrefM = tagOpen.match(/\bhref\s*=\s*"([0-9]+\.html)"/i);
  if (!hrefM) {
    hrefM = tagOpen.match(/\bhref\s*=\s*'([0-9]+\.html)'/i);
  }
  if (!hrefM) {
    return null;
  }
  var tM = tagOpen.match(/\btitle\s*=\s*"([^"]*)"/i);
  if (!tM) {
    tM = tagOpen.match(/\btitle\s*=\s*'([^']*)'/i);
  }
  return { href: hrefM[1], titleAttr: tM ? tM[1] : '' };
}

/**
 * 章节目录(目录为书籍首页 #list dl dd a)
 * @param tocUrl 与 bookInfo 的 tocUrl 相同
 */
async function chapterList(tocUrl) {
  legado.log('[三五中文网] chapterList tocUrl=' + String(tocUrl));
  var u = normalizeIndexUrl(tocUrl);
  var html = await legado.http.get(u);
  var i0 = html.indexOf('id="list"');
  if (i0 < 0) {
    return [];
  }
  var sub = html.substring(i0);
  var iEnd = sub.indexOf('class="listend"');
  if (iEnd > 0) {
    sub = sub.substring(0, iEnd);
  }
  // 任意属性格式:<a href="1.html" ...> 与 <a title="x" href="1.html" ...> 均支持
  var re = /<a(\s+[^>]+)>([^<]*)<\/a>/gi;
  var seen = {};
  var chapters = [];
  var m;
  re.lastIndex = 0;
  while (true) {
    m = re.exec(sub);
    if (!m) {
      break;
    }
    var p = parseChapterAOpen(' ' + m[1]);
    if (!p) {
      continue;
    }
    var full = absUrl(p.href, u);
    if (seen[full]) {
      continue;
    }
    seen[full] = 1;
    var inner = m[2] ? m[2].replace(/^\s+|\s+$/g, '') : '';
    var title = p.titleAttr ? p.titleAttr : inner;
    title = await htmlDecodeSafe(title);
    if (!title) {
      title = p.href;
    }
    chapters.push({ name: title, title: title, url: full });
  }
  return chapters;
}

/**
 * 从章节页只取 #content 内小说部分:在「广告 gg_read_content_up 的 &lt;/div&gt;」与「#content 结束的 &lt;/div&gt;」之间,不含 read_* 脚本
 */
function extractContentBodyHtml(pageHtml) {
  var p0 = pageHtml.indexOf('id="content"');
  if (p0 < 0) {
    return '';
  }
  p0 = pageHtml.indexOf('>', p0) + 1;
  var afterGg = p0;
  if (pageHtml.indexOf('gg_read_content_up', p0) >= 0) {
    var pGg = pageHtml.indexOf('gg_read_content_up', p0);
    var closeGg = pageHtml.indexOf('</div>', pGg);
    if (closeGg > 0) {
      afterGg = closeGg + 6;
    }
  }
  var close = pageHtml.indexOf('</div>', afterGg);
  if (close < 0) {
    return '';
  }
  var block = pageHtml.substring(afterGg, close);
  block = block.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
  return block;
}

/**
 * 简单 HTML 片段转纯文本:br/换行,去标签
 */
async function htmlFragmentToText(fragment) {
  if (!fragment) {
    return '';
  }
  var t = fragment;
  t = t.replace(/<br\s*\/?>\s*/gi, '\n');
  t = t.replace(/<\/p>\s*/gi, '\n');
  t = t.replace(/&nbsp;/gi, ' ');
  t = t.replace(/<[^>]+>/g, '');
  if (legado.htmlDecode) {
    t = await htmlDecodeSafe(t);
  } else {
    t = t.replace(/&lt;/g, '<');
    t = t.replace(/&gt;/g, '>');
    t = t.replace(/&amp;/g, '&');
    t = t.replace(/&quot;/g, '"');
  }
  t = t.replace(/[ \t]+\n/g, '\n');
  t = t.replace(/\n{3,}/g, '\n\n');
  return t;
}

/**
 * 剔除 read_* 残留与括号包起来的「三五 + 全角/半角域名」水印
 */
function stripSiteWatermarks(t) {
  if (!t) {
    return '';
  }
  t = t.replace(/read_[a-z0-9_]*\s*\(\s*\)\s*;?/gi, '');
  t = t.replace(/[((][\s\S]*?三五中文网[\s\S]*?[))]/g, '');
  t = t.replace(/[wwwwWW]{1,3}[.\\.\s\n]*[33三][55五][zzZ][wwWW][wwWW][.\\.][\s\n]*[ccC][ooOO][mmMM][))\s]*/g, '');
  t = t.replace(/www\.35zww\.com/gi, '');
  t = t.replace(/35zww[..][comcom]/gi, '');
  t = t.replace(/www[..][33][55][zz][ww]{2}/gi, '');
  t = t.replace(/三五中文网/g, '');
  t = t.replace(/[))]?\s*$/g, '');
  t = t.replace(/^\s*[))((]\s*/g, '');
  t = t.replace(/\n{3,}/g, '\n\n');
  return t;
}

/**
 * 章节正文(用 HTML 子串 + 去广告,避免 #content 内 read_* 脚本名进入正文)
 * @param chapterUrl
 */
async function chapterContent(chapterUrl) {
  legado.log('[三五中文网] chapterContent url=' + String(chapterUrl));
  var html = await legado.http.get(chapterUrl);
  var body = extractContentBodyHtml(html);
  var text;
  if (body) {
    text = await htmlFragmentToText(body);
  } else {
    var doc = legado.dom.parse(html);
    text = legado.dom.selectText(doc, '#content');
    legado.dom.free(doc);
  }
  if (!text) {
    return '';
  }
  text = stripSiteWatermarks(text);
  return text;
}

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: '未知: ' + String(type) };
}
广告