166看书小说

http://www.16kbook.co

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

小说 免费 小说 免费小说
166看书小说(16kbook.co),免费网文小说站,以玄幻/都市/言情题材为主。
二维码导入(APP尚未完成该功能)
// @uuid        019e120c-c6a3-75eb-b43a-ad55c4db19c5
// @name        166看书小说
// @version     2.0.1
// @author      Ethereal
// @url         http://www.16kbook.co
// @logo        http://www.16kbook.co/favicon.ico
// @enabled     true
// @tags        免费,小说,免费小说
// @description 166看书小说(16kbook.co),免费网文小说站,以玄幻/都市/言情题材为主。

// ─── 内置测试 ─────────────────────────────────────────────────────────────

async function TEST(type) {
  if (type === '__list__') return ['search', 'explore', 'bookInfo', 'chapterList', 'chapterContent'];

  if (type === 'search') {
    var results = await search('斗破苍穹', 1);
    if (!results || results.length < 1) return { passed: false, message: '搜索"斗破苍穹"无结果' };
    return { passed: true, message: '搜索返回 ' + results.length + ' 条' };
  }

  if (type === 'explore') {
    var books = await explore(1, '玄幻');
    if (!books || books.length < 1) return { passed: false, message: '发现页 [玄幻] 返回为空' };
    return { passed: true, message: '发现页 [玄幻]: ' + books.length + ' 条结果 ✓' };
  }

  if (type === 'bookInfo') {
    var r = await bookInfo('http://www.16kbook.co/81_81408/');
    return { passed: !!r.name, message: 'bookInfo name=' + r.name };
  }

  if (type === 'chapterList') {
    var r = await chapterList('http://www.16kbook.co/81_81408/');
    return { passed: r.length > 0, message: 'chapterList cnt=' + r.length + ' first=' + (r[0] ? r[0].name : 'N/A') };
  }

  if (type === 'chapterContent') {
    var r = await chapterContent('http://www.16kbook.co/81_81408/43271175.html');
    return { passed: r.length > 100, message: 'chapterContent len=' + r.length + ' first=' + r.substring(0, 40) };
  }

  return { passed: false, message: '未知测试类型: ' + type };
}

// ─── 配置 ────────────────────────────────────────────────────────────────

var BASE = 'http://www.16kbook.co';

/** 章节 URL 正则:/数字_数字/数字.html 或 /数字_数字/数字_数字.html(多页) */
var CHAPTER_URL_PATTERN = /\/\d+_\d+\/\d+(?:_\d+)?\.html/;

// ─── 工具 ────────────────────────────────────────────────────────────────

function toAbs(href) {
  if (!href) return '';
  if (href.indexOf('http') === 0) return href;
  return BASE + (href.charAt(0) === '/' ? href : '/' + href);
}

/** 将相对 href 解析为绝对 URL(基于 baseUrl 的目录) */
function resolveUrl(href, baseUrl) {
  if (!href) return '';
  if (href.indexOf('http') === 0) return href;
  if (href.charAt(0) === '/') return BASE + href;
  var dir = baseUrl.replace(/[^\/]*$/, '');
  return dir + href;
}

function extractCover(el, selector) {
  return legado.dom.selectAttr(el, selector, 'data-original')
      || legado.dom.selectAttr(el, selector, 'data-src')
      || legado.dom.selectAttr(el, selector, 'src')
      || '';
}

/** 与搜索路径解析共用同一页 HTML,只请求一次分类列表 */
var _cookieReady = false;
var _probeHtml = '';

/** 列表页 URL(用于 Referer;勿用 /rank/ 后立刻打搜索接口,易触发站点频控) */
var SEARCH_PATH_PROBE = '/class/xuanhuan/1/';

/** 站点对非浏览器栈较敏感,请求头贴近真实浏览器 */
var HTML_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';

/** 与常见桌面 Chrome 一致;站点对 Legado 默认 UA 会返回「搜索间隔」脚本 */
var CHROME_UA =
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

