Files
reader/server/test/filename.test.ts
T
cnb.dQC5J6ohAGA 2233b295bb feat: 初始化自部署书库服务端
按产品设计 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
2026-09-16 14:07:25 +08:00

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');
});