Files
peoplelib/src/sources/openstax.js
T
lofyer c5b9072ffd
构建与发布 / 单测与集成测试 (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
构建与发布 / 发布 GitHub Release (push) Has been cancelled
feat: 发布 v2.1.0 开放书源扩展
2026-08-06 00:36:20 +08:00

181 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { fetchJson, clampPage, stripTags } = require('./http');
const BASE = 'https://openstax.org';
const CATALOG_URL = `${BASE}/apps/cms/api/books`;
const DETAIL_BASE = `${BASE}/apps/cms/api/v2/pages`;
const PAGE_SIZE = 20;
const CATALOG_TTL = 30 * 60 * 1000;
let catalogPromise = null;
let catalogAt = 0;
function getJson(url) {
return fetchJson(url);
}
async function loadCatalog() {
const now = Date.now();
if (catalogPromise && now - catalogAt < CATALOG_TTL) return catalogPromise;
const promise = getJson(CATALOG_URL).then((j) => {
if (!j || !Array.isArray(j.books)) throw new Error('OpenStax 返回了无法识别的书目');
return j.books.filter((b) => b && b.id && b.book_state === 'live');
});
catalogPromise = promise;
catalogAt = now;
try {
return await promise;
} catch (e) {
if (catalogPromise === promise) {
catalogPromise = null;
catalogAt = 0;
}
throw e;
}
}
function validateId(postId) {
const id = String(postId || '');
if (!/^\d+$/.test(id)) throw new Error('无效的 OpenStax ID');
return id;
}
function pageUrl(book) {
const slug = String(book && book.slug || '').replace(/^\/+/, '');
return slug ? `${BASE}/details/${slug}` : `${BASE}/subjects`;
}
function toItem(book) {
return {
postId: String(book.id),
title: book.title || '(无标题)',
cover: book.cover_url || '',
date: '',
url: pageUrl(book),
subtitle: (book.subjects || []).slice(0, 3).join(' · ')
};
}
function pack(books, page) {
const start = (page - 1) * PAGE_SIZE;
return {
items: books.slice(start, start + PAGE_SIZE).map(toItem),
maxPage: Math.max(1, Math.ceil(books.length / PAGE_SIZE)),
page
};
}
function matches(book, keyword) {
const tokens = String(keyword || '').trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
if (!tokens.length) return true;
const text = [
book.title,
...(book.subjects || []),
...(book.subject_categories || [])
].filter(Boolean).join(' ').toLocaleLowerCase();
return tokens.every((token) => text.includes(token));
}
function authorsOf(book) {
return (book.authors || []).map((author) => {
if (typeof author === 'string') return author;
return author && author.value && author.value.name
? author.value.name
: (author && author.name) || '';
}).filter(Boolean);
}
function subjectNames(value) {
const items = Array.isArray(value) ? value : (value ? [value] : []);
return items.map((item) => (
typeof item === 'string' ? item : (item && (item.subject_name || item.name)) || ''
)).filter(Boolean);
}
async function bookDetail(postId) {
const id = validateId(postId);
const book = await getJson(`${DETAIL_BASE}/${id}/`);
if (!book || !book.id) throw new Error('未找到该 OpenStax 教材');
return book;
}
function detailUrl(book) {
return (book.meta && book.meta.html_url) || `${BASE}/details/books/${book.meta && book.meta.slug || book.id}`;
}
function licenseLabel(book) {
return [book.license_name, book.license_version].filter(Boolean).join(' ');
}
function safeName(title) {
return String(title || 'OpenStax 教材').replace(/[\\/:*?"<>|]/g, '_').slice(0, 80);
}
module.exports = {
id: 'openstax',
name: 'OpenStax 开放教材',
supportsSearch: true,
async list(page) {
page = clampPage(page);
return pack(await loadCatalog(), page);
},
async search(keyword, page) {
page = clampPage(page);
const books = (await loadCatalog()).filter((book) => matches(book, keyword));
return pack(books, page);
},
async detail(postId) {
const book = await bookDetail(postId);
const subjects = [
...subjectNames(book.book_subjects),
...subjectNames(book.book_categories)
];
return {
postId: String(book.id),
title: book.title || '(无标题)',
cover: book.cover_url || '',
authors: authorsOf(book),
date: String(book.publish_date || '').slice(0, 10),
tags: [
...subjects.slice(0, 4).map((subject) => `主题:${subject}`),
licenseLabel(book) ? `许可:${licenseLabel(book)}` : '',
book.digital_isbn_13 ? `ISBN${book.digital_isbn_13}` : ''
].filter(Boolean),
brief: stripTags(book.description || ''),
url: detailUrl(book),
links: [
{ name: 'OpenStax 页', url: detailUrl(book) },
...(book.webview_link || book.webview_rex_link
? [{ name: '在线阅读', url: book.webview_link || book.webview_rex_link }]
: [])
]
};
},
async download(postId) {
const book = await bookDetail(postId);
const files = [];
const seen = new Set();
for (const [url, label] of [
[book.pdf_url, 'PDF'],
[book.high_resolution_pdf_url, '高清 PDF']
]) {
if (!url || seen.has(url)) continue;
seen.add(url);
files.push({
name: `${safeName(book.title)}${label === '高清 PDF' ? '-高清' : ''}.pdf`,
link: url,
format: 'PDF'
});
}
const links = [{ name: 'OpenStax 页', url: detailUrl(book) }];
if (book.webview_link || book.webview_rex_link) {
links.push({ name: '在线阅读', url: book.webview_link || book.webview_rex_link });
}
if (book.license_url) links.push({ name: '许可说明', url: book.license_url });
return { files, links };
}
};