Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

136 lines
5.9 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
import {
callGateway,
emptyResult,
formatDate,
formatDuration,
formatRating,
makeDeepLink,
requireBookId,
truncate,
} from './utils.js';
/**
* 3-in-1 book lookup: `/book/info` always runs; `/book/chapterinfo` and
* `/book/getprogress` are opt-out via `--no-chapters` / `--no-progress`
* so users can shave 1-2 gateway calls when only a single section is needed.
*
* Output is a flat row list with a `section` column so table/json/yaml
* consumers can filter without ad-hoc joins:
* - section=info — single row, basic metadata
* - section=chapter — one row per chapter (level/title/wordCount)
* - section=progress — single row, progress % + cumulative read time
*/
cli({
site: 'weread-official',
name: 'book',
access: 'read',
description: 'Show WeRead book metadata, chapters, and reading progress',
domain: 'weread.qq.com',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'bookId', positional: true, required: true, help: 'WeRead bookId (from `weread-official search`)' },
{ name: 'no-chapters', type: 'boolean', default: false, help: 'Skip /book/chapterinfo call' },
{ name: 'no-progress', type: 'boolean', default: false, help: 'Skip /book/getprogress call' },
],
columns: ['section', 'idx', 'key', 'value', 'link'],
func: async (args) => {
const bookId = requireBookId(args.bookId);
const tasks = [callGateway('/book/info', { bookId })];
const want = {
chapters: !args['no-chapters'],
progress: !args['no-progress'],
};
if (want.chapters) tasks.push(callGateway('/book/chapterinfo', { bookId }));
if (want.progress) tasks.push(callGateway('/book/getprogress', { bookId }));
const results = await Promise.all(tasks);
let cursor = 0;
const info = results[cursor++];
const chapters = want.chapters ? results[cursor++] : null;
const progress = want.progress ? results[cursor++] : null;
const rows = [];
// ── info section ────────────────────────────────────────────────────
const infoPairs = [
['bookId', String(info?.bookId ?? bookId)],
['title', String(info?.title ?? '')],
['author', String(info?.author ?? '')],
['translator', String(info?.translator ?? '')],
['category', String(info?.category ?? '')],
['publisher', String(info?.publisher ?? '')],
['publishTime', String(info?.publishTime ?? '')],
['isbn', String(info?.isbn ?? '')],
['wordCount', String(info?.wordCount ?? '')],
['rating', formatRating(info?.newRating)],
['ratingCount', String(info?.newRatingCount ?? '')],
['intro', truncate(info?.intro, 400)],
['cover', String(info?.cover ?? '')],
];
for (let i = 0; i < infoPairs.length; i += 1) {
const [key, value] = infoPairs[i];
rows.push({ section: 'info', idx: i + 1, key, value, link: '' });
}
rows.push({
section: 'info',
idx: infoPairs.length + 1,
key: 'link',
value: '',
link: makeDeepLink({ bookId }),
});
// ── chapters section ────────────────────────────────────────────────
if (chapters) {
const list = Array.isArray(chapters?.chapters) ? chapters.chapters : [];
list.forEach((ch, i) => {
const chapterUid = String(ch?.chapterUid ?? '').trim();
const level = Number(ch?.level ?? 1);
const indent = ' '.repeat(Math.max(0, level - 1));
const title = `${indent}${String(ch?.title ?? '')}`;
const wordCount = Number(ch?.wordCount ?? 0);
const paid = Number(ch?.paid ?? 0) === 1;
const price = Number(ch?.price ?? 0);
const meta = [`${wordCount}字`];
if (price > 0) meta.push(paid ? '已购买' : `${price}元`);
rows.push({
section: 'chapter',
idx: Number(ch?.chapterIdx ?? i + 1),
key: chapterUid,
value: `${title} (${meta.join(' · ')})`,
link: chapterUid ? makeDeepLink({ bookId, chapterUid }) : '',
});
});
}
// ── progress section ────────────────────────────────────────────────
if (progress) {
const p = progress?.book ?? {};
const pct = Number(p?.progress ?? 0);
const updateTime = formatDate(p?.updateTime);
const finishTime = pct === 100 ? formatDate(p?.finishTime) : '';
const cumulative = formatDuration(p?.recordReadingTime);
const isStart = Number(p?.isStartReading ?? 0) === 1;
const progressPairs = [
['progress', `${pct}%`],
['cumulative', cumulative],
['lastReadAt', updateTime],
['finishedAt', finishTime],
['isStartReading', isStart ? 'true' : 'false'],
['currentChapterUid', String(p?.chapterUid ?? '')],
];
for (let i = 0; i < progressPairs.length; i += 1) {
const [key, value] = progressPairs[i];
rows.push({ section: 'progress', idx: i + 1, key, value, link: '' });
}
}
if (rows.length === 0) {
emptyResult('book', `No data returned for bookId=${bookId}`);
}
return rows;
},
});