/** 列表/详情/章节与探测页统一请求头;鸿蒙等端裸 http.get 常拿到空壳页,DOM 无 .item */
function httpGetHtml(url, referer) {
  if (!referer) referer = BASE + '/';
  return legado.http.get(url, {
    Accept: HTML_ACCEPT,
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'User-Agent': CHROME_UA,
    Referer: referer,
  });
}

async function ensureCookie() {
  if (_cookieReady) return;
  await resolveSearchPath();
  _cookieReady = true;
}

var _searchPath = '';
async function resolveSearchPath() {
  if (_searchPath) return _searchPath;
  var probeUrl = BASE + SEARCH_PATH_PROBE;
  legado.log('[searchPath] probe=' + probeUrl);
  if (!_probeHtml) {
    _probeHtml = await httpGetHtml(probeUrl);
  }
  var doc = legado.dom.parse(_probeHtml);
  var form = legado.dom.select(doc, 'form#search');
  if (!form) form = legado.dom.select(doc, 'form[name="search"]');
  if (!form) {
    legado.dom.free(doc);
    _probeHtml = '';
    legado.log('[searchPath] form not found');
    return '';
  }
  var act = (legado.dom.attr(form, 'action') || '').trim();
  legado.dom.free(doc);
  if (!act) {
    _probeHtml = '';
    legado.log('[searchPath] empty action');
    return '';
  }
  if (act.indexOf('http://') === 0 || act.indexOf('https://') === 0) {
    var slash = act.indexOf('/', act.indexOf('//') + 2);
    _searchPath = slash >= 0 ? act.substring(slash) : '';
  } else {
    _searchPath = act.charAt(0) === '/' ? act : '/' + act;
  }
  legado.log('[searchPath] ' + _searchPath);
  _probeHtml = '';
  return _searchPath;
}

// ─── 搜索 ─────────────────────────────────────────────────────────────────

async function search(keyword, page) {
  legado.log('[search] keyword=' + keyword);
  await ensureCookie();
  var path = await resolveSearchPath();
  if (!path) {
    legado.log('[search] unresolved path');
    return [];
  }
  var url = BASE + path + '?searchkey=' + encodeURIComponent(keyword);
  var html = await httpGetHtml(url, BASE + SEARCH_PATH_PROBE);

  if (html.indexOf('搜索间隔') !== -1) {
    legado.log('[search] http blocked, browser extract');
    var browserCode = [
      'await new Promise(function(r){ setTimeout(r, 600); });',
      'var nodes = document.querySelectorAll(".item");',
      'var out = [];',
      'for (var i = 0; i < nodes.length; i++) {',
      '  var el = nodes[i];',
      '  var na = el.querySelector("dt a");',
      '  if (!na) continue;',
      '  var href = na.getAttribute("href") || "";',
      '  if (!href) continue;',
      '  var name = (na.textContent || "").trim();',
      '  if (!name) continue;',
      '  var img = el.querySelector("img");',
      '  var coverUrl = "";',
      '  if (img) {',
      '    coverUrl = img.getAttribute("data-original") || img.getAttribute("data-src") || img.getAttribute("src") || "";',
      '  }',
      '  var ba = el.querySelector(".btm a");',
      '  var author = ba ? (ba.textContent || "").trim() : "";',
      '  var dd = el.querySelector("dd");',
      '  var intro = dd ? (dd.textContent || "").trim() : "";',
      '  out.push({ name: name, author: author, bookUrl: href, coverUrl: coverUrl, intro: intro });',
      '}',
      'return JSON.stringify(out);',
    ].join('');
    try {
      var json = await legado.browser.run(url, browserCode, {
        visible: false,
        waitUntil: 'domcontentloaded',
        timeoutSecs: 25,
      });
      if (!json || json.length < 5) return [];
      var arr = JSON.parse(json);
      for (var j = 0; j < arr.length; j++) {
        if (arr[j].bookUrl) arr[j].bookUrl = toAbs(arr[j].bookUrl);
        if (arr[j].coverUrl) arr[j].coverUrl = toAbs(arr[j].coverUrl);
      }
      legado.log('[search] found=' + arr.length + ' (browser)');
      return arr;
    } catch (e) {
      legado.log('[search] browser failed: ' + e.message);
      return [];
    }
  }

  if (!html || html.length < 200) {
    legado.log('[search] empty body');
    return [];
  }
  var doc = legado.dom.parse(html);
  var books = [];

  var items = legado.dom.selectAll(doc, '.item');
  for (var i = 0; i < items.length; i++) {
    var el = items[i];

    var nameLink = legado.dom.select(el, 'dt a');
    if (!nameLink) continue;
    var bookUrl = legado.dom.attr(nameLink, 'href') || '';
    if (!bookUrl) continue;
    bookUrl = toAbs(bookUrl);

    var name = (legado.dom.text(nameLink) || '').trim();
    if (!name) continue;

    var coverUrl = extractCover(el, 'img');

    // 作者在 .btm a 中
    var author = (legado.dom.selectText(el, '.btm a') || '').trim();
    var intro = (legado.dom.selectText(el, 'dd') || '').trim();

    books.push({
      name: name,
      author: author,
      bookUrl: bookUrl,
      coverUrl: coverUrl,
      intro: intro,
    });
  }

  legado.dom.free(doc);
  legado.log('[search] found=' + books.length);
  return books;
}

