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); const authors = Array.isArray(metadata.author) ? metadata.author : (metadata.author ? [metadata.author] : []); 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.map((a) => a.name || '').filter(Boolean).join(', ') }; } function parseDetail(html, slug) { const title = stripTags((html.match(/]*property="schema:name"[^>]*>([\s\S]*?)<\/h1>/) || [])[1] || ''); const authorBlock = (html.match(/]*property="schema:author"[^>]*>([\s\S]*?)<\/a>/) || [])[1] || ''; const author = stripTags((authorBlock.match(/property="schema:name"[^>]*>([\s\S]*?)<\/span>/) || [])[1] || authorBlock); const brief = decodeEntities((html.match(/]*property="schema:description"[^>]*content="([^"]*)"/) || [])[1] || ''); const cover = (html.match(/]*property="schema:image"[^>]*content="([^"]+)"/) || [])[1] || ''; const date = (html.match(/]*property="schema:datePublished"[^>]*content="([^"]+)"/) || [])[1] || ''; const epub = (html.match(/]*property="schema:contentUrl"[^>]*href="([^"]+)"[^>]*class="epub"/) || [])[1] || ''; return { slug, title, author, brief, cover: absolute(cover), date, epub: absolute(epub) }; } function validateSlug(post) { const slug = decodeURIComponent(post); if (!/^[a-z0-9-]+(?:\/[a-z0-9-]+)+$/i.test(slug)) throw new Error('无效的 Standard Ebooks ID'); return slug; } async function loadDetail(post) { const slug = validateSlug(post); const cached = detailCache.get(slug); if (cached && cached.expiresAt > Date.now()) return cached.promise; const promise = fetchText(`${BASE}/ebooks/${slug}`).then((html) => { const detail = parseDetail(html, slug); if (!detail.title) throw new Error('未找到该图书'); return detail; }); detailCache.set(slug, { promise, expiresAt: Date.now() + DETAIL_TTL }); while (detailCache.size > 50) detailCache.delete(detailCache.keys().next().value); try { return await promise; } catch (e) { detailCache.delete(slug); throw e; } } module.exports = { id: 'standardebooks', name: 'Standard Ebooks', supportsSearch: true, async list(page) { page = clampPage(page); const html = await fetchText(`${BASE}/ebooks?page=${page}&per-page=${PAGE_SIZE}&view=list`); return { items: parseCatalog(html), maxPage: catalogMaxPage(html, page), page }; }, async search(keyword, page) { page = clampPage(page); const j = await fetchJson(`${BASE}/feeds/opds/all?query=${encodeURIComponent(keyword)}&per-page=${PAGE_SIZE}&page=${page}`, { headers: { 'Accept': 'application/opds+json' } }); const publications = j.publications || []; return { items: publications.map(publicationToItem).filter((x) => decodeURIComponent(x.postId)), maxPage: publications.length === PAGE_SIZE ? page + 1 : page, page }; }, async detail(postId) { const e = await loadDetail(postId); return { postId, title: e.title, cover: e.cover, authors: e.author ? [e.author] : [], date: e.date, tags: [], brief: e.brief, url: `${BASE}/ebooks/${e.slug}`, links: [{ name: 'Standard Ebooks 页', url: `${BASE}/ebooks/${e.slug}` }] }; }, async download(postId) { const e = await loadDetail(postId); if (!e || !e.epub) throw new Error('未找到 EPUB 下载'); const url = new URL(e.epub); url.searchParams.set('source', 'download'); return { files: [{ name: `${e.title.replace(/[\\/:*?"<>|]/g, '_').slice(0, 60)}.epub`, link: url.toString(), format: 'EPUB' }], links: [] }; } };