const { fetchText, fetchJson, clampPage, decodeEntities, stripTags } = require('./http');
const BASE = 'https://standardebooks.org';
const PAGE_SIZE = 24;
const DETAIL_TTL = 5 * 60 * 1000;
const detailCache = new Map();
function absolute(url) {
return url ? new URL(url, BASE).toString() : '';
}
function slugFromUrl(url) {
return String(url || '').replace(/^https?:\/\/standardebooks\.org\/ebooks\//, '').replace(/^\/?ebooks\//, '').replace(/^\/+|\/+$/g, '');
}
function postId(slug) { return encodeURIComponent(slug); }
function parseCatalog(html) {
const items = [];
const re = /
)/g;
let m;
while ((m = re.exec(html))) {
const slug = slugFromUrl(m[1]);
const block = m[2];
const title = stripTags((block.match(/property="schema:name">([\s\S]*?)<\/span>/) || [])[1] || '');
const author = stripTags((block.match(/class="author"[^>]*>([\s\S]*?)<\/p>/) || [])[1] || '');
const cover = (block.match(/
]*property="schema:image"[^>]*src="([^"]+)"/) || [])[1]
|| (block.match(/
]*src="([^"]+)"[^>]*property="schema:image"/) || [])[1] || '';
if (!slug || !title) continue;
items.push({
postId: postId(slug),
title,
cover: absolute(decodeEntities(cover)),
date: '',
url: `${BASE}/ebooks/${slug}`,
subtitle: author
});
}
return items;
}
function catalogMaxPage(html, page) {
const pages = Array.from(html.matchAll(/[?&]page=(\d+)/g)).map((m) => parseInt(m[1], 10));
return Math.max(page, ...pages.filter(Number.isFinite));
}
function publicationToItem(p) {
const metadata = p.metadata || {};
const slug = slugFromUrl(metadata.identifier);
// OPDS 的 author 可能是字符串、对象或两者混排的数组
const raw = Array.isArray(metadata.author) ? metadata.author : (metadata.author ? [metadata.author] : []);
const authors = raw.map((a) => (typeof a === 'string' ? a : (a && a.name) || '')).filter(Boolean);
const image = (p.images || []).find((x) => x && x.href);
return {
postId: postId(slug),
title: metadata.title || '(无标题)',
cover: image ? absolute(image.href) : '',
date: String(metadata.published || '').slice(0, 10),
url: `${BASE}/ebooks/${slug}`,
subtitle: authors.join(', ')
};
}
function parseDetail(html, slug) {
const title = stripTags((html.match(/