// ─── 书籍详情 ─────────────────────────────────────────────────────────────

async function bookInfo(bookUrl) {
  legado.log('[bookInfo] url=' + bookUrl);
  await ensureCookie();
  var html = await httpGetHtml(bookUrl, BASE + '/');
  var doc = legado.dom.parse(html);

  var result = {
    name:        legado.dom.selectAttr(doc, '[property="og:novel:book_name"]', 'content') || '',
    author:      legado.dom.selectAttr(doc, '[property="og:novel:author"]', 'content') || '',
    coverUrl:    legado.dom.selectAttr(doc, '[property="og:image"]', 'content') || '',
    intro:       legado.dom.selectAttr(doc, '[property="og:description"]', 'content') || '',
    lastChapter: legado.dom.selectAttr(doc, '[property="og:novel:lastest_chapter_name"]', 'content')
              || legado.dom.selectAttr(doc, '[property="og:novel:latest_chapter_name"]', 'content')
              || '',
    kind:        legado.dom.selectAttr(doc, '[property="og:novel:category"]', 'content') || '',
    tocUrl:      bookUrl,
  };

  legado.dom.free(doc);
  return result;
}

// ─── 章节列表 ─────────────────────────────────────────────────────────────

async function chapterList(tocUrl) {
  legado.log('[chapterList] url=' + tocUrl);
  await ensureCookie();
  var seenUrls = {};

  var html = await httpGetHtml(tocUrl, BASE + '/');
  var doc = legado.dom.parse(html);

  // 结构: <a href="..." rel="chapter"><dd>章节名</dd></a>(a 包裹 dd)
  // 页面先有"最新章节"(倒序),再有"全部章节"(正序),需去重并按章节 ID 排序
  var links = legado.dom.selectAll(doc, '#list a');
  var chapters = [];
  for (var i = 0; i < links.length; i++) {
    var href = legado.dom.attr(links[i], 'href') || '';
    if (!CHAPTER_URL_PATTERN.test(href)) continue;
    var chUrl = toAbs(href);
    var chName = (legado.dom.text(links[i]) || '').trim();
    if (chName && chUrl && !seenUrls[chUrl]) {
      seenUrls[chUrl] = 1;
      // 提取章节数字 ID 用于排序(URL 格式 /数字_数字/章节ID.html)
      var idMatch = href.match(/\/(\d+)\.html/);
      var chId = idMatch ? parseInt(idMatch[1]) : i;
      chapters.push({ name: chName, url: chUrl, _id: chId });
    }
  }

  // 按章节 ID 升序排列,保证从第一章到最新章
  chapters.sort(function(a, b) { return a._id - b._id; });
  for (var j = 0; j < chapters.length; j++) {
    delete chapters[j]._id;
  }

  legado.dom.free(doc);
  legado.log('[chapterList] total=' + chapters.length);
  return chapters;
}

