mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-10 21:11:08 +08:00
按产品设计 v0.2 落地服务端,覆盖第一版范围中的扫描、元数据、权限与同步。 核心决策: - 书籍主键用 dc:identifier + 内容哈希,不用文件路径。 重命名/移动不丢进度,副本自动合并,重新编码的文件保持独立。 - 库目录只读挂载,写操作全部落到 DATA_DIR。 启动时断言 DATA_DIR 不在 BOOKS_DIR 内,路径解析统一走 resolveInside 拦截逃逸,扫描不跟随符号链接。 - 元数据优先级:手动修改 > 内嵌 > provider > 文件名解析。 手动修改独立成表,扫描永远碰不到,可单字段撤销。 - 中文文件名解析采取保守策略,无证据时不猜测作者方向。 - 离线合并用 LWW,笔记删除写墓碑,批次单事务。 - 无原生编译依赖:node:sqlite 与 scrypt 均为内置模块。 交付: - Fastify + TypeScript 服务端,Docker 单容器 - 27 个自动化测试用例全部通过 - AGPL-3.0,README 做到一条命令跑起来 - CNB 流水线:typecheck / test / build
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseFilename, detectLanguage } from '../src/indexer/filename.ts';
|
|
|
|
test('parses "title - author" when the author is a recognisable CJK name', () => {
|
|
const r = parseFilename('红楼梦 - 曹雪芹.epub');
|
|
assert.equal(r.title, '红楼梦');
|
|
assert.equal(r.author, '曹雪芹');
|
|
assert.equal(r.language, 'zh');
|
|
});
|
|
|
|
test('parses "author - title" when the author is a recognisable CJK name', () => {
|
|
const r = parseFilename('金庸 - 射雕英雄传.epub');
|
|
assert.equal(r.title, '射雕英雄传');
|
|
assert.equal(r.author, '金庸');
|
|
});
|
|
|
|
test('parses the 《title》author shape', () => {
|
|
const r = parseFilename('《三体》刘慈欣.epub');
|
|
assert.equal(r.title, '三体');
|
|
assert.equal(r.author, '刘慈欣');
|
|
});
|
|
|
|
test('parses the "author - 《title》" shape', () => {
|
|
const r = parseFilename('刘慈欣 - 《三体》.epub');
|
|
assert.equal(r.title, '三体');
|
|
assert.equal(r.author, '刘慈欣');
|
|
});
|
|
|
|
test('leaves a genuinely ambiguous CJK pair unsplit rather than guessing', () => {
|
|
// "基地" is not a personal name and neither side carries a marker, so the
|
|
// correct behaviour is to keep the whole string and report no author.
|
|
const r = parseFilename('基地 - 阿西莫夫.epub');
|
|
assert.equal(r.title, '基地 - 阿西莫夫');
|
|
assert.equal(r.author, '');
|
|
});
|
|
|
|
test('extracts series and index from bracket style', () => {
|
|
const r = parseFilename('[银河帝国 01] 基地.epub');
|
|
assert.equal(r.series, '银河帝国');
|
|
assert.equal(r.seriesIndex, 1);
|
|
assert.equal(r.title, '基地');
|
|
});
|
|
|
|
test('extracts series from 第N卷 style', () => {
|
|
const r = parseFilename('射雕英雄传(第1卷).epub');
|
|
assert.equal(r.series, '射雕英雄传');
|
|
assert.equal(r.seriesIndex, 1);
|
|
assert.equal(r.title, '射雕英雄传');
|
|
});
|
|
|
|
test('keeps author and title apart when a volume marker is present', () => {
|
|
const r = parseFilename('金庸 - 射雕英雄传(第2册).epub');
|
|
assert.equal(r.title, '射雕英雄传');
|
|
assert.equal(r.author, '金庸');
|
|
assert.equal(r.seriesIndex, 2);
|
|
});
|
|
|
|
test('a bracket holding only a number sets the index but not a series name', () => {
|
|
const r = parseFilename('[01] 基地.epub');
|
|
assert.equal(r.series, '');
|
|
assert.equal(r.seriesIndex, 1);
|
|
assert.equal(r.title, '基地');
|
|
});
|
|
|
|
test('parses trailing parenthesised author', () => {
|
|
const r = parseFilename('活着 (余华).epub');
|
|
assert.equal(r.title, '活着');
|
|
assert.equal(r.author, '余华');
|
|
});
|
|
|
|
test('strips download-site noise', () => {
|
|
const r = parseFilename('红楼梦 - 曹雪芹 (z-lib.org).epub');
|
|
assert.equal(r.title, '红楼梦');
|
|
assert.equal(r.author, '曹雪芹');
|
|
assert.ok(!r.title.includes('z-lib'));
|
|
});
|
|
|
|
test('keeps latin titles intact', () => {
|
|
const r = parseFilename('The Pragmatic Programmer - Hunt, Andrew.epub');
|
|
assert.equal(r.title, 'The Pragmatic Programmer');
|
|
assert.equal(r.author, 'Hunt, Andrew');
|
|
assert.equal(r.language, 'en');
|
|
});
|
|
|
|
test('falls back to the raw filename when no separator exists', () => {
|
|
const r = parseFilename('深入理解计算机系统.epub');
|
|
assert.equal(r.title, '深入理解计算机系统');
|
|
assert.equal(r.author, '');
|
|
});
|
|
|
|
test('detectLanguage distinguishes ja from zh', () => {
|
|
assert.equal(detectLanguage('ノルウェイの森'), 'ja');
|
|
assert.equal(detectLanguage('挪威的森林'), 'zh');
|
|
assert.equal(detectLanguage('Norwegian Wood'), 'en');
|
|
});
|