// ─── 正文 ─────────────────────────────────────────────────────────────────

async function chapterContent(chapterUrl) {
  legado.log('[content] url=' + chapterUrl);
  await ensureCookie();
  var paragraphs = [];
  var url = chapterUrl;
  var MAX_PAGES = 10;

  for (var p = 0; p < MAX_PAGES; p++) {
    var html = await httpGetHtml(url, BASE + '/');
    var doc = legado.dom.parse(html);

    var contentEl = legado.dom.select(doc, '#booktxt');
    if (!contentEl) {
      legado.dom.free(doc);
      legado.log('[content] #booktxt not found at page ' + (p + 1));
      break;
    }

    var contentHtml = legado.dom.html(contentEl) || '';
    var lines = contentHtml.split(/<br\s*\/?>|<\/?p[^>]*>/i);
    for (var i = 0; i < lines.length; i++) {
      var text = lines[i].replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;[^;]*&gt;/g, '').replace(/lt;[^;]*gt;/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/\s+/g, ' ').trim();
      if (text && !/本章未完|加入书签|章节报错|最近转码严重|退出阅读模式|一秒记住|最快更新|16kbook/.test(text)) {
        paragraphs.push(text);
      }
    }

    // 翻页
    var nextLink = legado.dom.selectByText(doc, '下一页');
    var nextHref = nextLink ? (legado.dom.attr(nextLink, 'href') || '') : '';
    legado.dom.free(doc);

    if (!nextHref || nextHref.indexOf('javascript') !== -1) break;
    var nextUrl = resolveUrl(nextHref, url);
    if (nextUrl === url) break;
    url = nextUrl;
  }

  return paragraphs.join('\n\n');
}

// ─── 发现页 ──────────────────────────────────────────────────────────────

var EXPLORE_CATEGORIES = [
  { name: '排行',     path: '/rank/' },
  { name: '玄幻',     path: '/class/xuanhuan/1/' },
  { name: '武侠',     path: '/class/wuxia/1/' },
  { name: '都市',     path: '/class/dushi/1/' },
  { name: '历史',     path: '/class/lishi/1/' },
  { name: '科幻',     path: '/class/kehuan/1/' },
  { name: '游戏',     path: '/class/youxi/1/' },
  { name: '女生',     path: '/class/nvsheng/1/' },
  { name: '其他',     path: '/class/qita/1/' },
  { name: '全本',     path: '/quanben/class/1/' },
];

async function explore(page, category) {
  var cat = null;
  for (var i = 0; i < EXPLORE_CATEGORIES.length; i++) {
    if (EXPLORE_CATEGORIES[i].name === category) {
      cat = EXPLORE_CATEGORIES[i];
      break;
    }
  }
  if (!cat) return EXPLORE_CATEGORIES.map(function(c) { return c.name; });

  await ensureCookie();
  var url = BASE + cat.path;
  var html = await httpGetHtml(url, BASE + '/');
  var doc = legado.dom.parse(html);
  var books = [];

  var items = legado.dom.selectAll(doc, '.item');
  for (var i = 0; i < items.length; i++) {
    var el = items[i];

    var nameLink = legado.dom.select(el, 'dt a');
    if (!nameLink) continue;
    var bookUrl = legado.dom.attr(nameLink, 'href') || '';
    if (!bookUrl) continue;
    bookUrl = toAbs(bookUrl);

    var name = (legado.dom.text(nameLink) || '').trim();
    if (!name) continue;

    var coverUrl = extractCover(el, 'img');
    var author = (legado.dom.selectText(el, '.btm a') || '').trim();

    books.push({ name: name, author: author, bookUrl: bookUrl, coverUrl: coverUrl });
  }

  legado.dom.free(doc);
  return books;
}
广告