docs: split bilingual readme and getting started guides

This commit is contained in:
hectorqin committed 2026-09-22 17:12:49 +08:00
1 parent 4af639d3b8
commit 5772014d3f
10 files changed
+864 -660

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
# reader
**One reading space for your local books, OPDS catalogs, and remote book sources.**
A self-hosted library and reader for browsers and Android, with EPUB typography, comic reading, per-user sync, and extensible sources.
[简体中文](README.md) · **English**
[Get started](docs/getting-started.en.md) · [User guide (中文)](docs/user-guide.zh-CN.md) · [Configuration](docs/configuration.en.md) · [Documentation](docs/README.md)
## Why reader
- **Your own library**: mount an existing book directory without reorganizing it first. Covers, indexes, and server state live in a separate data directory.
- **Built for reading**: preserve publisher styles in EPUBs and detect encodings and chapters in TXT files. Choose paged or scrolling reading, themes, fonts, and text-to-speech.
- **Multiple sources**: local libraries and OPDS are built in; independent plugins add remote book sources.
- **Continue across devices**: each account has its own shelf, progress, notes, and highlights. Browsers and Android connect to the same server.
- **Simple hosting**: one Docker container serves both the backend and Web UI. Mount your books read-only or writable according to your needs.
## Books and sources
| Type | Support |
| --- | --- |
| EPUB | Publisher styles, embedded fonts, table of contents, footnotes, and inline images |
| TXT | Encoding detection, inferred chapters, adjustable font size and line spacing |
| CBZ / ZIP / image directories | Natural page ordering, comic paging, and volumes |
| PDF | Basic viewing through the browser or device viewer, without reflow |
| OPDS | Browsing, search, pagination, and book acquisition |
| Remote source plugins | Multi-source search, chapter reading, rich text and images, catalog refresh, and automatic update tracking |
## Get started
Prepare Docker, Docker Compose, and a directory of books. After starting the service, open `http://<your-host>:8080`. The first registered account becomes the administrator.
**[Open the getting started guide →](docs/getting-started.en.md)**
The guide covers Compose setup, first login, adding books, read-only and writable mounts, and upgrades. The database, caches, and upload staging live in `DATA_DIR`, which must be outside `BOOKS_DIR` and must not be the same directory.
## Documentation
| I want to… | Start here |
| --- | --- |
| Install reader and open my first book | [Getting started](docs/getting-started.en.md) |
| Use shelves, the reader, sources, and text-to-speech | [User guide (中文)](docs/user-guide.zh-CN.md) |
| Configure environment variables, HTTPS, TTS, or troubleshoot deployment | [Configuration](docs/configuration.en.md) |
| Run from source, test changes, or build Android | [Development](docs/development.en.md) |
| Build a source plugin or use the API | [Documentation index](docs/README.md) |
## Current limits
- PDF support is basic viewing; `.rar/.cbr` and general format conversion are not supported.
- Offline access depends on cached content. Whole-book prefetch and cache quotas for remote chapters are not implemented.
- Clients are available for Web and Android. There are no standalone iOS or desktop clients; desktop users can use a browser.
## Contributing
Bug reports, documentation improvements, and code contributions are welcome. Include reproduction steps, your environment, and redacted logs. See the [development guide](docs/development.en.md) for the workflow and checks.
## License
[GNU Affero General Public License v3.0](LICENSE).
+37 -660
View File
@@ -1,683 +1,60 @@
# reader
**自部署精品书库阅读器。** 把你自己磁盘上的书籍挂载成书库,用 Android 或浏览器以「精品级排版」阅读。
**把本地书、OPDS 书库和远程书源,放进自己的阅读空间。**
书文件始终留在你自己的磁盘上,不经过任何厂商服务器。开源免费,AGPL-3.0。
支持浏览器与 Android 的自部署书库阅读器,提供 EPUB 排版、漫画阅读、多用户同步和可扩展书源。
三个部分:
**简体中文** · [English](README.en.md)
| 部分 | 位置 | 说明 |
| --- | --- | --- |
| 服务端 | `server/` | Node.js + TypeScript,单容器,扫描书库并同步进度 |
| 渲染层 | `web/` | TypeScript + Vite,EPUB 精排、TXT、漫画、PDF |
[开始使用](docs/getting-started.zh-CN.md) · [使用手册](docs/user-guide.zh-CN.md) · [部署配置](docs/configuration.zh-CN.md) · [文档中心](docs/README.md)
---
## 为什么使用 reader
## 一条命令跑起来
- **自己的书库**:挂载现有书籍目录,无需先把文件搬进另一套目录结构。封面、索引和服务端状态单独保存在数据目录。
- **专注阅读**:EPUB 保留出版方样式,TXT 支持编码识别与章节切分;支持分页、滚动、主题、字体设置和朗读。
- **多种来源**:内置本地书库和 OPDS,通过独立插件接入远程书源。
- **跨设备继续**:每个账号拥有独立书架、阅读进度、笔记和高亮;浏览器与 Android 连接同一服务。
- **适合自部署**:一个 Docker 容器包含服务端和 Web 界面,书库可按需要选择只读或可写挂载。
```bash
git clone https://cnb.cool/hectorqin/reader.git
cd reader
## 书籍与来源
# 把 /path/to/your/books 换成你自己的书库目录
sed -i 's#/path/to/your/books#/home/me/books#' docker-compose.yml
docker compose up -d
```
打开 `http://<你的主机>:8080`,**第一个注册的账号自动成为管理员**。
这时你已经有一个能用的 H5 阅读器了——服务端镜像里已经带了客户端,不用再起第二个容器。
手机浏览器打开同一个地址即可阅读。要原生 App 见下面的「Android 客户端」。
> 只有第一次注册是开放的。之后新账号由管理员在后台创建(默认关闭公开注册),
> 这样你的实例不会被陌生人占用。想让家人自行注册,在 `docker-compose.yml` 里
> 把 `ALLOW_REGISTRATION` 设为 `"true"`。
### 最小化运行(不用 compose)
```bash
docker run -d --name reader \
-p 8080:8080 \
-v /path/to/your/books:/books:ro \
-v ./data:/data \
cnb.cool/hectorqin/reader:latest
```
---
## 书库目录与数据目录
```yaml
volumes:
- /path/to/your/books:/books:ro # 只读部署;需要网页上传/文件管理时改为 :rw
- ./data:/data # 数据库、缓存、扫描记录和插件数据
```
`BOOKS_DIR` 是书籍文件所在目录。服务端不会在书旁边写封面、`.calibre`、转换产物或其它元数据文件;封面缓存、SQLite、扫描记录、上传暂存和插件数据都写入 `/data`。
管理员文件管理器在挂载可写时可以上传、改名、移动、删除和新建目录;只读挂载时这些入口隐藏或返回 `403 READ_ONLY_MOUNT`。
硬约束是 `DATA_DIR` 不能位于 `BOOKS_DIR` 内,代码会在启动时拒绝这种配置,避免服务自己的写入污染书库挂载。
---
## 支持格式
| 格式 | 状态 | 说明 |
| --- | --- | --- |
| EPUB | 精排 | 保留出版方样式、嵌入字体、脚注跳转、图文混排、竖排、振假名 |
| TXT | 支持 | 自动识别 UTF-8 / GB18030 / Big5 / UTF-16,按章节标题切分 |
| CBZ / ZIP | 支持 | 按页自然排序,逐页铺满屏幕 |
| 图片目录 | 支持 | 一个文件夹(可含卷子目录与卷压缩包)当一本书,逐页取图 |
| PDF | 保底可读 | 用浏览器自带阅读器,页数与进度照常上报,服务端不解析内容 |
> TXT 是本地书库里最容易被忽视的一块。相当一部分中文 TXT 没有 BOM、没有编码声明,
> 解码成 UTF-8 会满屏乱码。客户端先严格校验 UTF-8;失败时**不是**按顺序试
> GB18030 再试 Big5,而是两种都解一遍挑更干净的那个——这两个编码的字节范围重叠,
> 同一串字节往往两边都能解出字符,按顺序试就永远走不到第二个,繁体书会整本变乱码。
> 选中的编码会**显示在界面上**,可手动覆盖。
> 章节同样没有结构,只能从标题推断——推测不出时按长度分段,不会整本卡死。
---
## 阅读界面
界面设置按**设备**保存,不跟着账号同步:字号和主题是「手里这块屏幕」的属性,
把手机的字号推到平板上比不同步更糟。阅读进度和笔记才跟账号走。
### 排版
| 设置 | 说明 |
| 类型 | 支持情况 |
| --- | --- |
| 翻页方式 | 滚动(保留作者连续排版)/ 翻页(CSS 多列分页,一页一屏) |
| 字号 | 0.8×–2.2×,只改基准字号 |
| 行距 | 默认「原书」。选具体值才覆盖,否则一行都不动 |
| 页边距 | 0–4rem,控制每行字数 |
| 对齐 | 默认「原书」,可强制左对齐 / 两端对齐 |
| 字体 | 默认「原书」。可选系统黑体 / 宋体 / 楷体 / 苹方,作为字体栈的**前缀**而不是替换 |
| 主题 | 白 / 米黄 / 浅绿 / 夜间 |
| 亮度 | 35%–100%,叠加式调暗,文字与纸色的对比关系不变 |
| 图片适配 | 仅固定版式(漫画、PDF):完整显示 / 适宽 |
| 翻页方向 | 仅固定版式:左→右 / 右→左(日漫) |
| TXT 编码 | 仅 TXT:自动 / UTF-8 / GB18030 / Big5 / UTF-16LE |
| EPUB | 出版方样式、嵌入字体、目录、脚注、图文混排 |
| TXT | 编码识别、章节推断、字号与行距调整 |
| CBZ / ZIP / 图片目录 | 自然页序、漫画分页和卷目录 |
| PDF | 基础查看,能力取决于浏览器或设备查看器,不重排 |
| OPDS | 浏览、搜索、分页和获取书籍 |
| 远程书源插件 | 多源搜索、章节阅读、富文本与图片、刷新目录和自动追更 |
一条原则贯穿全部:**每个排版开关的默认值都是「不动」**,不是「我们的默认值」。
「原书」不是营销话术而是实现——行距的默认值是 `inherit`,页边距和字号挂在容器的
继承变量上,所以一本按章节变行距的书不会被压平,一本给段落写死 `font-size: 14px`
的书也不会被字号滑块踩掉。这与「所有书看起来一致」的 Readium 路线正好相反。
## 开始使用
### 界面与图标
准备 Docker、Docker Compose 和一个书籍目录,即可部署。服务启动后访问 `http://<你的主机>:8080`,首个注册账号成为管理员。
阅读页以手机浏览器为优先:顶部显示返回、书名和章节,底部集中目录、朗读、界面、设置。
工具栏和半屏面板覆盖正文,显隐不改变正文区域尺寸。界面设置可分别选择顶部左/右、底部左/右显示书名、章节名、章节进度、时间或不显示;默认顶部左侧章节名、底部左侧章内页码与全书百分比、底部右侧时间,按设备保存。
底栏上方保留一个日夜切换按钮,随工具栏一起收起。
**[打开入门指南 →](docs/getting-started.zh-CN.md)**
图标使用 Lucide SVG,通过统一的 Icon 组件控制尺寸、线宽和无障碍标签,不再加载字体图标。
界面规范见 [docs/ui.md](docs/ui.md),手机回归可运行 `cd web && npm run ui:review`。
指南包含 Compose 配置、首次登录、添加书籍、只读/可写挂载和更新流程。数据库、缓存和上传暂存均写入 `DATA_DIR`;该目录不能与 `BOOKS_DIR` 相同或位于其内部。
### 翻页与章节导航
## 文档
- **点击区域**:中间呼出/隐藏工具栏,左右翻页;可切换「左进右退」适配左右手。
- **上一章 / 下一章**:页脚中间的翻页按钮直接换章,不用先滚到章末。
到达书的第一章 / 最后一章时按钮置灰。跨窗口换章(一千二百章的书按 40 章一窗)
会先取那一窗再落章,页脚在取窗期间显示「正在切换…」。
- **页脚报位置**:`第 100/120 章 · 本章 1/3 页 · 40%`。章内页码与进度条同源、
同一次量测,所以两者不会互相矛盾。
- **目录即跳转**:点任意一章直接跳过去,不在当前窗口的会先拉取那一窗。
跳转失败的章会明说「这一章暂时无法跳转」,不会静默无反应。
- **翻页动画**:滑动 / 淡入 / 无。系统开启「减少动态效果」时自动不播。
- **硬件翻页键**:`←` / `→`、`PageUp` / `PageDown`,空格播放/暂停朗读。
Android 外壳把音量键映射成方向键,所以音量键翻页、蓝牙翻页器都走同一条路。
### 朗读(TTS)
三种引擎,一套控制。用哪一个是 `auto` 决定的,也可以手动指定:
| 引擎 | 声音来自 | 什么时候用 |
| --- | --- | --- |
| 系统语音(原生) | Android 外壳的 `TextToSpeech` | Android 客户端默认 |
| HTTP 朗读 | 服务端 `TTS_URL` 指向的合成服务 | 前两者不可用时 |
**HTTP 朗读**:给没有 `speechSynthesis` 的浏览器(比如 Linux 上的 Firefox)、
或者没有中文语音的设备用。服务端只做代理,不内置合成模型——容器体积和 NAS 的 CPU 预算
都不该为它买单。在 `docker-compose.yml` 里加:
```yaml
environment:
TTS_URL: "http://host.docker.internal:5002/tts" # edge-tts / Piper 等兼容服务
# TTS_TOKEN: "..." # 需要鉴权的上游
# TTS_VOICES_URL: "..." # 语音列表;默认 <TTS_URL>/voices
# TTS_CACHE_BYTES: "268435456" # 音频磁盘缓存上限,0 关闭
```
**共同行为**:
- **按句切分**:中文按 `。!?;…`,西文按 `. ! ?` 且避开 `Mr.` / `3.14` / `e.g.`。
逐句朗读才能做到朗读位置跟随显示高亮,也能让「暂停/继续」和「改语速」都只影响下一句。
- **从当前位置开始**:默认从句首读起;若先选中一段文字再按朗读,就从选中处开始。
- **句子级控制**:上一句 / 下一句 / 进度条直接跳句。
- **语速、音调、音量**:语速 0.5×–2.5×,改动即时生效(不重读整段,只重读当前句)。
音调只对系统/原生引擎显示——合成音频没有不改变语速的音调控制,所以那一行直接不出现。
- **语音可选**:列出当前引擎所有可用语音(含语言标签),默认「跟随系统」。
- **章节播完**:可选「继续下一章」或「停在章末」——后者就是有声书的「一次一章」。
- **高亮**:正在朗读的句子用半透明底色标出,并用 `Range` 绘制,**不改写书的 DOM**,
所以开始朗读不会让章节重新分页。
> 一个引擎都没有时,设置面板会直接说明原因和解决办法,而不是给一个按了没反应的按钮。
### 首页(书架)
- **先看到书,再看到控件**:标题和封面在最上面,搜索在后,排序与数量贴在它们要作用的
那一节上。旧版把搜索框、数量、四个排序 chip 依次排在封面之前——三行控件才轮到第一本书。
- **排序栏是唯一吸顶的东西**:它承载的是「有多少本」和「按什么排」这两件读者会滚回去改的事;
标题吸顶只会用永远占掉手机 4rem 高度去说一件读者已经知道的事。
- **空状态给一条出路**:空书库不是「这里什么都没有」,而是「打开书库 / 刷新」两个按钮;
搜不到结果时给的是「清除搜索」。空态居中的是那块留白,不是文字。
- **骨架屏**:冷启动时按当前密度画出网格的形状,而不是先白屏再弹出一整屏封面——
密度不对的骨架屏比没有更糟,读者会看到布局动两次。
- **宽屏是更大的封面,不是更多的封面**:内容列封顶 68rem,封面随断点变大。
不然 1600px 的窗口会排出十一个 130px 的封面和十二个字的书名,比手机上的三列更难扫。
- **深链与返回**:`#/book/<id>` 直接打开那本书,刷新后还在;`#/library/<路径>` 指向
书库里的一个目录,可以发给别人。返回键走应用自己的屏幕轨迹。
- **排序**:**最近阅读(默认)** / 最近入库 / 书名 / 作者,书架上一键切换,并记住选择。
两个「最近」问的是两个不同的时间——**上次读**和**进库**——而不是同一个「文件改动时间」。
中文书名按数字大小排(`第9章` 在 `第10章` 前),不是按字符码。
- **密度**:紧凑 / 适中 / 宽松,封面一行放几个。手机上从一行三个到一行两个。
- **显示**:可关掉作者、可关掉封面上的进度条。书架安静下来之后更像书库,不像仪表盘。
- **封面进度条**:读过的书在封面底部画一条,扫一眼就知道哪本在读。
书架顶部**没有**单独的「继续阅读」横排:那一条画的是下面网格里已经有的十本,
而「我读到哪了」现在由**顺序**回答——默认按阅读时间排,读过的自然在前面。
- **搜索**:输入即搜(防抖 250ms),带一键清空;回车立刻搜不等防抖。
- **下拉刷新**:在顶部再往下拉一次,手动重新拉取(刚扫完目录时用)。
- **书库**:书架右上角的 ▮▮ 打开,看的是 `BOOKS_DIR` 本身而不是目录库。
它有两个页面:**预览**(默认,这个文件夹里的书,可以点开读、可以把不在书架上的
一键加回书架)和**文件**(文件管理器)。
- **离线优先**:先画本地镜像再换成服务端数据,离线时书架照常可用;
离线下的排序规则与服务端一致,所以断网不会让书架重排。
### 书库
书架回答「我能读什么」,书库回答「磁盘上有什么」。两件事都需要,因为书籍记录里
**从不带路径**——「刚扫完目录,可为什么书架上没有这本书」这个问题,在书库之外
没有任何地方能回答。
书架右上角的 ▮▮ 打开。它是**一个屏幕、两个页面**:
- **预览**:这个文件夹里的**书**,用的是书架的网格。不在你书架上的那几本,
封面角上有一个「加入书架」,按一下就把那个**文件**对应的书加回来——
这就是上面那个问题的一条出路,而且是一步。
- **文件**:下面那套文件管理器。
两页共用面包屑、位置和 URL(`#/library/files/<路径>`),所以来回切是**标签**,
Back 走的是「离开书库」而不是「切回上一个页面」。
#### 文件页
- **列的是挂载点本身**,不是整理后的目录视图。面包屑可点,路径就是你 `BOOKS_DIR` 里的路径。
- **被扫描忽略的条目照样显示**,只打一个「扫描忽略」标记。`.trash`、`@eaDir` 是设计上
不扫的;在界面上也把它们藏起来,就正好藏掉了读者正在找的那批文件。
- **长按进多选**,多选后才出现改名 / 移动到… / 删除。点一行是浏览,不是选择:
在一个上万文件的 NAS 书库里滚动,每行都挂一个删除按钮,离误删只有一次手滑。
- **删除要再确认一次**,且会说明「不进回收站、需要重新扫描」。
- **只读挂载上隐藏文件写操作按钮**。示例使用 `:ro`;需要管理员上传和文件管理时,
改用 `:rw`,并确保服务端运行用户拥有目标目录的写权限。
- **上传**:直接往当前目录传书。`.zip` / `.cbz` 当合集解开(单层包裹目录抹平,更深的树保留,
因为漫画的卷结构就是深层目录)。重名可选保留两份 / 跳过 / 覆盖 / 整批拒绝。
- **批量管理**:长按多选后可以一次改一批的元数据、一起从书架拿掉、一起移动。
改元数据和「从书架拿掉」都不动磁盘——`remove` 只是 `hidden = 1`,书还在索引里,放得回去。
文件写操作遵循以下规则:
- **改名和移动使用 `rename`**。同挂载点改名不动字节、不动 `mtime`,扫描器视为未变化,
书籍主键(`identifier + 内容哈希`)因此不变——**进度和笔记都还在**。
索引同时被指向新路径,不用等下一次扫描。
- **先探测挂载是否可写**(真建一个临时目录再删),再动手。`drwxr-xr-x` 的只读 bind mount
看起来可写,写起来 `EPERM`;探测一次比从 `EROFS` 栈里推断便宜得多。
> 敢开这个页面,就等于接受「能改书库的人能改书库里的任何文件」——和 `POST /library/scan`
> 已有的信任级别一样。在这之上再叠一层按路径的权限只是安全表演。
---
## 书库扫描
- **增量扫描**:先用 `mtime + size` 快速判断,再用内容哈希确认。绝大多数文件在第一阶段就被跳过。
- **目录挂载**:递归扫描挂载目录,自动跳过 `.git`、`@eaDir`、`#recycle` 等目录,不跟随符号链接。
- **自动扫描**:默认每 30 分钟全量、每 60 秒增量轮询。设 `SCAN_INTERVAL=0` / `WATCH_INTERVAL=0` 可关闭。
- **手动触发**:`POST /api/v1/library/scan`(管理员)。
### 一个文件夹什么时候是一本书
漫画在 NAS 上的常见存放方式是「一个系列一个文件夹,下面按卷分」。判定规则很短,
但它决定了一本书会不会**从书架上消失**:
- 文件夹里**有别的格式的书文件**(epub / pdf / txt / 单张图)→ 这是**书架**,不是漫画。
夹着一本书的目录绝不会因为图片多就把那本书吞掉;这条规则会递归到嵌套层级。
- 否则,有**两页以上的散图或压缩包** → 这个文件夹就是一本漫画。
- 或者,卷子目录与卷压缩包**并列**(`第01卷.cbz` 与 `第02卷/` 同时存在)→ 整卷系列,算一本。
卷压缩包里的页按「一个压缩包一页」计,包内页码由 `/assets?ref=page:卷:页` 寻址;
`/books/:id/file` 服务的路径与 manifest 列出的完全一致。
### 书籍主键:为什么不按文件路径
书籍主键是 `sha256(dc:identifier + 文件内容哈希)`,**不是文件路径**。
用路径做主键的话,用户重命名或移动一下文件,阅读进度和笔记就会全部丢失——
这是自建书库产品最常见的差评来源。当前设计下:
- 重命名 / 移动文件 → 主键不变,进度和笔记保留 ✅
- 同一本书的多份副本 → 自动合并成一条书架记录 ✅
- 重新编码过的同名文件 → 视为不同副本,不会被误认 ✅
### 元数据优先级
```
1. 用户手动修改 最高,永不自动覆盖
2. EPUB 内嵌元数据 次高,扫描不会覆盖手动修改
3. 在线刮削 provider 只填补空缺字段,标注来源,可一键撤销
4. 文件名解析 最弱,仅在以上都缺时使用
```
在线刮削是可插拔的 provider 机制。**第一版只做内嵌元数据 + 文件名解析 + 手动补全**,
不接任何在线源——避免引入网络依赖与限流问题,也避开豆瓣的合规风险。
后续接入 Google Books、Open Library 时,实现 `MetadataProvider` 接口注册即可,索引层不用改。
手动修改存在独立的覆盖层,扫描永远碰不到它。撤销单个字段用
`DELETE /api/v1/books/:id/metadata/:field`,会回落到内嵌值。
---
## 多用户
单个部署实例内的多账号,适合家庭或小圈子。两级权限:
- **admin**:创建/停用账号、触发扫描、查看实例信息
- **member**:只能看自己的书架、进度和笔记
每个账号有独立的书架、阅读进度、笔记与高亮。新账号注册后自动看到整个共享书库,
不需要逐本添加。
---
## 阅读体验:一次只取一章
打开一本书是**一次请求**。服务端的 `manifest` 默认只带**第一个窗口**的条目,
不是整本书的清单——一本 1200 章的全集打开时只传一个窗口,翻到下一章才取下一章。
- **分窗**:`/items?group=N` 一次返回一个窗口(epub 是 40 章,漫画是一卷)。
每个 group 自带 `offset`,所以客户端拿到单窗也能定位到全书位置。
**省略 `group` 等于 `group=0`**;要完整结构用 `?group=all`。
- **目录是目录**:`/toc` 返回真正的完整目录。它和分窗是两件事:分窗是传输边界,
目录必须完整才有用。把它们合成一个接口,目录就会显示成「第 1 章 – 第 40 章」
——服务端的分页边界漏进 UI。
- **章节按路径寻址**:`items[].href`(`xhtml:OEBPS/ch1.xhtml`)是章节身份。
不按下标寻址:分窗返回的是 spine 前缀,按下标算出的章节在完整 spine 里指向另一章。
- **HTTP Range**:PDF 跳页、漫画跳卷、断点续传靠它。压缩方式为 `stored` 的条目
可字节定位;`deflate` 的会诚实上报 `Accept-Ranges: none`。
尾部范围(`bytes=-N`)无法靠前向流跳过,大文件时会退化为整包响应而不是空读全文件。
- **目录跳转跨窗口**:点第 900 章会先取窗口 22,而不是「什么都没发生」。
章节 id 是唯一跨窗口稳定的身份,进度也存它。
---
## 客户端三态
服务端是唯一真相源,客户端只做「渲染 + 缓存」。客户端有三种状态:
| 状态 | 行为 |
| 我想要…… | 从这里开始 |
| --- | --- |
| 局域网直连 | 正常同步 |
| 公网可达 | 正常同步 |
| 完全离线 | 读本地缓存,恢复后合并进度 |
| 安装并读到第一本书 | [入门指南](docs/getting-started.zh-CN.md) |
| 使用书架、阅读器、书源和朗读 | [使用手册](docs/user-guide.zh-CN.md) |
| 设置环境变量、HTTPS、TTS 或排查部署问题 | [部署配置](docs/configuration.zh-CN.md) |
| 从源码运行、测试或构建 Android | [开发指南](docs/development.zh-CN.md) |
| 开发来源插件或调用 API | [文档中心](docs/README.md) |
同步的只有轻量数据:书架元数据、阅读进度、笔记、高亮。书文件不走同步。
## 当前边界
### 两端的关系
- PDF 提供基础查看;不支持 `.rar/.cbr` 或通用格式转换。
- 离线能力取决于已缓存内容;远程章节整书预下载与缓存配额尚未实现。
- 当前客户端为 Web 和 Android,没有独立 iOS 或桌面客户端;电脑端可使用浏览器。
连上一个没有出口的 Wi-Fi 时它会骗人,客户端于是转圈而不是读缓存。
Android 侧用系统的 `NET_CAPABILITY_VALIDATED` 判断。
- **稳定的设备名**:UA 里的型号会随 Chrome 升级变化,作为「上次在哪个设备读的」标签不可靠。
- **原生提示**:一行 Kotlin 的 Toast,不用在 Web 层造一套通知 UI。
## 参与贡献
### 离线合并语义
欢迎提交问题、改进文档或贡献代码。请附上复现步骤、运行环境和脱敏日志;开发与验证流程见[开发指南](docs/development.zh-CN.md)。
进度和笔记都带 `updatedAt`,冲突按 **last-writer-wins** 解决:
## 许可证
- 离线设备重连后推的旧数据**不会**回滚你在别处读到的新进度
- 笔记删除是**墓碑(tombstone)**而不是硬删除,所以离线设备无法让已删除的笔记复活
- `GET /api/v1/sync?since=<上次的 serverTime>` 只返回增量,重连很便宜
---
## 配置
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `BOOKS_DIR` | `/books` | 书库目录;可只读挂载,也可由管理员以可写方式挂载 |
| `DATA_DIR` | `./data` | 服务端写入目录,**不能**在 `BOOKS_DIR` 内 |
| `PORT` | `8080` | 监听端口 |
| `PUBLIC_URL` | 空 | 对外地址,反代后设置 |
| `SCAN_INTERVAL` | `1800` | 全量扫描间隔(秒),0 关闭 |
| `WATCH_INTERVAL` | `60` | 增量轮询间隔(秒),0 关闭 |
| `ALLOW_REGISTRATION` | `false` | 是否允许公开注册 |
| `ACCESS_TOKEN_TTL` | `86400` | 访问令牌有效期(秒) |
| `REFRESH_TOKEN_TTL` | `31536000` | 刷新令牌有效期(秒) |
| `READER_TOKEN_SECRET` | 自动生成 | 签名密钥。留空则生成并持久化到 `/data` |
| `LOG_LEVEL` | `info` | 日志级别 |
| `CORS_ORIGINS` | 空 | 允许跨域调用的来源,逗号分隔。空表示反射任意来源 |
| `WEB_DIR` | `./web` | H5 客户端产物目录,镜像里已设好 |
| `TTS_URL` | 空 | HTTP 朗读上游地址。**不设则关闭 HTTP 朗读** |
| `TTS_TOKEN` | 空 | 转发给上游的 Bearer 令牌 |
| `TTS_VOICES_URL` | `<TTS_URL>/voices` | 上游语音列表地址 |
| `TTS_TIMEOUT_MS` | `20000` | 单次合成超时(毫秒) |
| `TTS_CACHE_BYTES` | `268435456` | 音频磁盘缓存上限,存放于 `DATA_DIR/tts-cache`;0 关闭 |
> `READER_TOKEN_SECRET` 留空时会在首次启动生成并存到 `DATA_DIR/token.secret`。
> 只要 `/data` 还在,重启不会踢掉所有客户端。**删掉 `/data` 会清空所有账号和进度。**
---
## 故障排查
### 启动即退出:`EACCES: permission denied, open '/data/token.secret'`
```
fatal: Error: EACCES: permission denied, open '/data/token.secret'
at loadOrCreateSecret (file:///app/dist/config/index.js:27:5)
```
`/data` 挂上了,但容器里的服务进程写不进去。服务端以非特权用户 `reader`
(uid/gid `100:100`)运行,而 `/data` 若是 **bind mount**(`./data:/data`),
挂载点的属主来自宿主机目录,**会覆盖镜像里构建时做的 chown**——宿主机上
`./data` 不存在时 Docker 会以 `root:root` 新建它,于是非特权用户写不了。
命名卷(named volume)继承镜像里的 chown,不受影响。
镜像的入口脚本已经会在启动时自动把 `/data` 的属主改成 `reader` 再降权运行,
所以**正常用官方镜像不会遇到这个问题**。仍然报错时按顺序检查:
1. 拉的是最新镜像:`docker compose pull && docker compose up -d`。
2. 自己加了 `user:` / `docker run --user` 覆盖了启动用户,去掉它。
3. `/data` 所在文件系统不支持改属主(NFS / SMB / NTFS 挂载,如群晖的部分共享目录),
此时给 `READER_TOKEN_SECRET` 设一个 ≥16 字符的随机串,服务端就不需要写密钥文件:
```bash
openssl rand -base64 48 # 生成一个
```
在 compose 里加 `READER_TOKEN_SECRET: "<上面的值>"`。
4. 手动修宿主机目录属主(Linux;群晖等 NAS 上 uid 可能不同):
```bash
sudo chown -R 100:100 ./data
```
> 自建的镜像若来自 `server/Dockerfile`,注意 `ENTRYPOINT` 不能被覆盖,否则降权
> 那一步不会执行。服务端本身在写密钥失败时会直接报出「`DATA_DIR` 必须可写」
> 并给出一键修法,不会再只抛一个 errno。
---
## 让外网访问(内网穿透 / 域名 / HTTPS)
自部署最大的门槛在这里,所以单独说清楚。三种常见做法:
**1. 只在局域网用**(最简单)
不用做任何事,直接访问 `http://<NAS的局域网IP>:8080`。
**2. 反向代理 + HTTPS**(推荐)
用 Caddy 最省事,自动申请证书:
```caddyfile
reader.example.com {
reverse_proxy 127.0.0.1:8080
}
```
然后设置 `PUBLIC_URL=https://reader.example.com`,下载链接才会正确。
**3. 内网穿透**(没有公网 IP)
用 frp / Tailscale / Cloudflare Tunnel 任选一个。Tailscale 最简单:
在 NAS 和手机上装 Tailscale,直接用 Tailscale 分配的 IP 访问,不用配证书。
> 服务端设计上不依赖域名和 HTTPS,所以三种方式都能工作。但公网暴露时
> **务必**套一层 HTTPS,否则令牌是明文传输的。
---
## Android 客户端
APK 走 GitHub Releases / 侧载分发,不上商店。开源项目没有收入覆盖 Apple 的
99 美元开发者账号,所以 Android 先做、iOS 等有赞助再说(这条是设计文档 §5 定的)。
### 自己构建
```bash
# 1. 先把 H5 渲染层构建好并放进 assets
sh android/scripts/build-web-assets.sh
# 2. 构建 APK
cd android
./gradlew :app:assembleDebug
# 产物:android/app/build/outputs/apk/debug/app-debug.apk
# 想预置服务端地址(个人自用构建):
./gradlew :app:assembleRelease -PdefaultServerUrl=http://nas.local:8080
```
不预置地址是刻意的:没有人知道你会用什么 IP 访问自己的 NAS,
所以首次启动时问一次、之后记住。
### 装好之后
打开 App → 填服务端地址(如 `http://192.168.1.10:8080`)→ 登录。
之后所有书和进度都跟着账号走,与浏览器端互通。
### 为什么允许明文 HTTP
主要部署场景就是局域网 IP,而私有 IP 拿不到证书。要求 HTTPS 等于把用户挡在门外,
这正是设计文档里说的「部署门槛是最大的获客阻力」。
**但对公网暴露时请务必套一层 HTTPS**,否则令牌是明文传输的。
## 从源码开发
# 指向一个测试书库
BOOKS_DIR=/tmp/books DATA_DIR=/tmp/data npm run dev
```
```bash
# 客户端(另开一个终端)
cd web
npm install
npm test # Vitest 与 node:test
npm run dev # http://localhost:5174,自动把 /api 代理到 8080
npm run build # 产出 web/dist,服务端会在 / 上直接托管
```
### 项目结构
```
server/src/
config/ 运行时配置(含「DATA_DIR 不得在 BOOKS_DIR 内」的启动断言)
db/ SQLite schema 与访问层(node:sqlite,无原生依赖)
indexer/
identity.ts 书籍主键:dc:identifier + 内容哈希
filename.ts 文件名解析(保守策略,宁可留空不猜错)
metadata.ts EPUB/PDF 元数据与封面提取
scanner.ts 增量扫描、变更检测、清理
providers/ 可插拔刮削 provider 接口
services/
users.ts 账号、角色、令牌
shelf.ts 书架查询、手动覆盖层
sync.ts 进度/笔记同步与冲突合并
merge.ts 元数据优先级链
http/ Fastify 路由(含 CORS 与 H5 静态托管)
web/src/
api/ 服务端契约与请求层(令牌刷新在这里,全局只做一次)
core/ 平台抽象、同步引擎
platform.ts 两端共同接口:传输、存储、连通性
sync.ts 待发送队列 + 增量拉取 + 退避重试
android-platform.ts Android 侧的实现(很薄)
store/
offline.ts 本地镜像与待发送队列
idb.ts IndexedDB 包装,不可用时降级到内存
formats/ 格式层:拆包、编码识别、章节切分
epub.ts OPF/spine/nav 解析、资源重写
txt.ts 编码探测 + 章节推断 + 超长分段
comic.ts CBZ / 图片目录
detect.ts 魔数优先的格式识别
ui/
router.ts 路由:fragment ↔ route,以及应用自己的屏幕轨迹
mount.ts 组件树与类式屏幕之间唯一的接缝
toolkit.tsx 按钮/分段控件/开关/面板这些原语
icon.tsx 图标组件(字体的唯一使用点)
icon-names.ts 码位表,由生成器写出,不要手改
vendor/preact.ts 框架的唯一入口(换版本/换实现只改这一处)
*-screen.tsx 书架、登录、书库、阅读器(铬层)
reader-chrome.tsx 阅读器的顶栏/页脚/目录/设置/朗读条
reader-view.ts 分页、滚动、手势、定位(命令式,零框架)
locator.ts 阅读位置格式
shadow.ts 书的样式与 App 样式隔离
resources.ts 资源按需解出,不预展开
styles/
reader.css ← 干预策略都在这里,注释说明每条为什么必要
reader-icons.ttf 图标字体(生成物,见 tools/icons/)
tools/icons/ 图标字体的生成器:描边路径 → 字体
### 技术选型说明
- **Node.js 22+(`engines: >=22`,内置 `node:sqlite` 与 `scrypt` 所在的最低版本)+ TypeScript + Fastify**:单容器体积可控(约 150MB),迭代最快,
`node:sqlite` 与 `scrypt` 都是内置模块,**没有原生编译依赖**——这让镜像构建稳定,
也兑现了「一条命令跑起来」。
- **不用 better-sqlite3 / argon2 / bcrypt**:它们需要构建时工具链(`make`、`gcc`),
会把镜像撑大并让安装变脆。
- **SQLite 存 metadata,不存书**:schema 里的 `books` 表可以随时重建,书本身才是真相源。
### 客户端技术选型说明
- **UI 层用 Preact + hooks,排版层不用框架**:这条线划在「有没有布局要量测」上。
书架、登录、书库、阅读器的**铬层**(顶栏/页脚/目录/设置面板/朗读条)是状态与
事件,用组件描述;而**阅读舞台**——分页、注入文档、shadow root、量测列宽——仍然是
命令式 DOM,一个虚拟 DOM 加在代码和它要量测的布局之间,恰好是这个产品最不该透过它
去调试的东西。`ReaderScreen` 把这两半拼在一个文件里:`stage` 节点由类持有并交给
`ReaderView`,其余全部是 `ChromeState` 的函数。
选型对比见 `docs/architecture.md` §8;产物是一个 252KB(gzip 79KB)的单文件 bundle。
- **图标是一个自带的字体**:37 个字形、16KB、随 bundle 走。理由和生成方式见
[`docs/ui.md`](docs/ui.md) §1。一句话:字体把字形变成字符,它继承 `color` 和
`font-size`,因此没有可以漂移的几何——而三十个内联 SVG 组件里,颜色、尺寸、线宽
每一项都是单个字形可以漂移的位置。
- **界面用 URL 表达**:每个屏幕都有地址——`#/shelf`、`#/book/<id>`、`#/library/<路径>`。
没有服务端)和 reader 服务端(没有客户端路由回落)同时加载,只有 fragment 在两边都成立。
换来的是:一本书可以分享、可以收藏、刷新不丢;Android 返回手势和浏览器返回键
走应用自己的屏幕轨迹;走进书库子目录用 `replace`,所以返回是退出管理器而不是逐层退目录。
带着链接进来而尚未登录时,链接会等在登录页后面,登录后直接落到那本书。
- **单文件 bundle、相对路径**:产物要被两个宿主消费——服务端托管给浏览器、Android 打进
打成一份,`base: './'`。这条约束也是选 Preact 而不是 React/Vue 的关键:
`preact/compat` 不进包、没有 `react-dom`,只引入 `preact` 与 `preact/hooks`,
两个模块压缩前合计约 15KB。
- **不注入 reset 样式**:见 `web/src/styles/reader.css` 顶部。整个产品的差异点是「忠于
出版方的排版」,而一份 reset 正好会抹掉出版方的字体、缩进和行距。
可调项全部是可继承的自定义属性,默认值是 `inherit`——「关」的意思是「不动它」,
不是「用我们的默认值」。
- **书的样式装进 shadow root**:书里一条 `p { color: red }` 不能改到书架。
用 iframe 也能隔离,但每次换章要重建文档,会丢滚动位置、在 Android 上多花几倍内存。
- **书的内容不允许联网**:章节里所有本地引用在加载时被改写成内部 `reader-res:` 协议,
由渲染层从内存里的资源表解出。绝对 URL 原样保留,然后被 CSP 挡掉——
一本来路不明的书不能把阅读行为报到远端。
---
## API
所有业务接口在 `/api/v1` 下,用 `Authorization: Bearer <token>` 认证。
响应错误统一为 `{ "error": { "code": "...", "message": "..." } }`。
服务端 API 承诺**长期向后兼容**:自部署用户不会及时升级,破坏性变更只允许
「加新字段 / 加新端点」,不允许改已有字段语义。
<details>
<summary>端点列表</summary>
**公开**
- `GET /api/v1/health` — 健康检查
- `GET /api/v1/instance` — 实例信息(是否开放注册、账号数)
- `POST /api/v1/auth/register` — 注册(仅首次或开启 ALLOW_REGISTRATION)
- `POST /api/v1/auth/login` — 登录
- `POST /api/v1/auth/refresh` — 刷新令牌
**需认证**
- `GET /api/v1/auth/me`
- `POST /api/v1/auth/password`
- `POST /api/v1/auth/logout`
- `GET /api/v1/books` — 书架列表,支持 `search` `author` `series` `tag` `format` `path` `sort` `page`
- `GET /api/v1/books/:id`
- `GET /api/v1/books/:id/manifest` — 打开一本书的全部所需:文件清单 + 可寻址结构,一次请求
- `GET /api/v1/books/:id/items` — 可寻址结构(章节/页/卷),`?group=N` 取单卷
- `GET /api/v1/books/:id/toc` — 完整目录,与分窗分开
- `GET /api/v1/books/:id/assets?ref=<ref>` — 单个资源(章节文档、页图、字体)
- `GET /api/v1/books/:id/file?path=<rel_path>` — 目录书的单页,只认 manifest 列出的路径
- `GET /api/v1/library/formats` — 本实例支持的格式清单
- `GET /api/v1/books/:id/content` — 书文件流
- `GET /api/v1/books/:id/cover` — 封面
- `PATCH /api/v1/books/:id/metadata` — 手动补全元数据
- `DELETE /api/v1/books/:id/metadata/:field` — 撤销单个字段
- `GET /api/v1/library/facets` — 作者/系列/标签/格式聚合
- `GET /api/v1/library/continue` — 继续阅读
- `GET /api/v1/sync?since=<ms>` — 增量拉取
- `POST /api/v1/sync` — 批量合并
- `GET/PUT /api/v1/sync/progress/:bookId`
- `GET /api/v1/notes` / `POST /api/v1/notes` / `DELETE /api/v1/notes/:id`
**书库**(读写 `/books`;挂载只读时任何写操作返回 `403 READ_ONLY_MOUNT`)
- `GET /api/v1/library/browse?path=<相对路径>` — 列一个目录,带面包屑与标记
- `POST /api/v1/library/browse/move` — 移动到另一个目录
- `POST /api/v1/library/browse/rename` — 改名
- `POST /api/v1/library/browse/mkdir` — 新建文件夹
- `POST /api/v1/library/browse/delete` — 删除(递归)
- `POST /api/v1/library/browse/metadata` — 批量改元数据(路径可以是目录,等于目录里每本书)
- `POST /api/v1/library/browse/shelf` — 批量加入书架 / 从书架拿掉(只动 `user_books`,不动磁盘)
- `POST /api/v1/library/upload` — 上传书籍(multipart;`.zip`/`.cbz` 按合集解开)
- `GET /api/v1/library/upload` — 挂载是否可写,客户端在开始传之前先问
**管理员**
- `POST /api/v1/library/scan`
- `GET /api/v1/library/scan`
- `GET /api/v1/library/stats`
- `GET/POST/PATCH /api/v1/admin/users`
- `GET /api/v1/providers`
</details>
---
## 范围边界(第一版)
**做**:目录挂载扫描、元数据刮削、两级权限、同步 API、Docker 单容器、Android 客户端、EPUB 精排、PDF 保底、按书离线缓存。
**不做**:在线书城、iOS、桌面端、社交、AI 问答、全格式转换、移动端批注输入。
`.cbr` / `.rar` 不在支持列表里——它需要 RAR 解码器,而当前依赖树刻意不含任何原生模块。
### 已知边界
写在明处,避免预期错位:
- **PDF 只做保底可读**:不解析页数、书签,也不重排。页数上报 `null` 而不是猜一个数字 ——
猜错会让客户端的进度条错乱,比没有更糟。
- **文本不做精品排版**:TXT 按行渲染,不折行重排、不调字号。精品排版是 EPUB 的承诺。
- **不做全格式转换**:不把 epub 转 pdf、不把 txt 转 epub。
- **ZIP 不支持 ZIP64 / 加密 / 分卷**:超限会明确报错,不会静默解出坏数据。
- **漫画目录的嵌套有上限**:卷目录下面还有目录时会继续往下收页(文件系统里读到第 4 层为止),
再深会把一个杂乱目录变成几千页的巨型漫画。图片目录里**内嵌的压缩包不再展开**,一个压缩包算一页。
页序是真自然序(`1, 2, 10`,不是 `1, 10, 2`),排错不是观感问题,是故事被静默打乱。
- **没有内置 HTTPS 与限流**:给反向代理留位置,不重复造轮子。
---
## 许可
AGPL-3.0。选择 AGPL 而非 GPL:自部署产品最怕别人拿去做闭源付费服务,
AGPL 的网络分发条款正好卡住这条路径(Kavita、Audiobookshelf 用的 GPL-3.0 更松一档)。
## 贡献
欢迎提交 Issue 和 PR。请先跑通 `npm test`,并为新行为补测试。
[GNU Affero General Public License v3.0](LICENSE)。
+38
View File
@@ -0,0 +1,38 @@
# 文档中心 · Documentation
[项目首页](../README.md) · [English overview](../README.en.md)
项目首页介绍用途与能力;安装、日常使用、运维和开发步骤分别放在下面的指南中。
The project README introduces reader. Use the guides below for installation, everyday use, operations, and development.
## 使用与部署 · Using and deploying reader
| 内容 / Topic | 简体中文 | English |
| --- | --- | --- |
| 安装、首次登录、添加书籍、更新 / Installation and first steps | [入门指南](getting-started.zh-CN.md) | [Getting started](getting-started.en.md) |
| 书架、阅读、朗读、来源和离线 / Shelves, reading, TTS, sources, offline | [使用手册](user-guide.zh-CN.md) | Chinese only |
| 环境变量、目录权限、HTTPS、排错 / Settings, permissions, HTTPS, troubleshooting | [部署配置](configuration.zh-CN.md) | [Configuration](configuration.en.md) |
| 本地开发、验证、镜像与 Android 构建 / Development, checks, Docker and Android builds | [开发指南](development.zh-CN.md) | [Development](development.en.md) |
## 技术参考 · Technical reference
以下参考文档目前以中文维护。The following reference documents are currently maintained in Chinese.
| 文档 / Document | 内容 / Contents |
| --- | --- |
| [架构与数据边界 / Architecture](architecture.md) | 数据目录、身份、同步和客户端架构 / Data boundaries, identity, sync, client architecture |
| [API](api.md) | 鉴权、书籍、资源、进度、文件管理、来源 / Auth, books, resources, progress, file management, sources |
| [来源插件系统 / Source plugins](source-plugins.md) | 契约、宿主职责及实施状态 / Contracts, host responsibilities, implementation status |
| [通用插件扩展 / Plugin extensions](plugin-extensions.md) | 配置页面、任务、搜索、换源 / Configuration pages, tasks, search, source switching |
| [界面规范 / UI guidelines](ui.md) | 布局、交互、可访问性 / Layout, interaction, accessibility |
## 验证记录 · Review records
这些是特定功能交付时的验证记录,当前结果以对应提交的测试和 CI 输出为准。
These records describe checks at specific delivery points. Use tests and CI for the current commit's results.
- [UI 评审工具 / UI review tooling](ui-review/README.md)
- [书源界面 / Source UI](ui-review/sources-ux.md)
- [书库与阅读界面 / Library and reader UI](ui-review/collections-ux.md)
- [书源搜索 / Source search](ui-review/source-search.md)
- [搜索性能 / Search performance](ui-review/source-search-performance.md)
+95
View File
@@ -0,0 +1,95 @@
# Configuration
[Project home](../README.en.md) · [Documentation](README.md) · [简体中文](configuration.zh-CN.md)
For a first installation, follow [getting started](getting-started.en.md). This page covers configuration, maintenance, and troubleshooting.
## Directories and permissions
- `BOOKS_DIR` contains your books. Use `:ro` for read-only access, or `:rw` plus filesystem write permissions for administrator file management.
- `DATA_DIR` stores SQLite, accounts, reading records, keys, covers, scan state, upload staging, remote downloads, and plugin data. It must always be writable.
- `DATA_DIR` must not be the same directory as, or a child of, `BOOKS_DIR`. Startup rejects that configuration.
- The image entrypoint attempts to fix ownership of the data directory itself, then runs as the `reader` user. It does not change book-directory ownership or recursively repair existing data-file permissions.
Read-only books do not prevent account or progress updates in the data directory. To enable uploads, check host ACLs and directory permissions as well as changing the mount to `:rw`.
## Environment variables
These are defaults when running the server directly. The Docker image additionally sets `DATA_DIR=/data` and `WEB_DIR=/app/web`, with `BOOKS_DIR=/books`.
| Variable | Default | Purpose |
| --- | --- | --- |
| `BOOKS_DIR` | `/books` | Book directory |
| `DATA_DIR` | `data` under the current working directory | Server state and caches |
| `HOST` | `0.0.0.0` | Listen address |
| `PORT` | `8080` | Listen port |
| `PUBLIC_URL` | Empty | Public-facing URL; set when using a reverse proxy |
| `SCAN_INTERVAL` | `1800` | Scheduled scan interval in seconds; `0` disables it |
| `WATCH_INTERVAL` | `60` | Change-check interval in seconds; `0` disables it |
| `ALLOW_REGISTRATION` | `false` | Allow public registration after the first account |
| `ACCESS_TOKEN_TTL` | `86400` | Access-token lifetime in seconds |
| `REFRESH_TOKEN_TTL` | `31536000` | Refresh-token lifetime in seconds |
| `READER_TOKEN_SECRET` | Generated | Signing secret, at least 16 characters; otherwise stored in `DATA_DIR/token.secret` |
| `LOG_LEVEL` | `info` | Logging level |
| `CORS_ORIGINS` | Empty | Comma-separated browser origins; empty reflects the requesting origin |
| `WEB_DIR` | `web` under the current working directory | Built Web client directory |
| `TTS_URL` | Empty | HTTP speech upstream; empty disables HTTP speech |
| `TTS_TOKEN` | Empty | Upstream Bearer token |
| `TTS_VOICES_URL` | `<TTS_URL>/voices` | Upstream voice list |
| `TTS_TIMEOUT_MS` | `20000` | Speech request timeout in milliseconds |
| `TTS_CACHE_BYTES` | `268435456` | Byte limit for `DATA_DIR/tts-cache`; `0` disables caching |
Run `docker compose up -d` after changing environment variables. Keep the signing secret stable to preserve sessions. Accounts, progress, and notes cannot be recovered by rescanning book files.
## HTTPS and reverse proxies
Use `http://<server-ip>:8080` on your LAN. Use HTTPS for public access to protect login credentials and access tokens.
For Caddy running on the same host as reader:
```caddyfile
reader.example.com {
reverse_proxy 127.0.0.1:8080
}
```
Set `PUBLIC_URL=https://reader.example.com`. If Caddy also runs in a container, use reader's service address on the container network instead.
For separately hosted frontends, set `CORS_ORIGINS` to the Web client's actual origin. Source search uses Streamable HTTP: proxies must forward `text/event-stream` incrementally rather than buffer the entire response. A VPN or tunnel can provide access when you have no public IP.
## HTTP text-to-speech
Use Android system speech or browser `speechSynthesis` when available. To enable an HTTP speech service, configure a compatible upstream in Compose:
```yaml
environment:
TTS_URL: "http://your-tts-host:5002/tts"
# TTS_TOKEN: "your-token"
# TTS_VOICES_URL: "http://your-tts-host:5002/voices"
TTS_CACHE_BYTES: "268435456"
```
reader proxies speech requests and does not include a synthesis model. The upstream must implement the expected request protocol; a service's name alone does not establish compatibility. See the [TTS API (中文)](api.md). The address must be reachable from the reader container; `localhost` inside a container refers to that container.
## Troubleshooting
### Data directory is not writable at startup
Check `docker compose logs --tail=100 reader`. For `EACCES` on `/data/token.secret`, the database, or caches:
1. Confirm `/data` is writable and has free space.
2. If you set `user:`, `--user`, or replace the entrypoint, ensure that user can write the data directory.
3. Run `docker compose run --rm --no-deps --entrypoint id reader reader` to inspect the image's `reader` UID/GID, then correct host directory ownership or ACLs. If you use a custom runtime user, use that user's identity instead.
4. NFS, SMB, and NTFS may need their own permission configuration rather than `chown` alone.
Setting `READER_TOKEN_SECRET` only avoids writing the secret file. It does not remove the need for writable database and cache storage.
### File management returns `READ_ONLY_MOUNT`
Check whether the mount uses `:ro`. After switching to `:rw` and recreating the container, verify the server process can write the target directory. Uploads are available only on the administrator file-management page.
### The UI opens but books are missing
Check that the mounted files are readable and supported, then wait for scanning or trigger a scan as an administrator. The scanner skips directories such as `.git`, `@eaDir`, and `#recycle` and does not follow symlinks. Use the library file page to inspect the actual directory.
### Remote source searches fail
+95
View File
@@ -0,0 +1,95 @@
# 部署配置
[项目首页](../README.md) · [文档中心](README.md) · [English](configuration.en.md)
初次部署请先看[入门指南](getting-started.zh-CN.md)。本页适用于部署后的配置、维护与排错。
## 目录与权限
- `BOOKS_DIR` 是用户书籍目录。`:ro` 用于只读部署;`:rw` 配合文件系统写权限启用管理员文件管理。
- `DATA_DIR` 保存 SQLite、账号、阅读记录、密钥、封面、扫描状态、上传暂存、远程下载与插件数据,必须始终可写。
- `DATA_DIR` 不能与 `BOOKS_DIR` 相同或位于其内部,启动时会拒绝该配置。
- 镜像入口脚本尝试修正数据目录本身的属主,再以 `reader` 用户运行。它不会替你修改书籍目录的属主,也不会递归修复已有数据文件的权限。
书库只读不影响账号和阅读进度写入数据目录。需要上传时,除修改 `:rw` 外,还要检查宿主机 ACL 或目录权限。
## 环境变量
以下是直接运行服务端时的默认值。Docker 镜像另设 `DATA_DIR=/data`、`WEB_DIR=/app/web`,`BOOKS_DIR=/books`。
| 变量 | 默认值 | 用途 |
| --- | --- | --- |
| `BOOKS_DIR` | `/books` | 书籍目录 |
| `DATA_DIR` | 当前工作目录下的 `data` | 服务端状态和缓存目录 |
| `HOST` | `0.0.0.0` | 监听地址 |
| `PORT` | `8080` | 监听端口 |
| `PUBLIC_URL` | 空 | 对外访问地址,反向代理后设置 |
| `SCAN_INTERVAL` | `1800` | 定时扫描间隔,秒;`0` 关闭 |
| `WATCH_INTERVAL` | `60` | 变化检查间隔,秒;`0` 关闭 |
| `ALLOW_REGISTRATION` | `false` | 首个账号之后是否允许公开注册 |
| `ACCESS_TOKEN_TTL` | `86400` | 访问令牌有效期,秒 |
| `REFRESH_TOKEN_TTL` | `31536000` | 刷新令牌有效期,秒 |
| `READER_TOKEN_SECRET` | 自动生成 | 至少 16 字符的签名密钥;默认保存在 `DATA_DIR/token.secret` |
| `LOG_LEVEL` | `info` | 日志级别 |
| `CORS_ORIGINS` | 空 | 允许的浏览器来源,逗号分隔;空值反射请求来源 |
| `WEB_DIR` | 当前工作目录下的 `web` | 已构建 Web 客户端目录 |
| `TTS_URL` | 空 | HTTP 语音上游地址,空值关闭 HTTP 朗读 |
| `TTS_TOKEN` | 空 | 上游 Bearer 令牌 |
| `TTS_VOICES_URL` | `<TTS_URL>/voices` | 上游语音列表 |
| `TTS_TIMEOUT_MS` | `20000` | 单次语音请求超时,毫秒 |
| `TTS_CACHE_BYTES` | `268435456` | `DATA_DIR/tts-cache` 缓存上限,字节;`0` 关闭 |
修改 Compose 环境变量后执行 `docker compose up -d`。签名密钥需要稳定保存,否则已有会话会失效。数据目录中的账号、进度和笔记不能靠重新扫描书籍恢复。
## HTTPS 与反向代理
局域网可直接访问 `http://<服务器IP>:8080`。对公网提供服务时使用 HTTPS,保护登录凭据和访问令牌。
例如 Caddy 与 reader 运行在同一台主机上时:
```caddyfile
reader.example.com {
reverse_proxy 127.0.0.1:8080
}
```
设置 `PUBLIC_URL=https://reader.example.com`。如果 Caddy 也在容器中,代理地址应改为容器网络中 reader 的服务地址。
前后端分开部署时,将 `CORS_ORIGINS` 设置为 Web 客户端的实际来源。书源搜索采用 Streamable HTTP,代理需要持续转发 `text/event-stream`;不要把搜索响应缓冲到完成后才返回。没有公网 IP 时也可用 VPN 或隧道连接服务。
## HTTP 朗读
优先使用 Android 系统语音或浏览器 `speechSynthesis`。需要 HTTP 语音时,在 Compose 中配置兼容上游:
```yaml
environment:
TTS_URL: "http://your-tts-host:5002/tts"
# TTS_TOKEN: "your-token"
# TTS_VOICES_URL: "http://your-tts-host:5002/voices"
TTS_CACHE_BYTES: "268435456"
```
reader 代理语音请求,不内置合成模型。上游接口需兼容项目的请求协议;不能仅凭服务名称判断是否兼容,详见 [TTS API](api.md)。地址必须从 reader 容器内可达,容器内的 `localhost` 指容器自身。
## 故障排查
### 启动时报数据目录不可写
先看 `docker compose logs --tail=100 reader`。如果是 `/data/token.secret`、数据库或缓存目录的 `EACCES`:
1. 检查 `/data` 没有被只读挂载,且有可用空间。
2. 若设置了 `user:`、`--user` 或替换了入口脚本,需要自行保证该用户可写数据目录。
3. 运行 `docker compose run --rm --no-deps --entrypoint id reader reader` 查看镜像中 `reader` 用户的 UID/GID,再在宿主机修正该数据目录的属主或 ACL;自定义运行用户时以该用户为准。
4. NFS、SMB 或 NTFS 需按共享文件系统的权限机制配置,不能只依赖 `chown`。
设置 `READER_TOKEN_SECRET` 只能省去密钥文件写入,不能替代数据库和缓存所需的写权限。
### 文件管理返回 `READ_ONLY_MOUNT`
检查卷是否为 `:ro`。改用 `:rw` 并重新创建容器后,还需确认服务进程对目标目录有写权限。上传仅在管理员文件管理页面提供。
### 页面打开但看不到书
确认挂载目录中的文件对服务进程可读,格式受支持,并等待扫描完成或由管理员手动扫描。扫描器跳过 `.git`、`@eaDir`、`#recycle` 等目录,不跟随符号链接;可在书库文件页核对实际目录。
### 远程书源搜索失败
+114
View File
@@ -0,0 +1,114 @@
# Development
[Project home](../README.en.md) · [Documentation](README.md) · [简体中文](development.zh-CN.md)
## Requirements
- Node.js 24 is recommended to match the current container and CI. The server uses built-in `node:sqlite`.
- npm and Git. Server, Web, and the source plugin have separate package manifests and lockfiles; there is no root-level npm script.
- Chromium for browser-rule tests and UI reviews.
- For Android: JDK 17+, Android SDK 35, and Build Tools 35.0.1. The Web asset script requires a POSIX shell; Windows users can use Git Bash or WSL.
Unless noted otherwise, run commands from the repository root.
## Install dependencies
## Start development servers
Open two terminals at the repository root. Use a test library and keep data outside it.
Linux/macOS:
```sh
mkdir -p /tmp/reader-dev-books /tmp/reader-dev-data
BOOKS_DIR=/tmp/reader-dev-books DATA_DIR=/tmp/reader-dev-data npm run dev --prefix server
```
PowerShell:
```powershell
$env:BOOKS_DIR = Join-Path $env:TEMP 'reader-dev-books'
$env:DATA_DIR = Join-Path $env:TEMP 'reader-dev-data'
New-Item -ItemType Directory -Force -Path $env:BOOKS_DIR, $env:DATA_DIR | Out-Null
npm run dev --prefix server
```
In the second terminal:
```sh
npm run dev --prefix web
```
Open `http://localhost:5174`. Vite proxies `/api` to `http://127.0.0.1:8080` by default. Set `READER_SERVER` to change the proxy target.
## Checks and tests
Run checks for the modules you change. Available entry points are:
```sh
npm run typecheck --prefix server
npm test --prefix server
npm run build --prefix server
npm run typecheck --prefix web
npm test --prefix web
npm run build --prefix web
Web tests use both Vitest and Node's test runner. Plugin tests include real Chromium; prepare it as described above. Historical test counts are not a substitute for current results.
For UI changes, run `npm run ui:review --prefix web`; source workflows have `npm run ui:sources --prefix web`. See [UI review (中文)](ui-review/README.md) for tooling and requirements. Reviews generate screenshots and reports; screenshots are ignored by Git by default.
## Build and run
### Single-container image
```sh
docker build -f server/Dockerfile -t reader:local .
```
Use the repository root as the build context. The image builds both the Web client and server. Set the Compose image to `reader:local`, or use its commented `build` configuration.
### Without a container
Build both server and Web with their `npm run build` scripts, then start from the repository root:
```sh
BOOKS_DIR=/tmp/reader-dev-books DATA_DIR=/tmp/reader-dev-data WEB_DIR="$PWD/web/dist" npm start --prefix server
```
In PowerShell, set `$env:BOOKS_DIR`, `$env:DATA_DIR`, and `$env:WEB_DIR = (Resolve-Path 'web/dist').Path`, then run `npm start --prefix server`. `WEB_DIR` must point to the built client; the server does not automatically locate another project's `web/dist` directory.
### Android
Build the shared Web assets from the repository root, then build the APK:
```sh
sh android/scripts/build-web-assets.sh
cd android
./gradlew :app:assembleDebug
```
Use `gradlew.bat` from a native Windows terminal. The output is `android/app/build/outputs/apk/debug/app-debug.apk`. Add `-PdefaultServerUrl=http://nas.local:8080` to preset the server address; otherwise users enter it and sign in. Building an APK does not validate reading on a physical device.
## Code map
| Directory | Responsibility |
| --- | --- |
| `server/src/config`, `db` | Runtime configuration, SQLite schema and access |
| `server/src/indexer` | File formats, scanning, identity, and metadata |
| `server/src/services`, `http` | Business services, API, authentication, and resources |
| `server/src/sources` | Built-in sources and generic plugin contracts |
| `web/src/formats` | EPUB, TXT, comics, and other format handling |
| `web/src/ui`, `styles` | Preact UI, reading stage, and styles |
| `web/src/core`, `store` | Platform capabilities, synchronization, and offline storage |
Preact manages UI state; the reading stage handles content layout and measurement. Shadow DOM isolates book styles, and icons use Lucide SVG. Both the server and Android consume the Web build. See [architecture (中文)](architecture.md) and [UI guidelines (中文)](ui.md) for details.
## Contributing workflow
1. Describe the issue, reproduction steps, and expected behavior. Remove credentials from logs, source rules, and examples.
2. Keep changes focused and follow existing error handling and test patterns.
3. Add appropriate regression checks for behavior changes. Documentation-only changes need link, command, and example checks.
4. Describe the changes, validation, and known limitations when submitting.
For integrations, see the [API reference (中文)](api.md), [source architecture (中文)](source-plugins.md), and [extension protocol (中文)](plugin-extensions.md).
+114
View File
@@ -0,0 +1,114 @@
# 开发指南
[项目首页](../README.md) · [文档中心](README.md) · [English](development.en.md)
## 环境
- 推荐 Node.js 24,与当前容器和 CI 保持一致;服务端使用内置 `node:sqlite`。
- npm、Git;服务端、Web 和书源插件各有独立 `package.json` 与锁文件,仓库根目录没有统一 npm 脚本。
- 运行浏览器规则测试或 UI 评审需要 Chromium。
- 构建 Android 另需 JDK 17+、Android SDK 35、Build Tools 35.0.1;Web 资源构建脚本需要 POSIX shell(Windows 可使用 Git Bash 或 WSL)。
以下命令从仓库根目录执行。
## 安装依赖
## 启动开发服务
在仓库根目录开两个终端。使用测试书库,不要把数据目录放进书库里。
Linux/macOS:
```sh
mkdir -p /tmp/reader-dev-books /tmp/reader-dev-data
BOOKS_DIR=/tmp/reader-dev-books DATA_DIR=/tmp/reader-dev-data npm run dev --prefix server
```
PowerShell:
```powershell
$env:BOOKS_DIR = Join-Path $env:TEMP 'reader-dev-books'
$env:DATA_DIR = Join-Path $env:TEMP 'reader-dev-data'
New-Item -ItemType Directory -Force -Path $env:BOOKS_DIR, $env:DATA_DIR | Out-Null
npm run dev --prefix server
```
第二个终端:
```sh
npm run dev --prefix web
```
访问 `http://localhost:5174`。Vite 默认把 `/api` 代理到 `http://127.0.0.1:8080`;通过 `READER_SERVER` 环境变量可调整代理目标。
## 检查与测试
按修改范围执行,以下是各模块入口:
```sh
npm run typecheck --prefix server
npm test --prefix server
npm run build --prefix server
npm run typecheck --prefix web
npm test --prefix web
npm run build --prefix web
Web 测试同时包含 Vitest 和 Node 测试。插件测试包含真实 Chromium;浏览器缺失时按上面的步骤准备环境。不要把历史测试数量当作当前验证结果。
UI 改动可运行 `npm run ui:review --prefix web`,书源流程可运行 `npm run ui:sources --prefix web`。评审工具及环境说明见 [UI 评审](ui-review/README.md),会生成截图和报告;截图目录默认不纳入 Git。
## 构建与运行
### 单容器镜像
```sh
docker build -f server/Dockerfile -t reader:local .
```
构建上下文必须是仓库根目录,镜像会同时构建 Web 和服务端。部署时将 Compose 的镜像名改为 `reader:local`,或使用文件内的 `build` 配置。
### 不使用容器
先执行两端的 `npm run build`,再从仓库根目录启动:
```sh
BOOKS_DIR=/tmp/reader-dev-books DATA_DIR=/tmp/reader-dev-data WEB_DIR="$PWD/web/dist" npm start --prefix server
```
PowerShell 中先设置 `$env:BOOKS_DIR`、`$env:DATA_DIR`,并将 `$env:WEB_DIR = (Resolve-Path 'web/dist').Path`,再运行 `npm start --prefix server`。`WEB_DIR` 需要指向产物目录,服务端不会自动猜测另一个项目中的 `web/dist`。
### Android
从仓库根目录构建 Web 资源,再构建 APK:
```sh
sh android/scripts/build-web-assets.sh
cd android
./gradlew :app:assembleDebug
```
Windows 原生终端使用 `gradlew.bat`。产物为 `android/app/build/outputs/apk/debug/app-debug.apk`。可通过 `-PdefaultServerUrl=http://nas.local:8080` 预设地址;未预设时由用户填写服务地址并登录。编译 APK 不等于完成真机阅读验证。
## 代码导览
| 目录 | 责任 |
| --- | --- |
| `server/src/config`、`db` | 运行时配置、SQLite schema 和访问层 |
| `server/src/indexer` | 文件格式、扫描、身份与元数据 |
| `server/src/services`、`http` | 业务服务、API、鉴权和资源交付 |
| `server/src/sources` | 内置来源和通用插件契约 |
| `web/src/formats` | EPUB、TXT、漫画等格式处理 |
| `web/src/ui`、`styles` | Preact 界面、阅读舞台和样式 |
| `web/src/core`、`store` | 平台能力、同步、离线存储 |
界面状态使用 Preact,阅读内容布局与测量由阅读舞台管理;书籍样式通过 Shadow DOM 隔离,图标使用 Lucide SVG。Web 产物由服务端和 Android 共同使用。详细取舍见[架构文档](architecture.md)和[界面规范](ui.md)。
## 贡献流程
1. 描述问题、复现步骤和期望行为;日志、书源及示例请移除凭据。
2. 将修改限制在相关模块,参考已有错误语义和测试方式。
3. 为行为变更补充适当的回归验证;纯文档修改检查链接、命令和示例即可。
4. 提交变更时说明改动、验证方式和已知限制。
API 详见 [API 文档](api.md),插件开发见[来源架构](source-plugins.md)与[扩展协议](plugin-extensions.md)。
+109
View File
@@ -0,0 +1,109 @@
# Getting started
[Project home](../README.en.md) · [Documentation](README.md) · [简体中文](getting-started.zh-CN.md)
This guide takes you through Docker deployment, account registration, and opening your first book. To run from source, use the [development guide](development.en.md).
## 1. Prepare your environment
- Install Docker Engine or Docker Desktop and the Docker Compose plugin.
- Prepare a book directory and a separate data directory.
- Make port `8080` available, or change the host port in Compose, for example to `8090:8080`.
The container serves both the API and Web UI. No separate frontend deployment is needed. Run the following commands from the repository root.
## 2. Get the deployment configuration
```sh
git clone https://cnb.cool/hectorqin/reader.git
cd reader
```
Edit [docker-compose.yml](../docker-compose.yml) and replace the host-side book path:
```yaml
volumes:
- /path/to/your/books:/books:ro
- ./data:/data
```
For example, use `/srv/books:/books:ro` on Linux, or `"D:/books:/books:ro"` with Docker Desktop on Windows. The relative `./data` path is resolved from the Compose file's directory.
| Directory | Purpose |
| --- | --- |
| `/books` (`BOOKS_DIR`) | Your existing book files |
| `/data` (`DATA_DIR`) | Database, accounts, reading records, covers, upload staging, downloaded files, and plugin data |
`DATA_DIR` must be outside `BOOKS_DIR` and must not be the same directory. Startup checks this constraint. Metadata stays in the data directory; reader does not create cover files or `.calibre` metadata beside your books.
### Choose read-only or writable books
- **Browse and read only**: keep `:ro`. Add books through the host or NAS and let reader scan them.
- **Manage files in the browser**: use `:rw` and give the server process write permission. Administrators can upload, rename, move, delete, and create directories in the library file manager.
A read-only book mount still allows shelves, progress, and notes to be saved in `/data`. See [configuration](configuration.en.md) for permissions and mount details.
## 3. Start and sign in
```sh
docker compose up -d
docker compose ps
docker compose logs --tail=100 reader
```
Open `http://localhost:8080`, or `http://<server-ip>:8080` from another device.
1. Register the first account. It automatically becomes the administrator.
2. Wait for the initial scan, then open a book from the shelf or library.
3. Create accounts for other members. Public registration is disabled by default after the first account.
To allow later self-registration, set `ALLOW_REGISTRATION: "true"` in the Compose `environment` section and run `docker compose up -d` again.
The health endpoint is `/api/v1/health`. For startup failures, missing books, or write errors, see [troubleshooting](configuration.en.md#troubleshooting).
### Without Compose
On Linux/macOS with a POSIX shell, you can also run the container using absolute mount paths:
```sh
mkdir -p ./data
docker run -d --name reader --restart unless-stopped \
-p 8080:8080 \
-v /path/to/your/books:/books:ro \
-v "$(pwd)/data:/data" \
cnb.cool/hectorqin/reader:latest
```
On Windows, the Compose setup above avoids shell-specific path and line-continuation syntax.
## 4. Add books and sources
### Local books
Place EPUB, TXT, PDF, CBZ/ZIP, or comic image directories in your mounted library. By default, the server runs scheduled scans every 30 minutes and checks for changes every 60 seconds. Administrators can trigger a scan manually. With a writable mount, uploads are available in the administrator file manager.
The shelf contains readable books; the library lets you browse the disk directory. Removing a book from a shelf is different from deleting its file. See the [user guide (中文)](user-guide.zh-CN.md).
### OPDS
As an administrator, add an OPDS source under Sources (书源) and configure its server URL. If authentication is required, set credentials for your current account. Open the source to browse or search its catalog and acquire books for your shelf.
## 5. Upgrade and back up
Back up both the book and data directories before upgrading. For a simple file-based SQLite backup, stop the service and copy the entire `/data` directory, including database auxiliary files, to avoid copying inconsistent live state.
```sh
docker compose stop
# Back up the host book directory and ./data here.
docker compose pull
docker compose up -d
```
Keep your data directory: it contains accounts, reading records, signing keys, and plugin state. Plugin packages are upgraded separately; disable the plugin in the UI before replacing its package.
## Next steps
- [User guide (中文)](user-guide.zh-CN.md): reading settings, shelves, sources, TTS, and offline access.
- [Configuration](configuration.en.md): environment variables, HTTPS, permissions, and troubleshooting.
- [Development](development.en.md): local setup, checks, Docker builds, and Android.
- [API reference (中文)](api.md): automation and integrations.
+109
View File
@@ -0,0 +1,109 @@
# 入门指南
[项目首页](../README.md) · [文档中心](README.md) · [English](getting-started.en.md)
本文带你完成 Docker 部署、注册账号和读取第一本书。直接从源码运行请看[开发指南](development.zh-CN.md)。
## 1. 准备环境
- 安装 Docker Engine 或 Docker Desktop,以及 Docker Compose 插件。
- 准备一个书籍目录和一个独立的数据目录。
- 确保主机的 `8080` 端口可用;也可修改 Compose 左侧端口,例如 `8090:8080`。
容器同时提供 API 和 Web 界面,无需单独部署前端。以下命令从仓库根目录执行。
## 2. 获取部署配置
```sh
git clone https://cnb.cool/hectorqin/reader.git
cd reader
```
编辑 [docker-compose.yml](../docker-compose.yml),把宿主机书籍路径替换为自己的路径:
```yaml
volumes:
- /path/to/your/books:/books:ro
- ./data:/data
```
例如 Linux 使用 `/srv/books:/books:ro`,Windows Docker Desktop 可使用 `"D:/books:/books:ro"`。相对路径 `./data` 位于 Compose 文件所在目录。
| 目录 | 用途 |
| --- | --- |
| `/books`(`BOOKS_DIR`) | 现有书籍文件 |
| `/data`(`DATA_DIR`) | 数据库、账号、阅读记录、封面缓存、上传暂存、下载文件和插件数据 |
`DATA_DIR` 不能与 `BOOKS_DIR` 相同,也不能位于其中;启动时会检查这项约束。元数据保存在数据目录,不会在书旁生成封面或 `.calibre` 文件。
### 选择只读或可写书库
- **只浏览和阅读**:保留 `:ro`。通过宿主机或 NAS 放入新书,等待扫描。
- **在网页管理文件**:改为 `:rw`,并确保服务进程对该目录有写权限。管理员可以在书库文件管理页上传、改名、移动、删除和新建目录。
只读书库不会妨碍进度、笔记或书架保存,这些数据写入 `/data`。挂载和权限的详细说明见[部署配置](configuration.zh-CN.md)。
## 3. 启动与首次登录
```sh
docker compose up -d
docker compose ps
docker compose logs --tail=100 reader
```
打开 `http://localhost:8080`;在另一台设备上使用 `http://<服务器IP>:8080`。
1. 注册第一个账号,该账号自动成为管理员。
2. 等待初次扫描,在书架或书库中打开一本书。
3. 为家人或其他成员创建账号。首个账号创建后默认关闭公开注册。
如需开放后续注册,在 Compose 的 `environment` 中设置 `ALLOW_REGISTRATION: "true"`,然后执行 `docker compose up -d`。
健康检查地址为 `/api/v1/health`。若启动失败、目录为空或无法写入,请看[故障排查](configuration.zh-CN.md#故障排查)。
### 不使用 Compose
在 Linux/macOS 的 POSIX shell 中,也可使用绝对路径启动容器:
```sh
mkdir -p ./data
docker run -d --name reader --restart unless-stopped \
-p 8080:8080 \
-v /path/to/your/books:/books:ro \
-v "$(pwd)/data:/data" \
cnb.cool/hectorqin/reader:latest
```
Windows 推荐使用上面的 Compose 配置,避免不同 shell 的路径和换行语法差异。
## 4. 添加书籍与来源
### 本地书籍
把 EPUB、TXT、PDF、CBZ/ZIP 或漫画图片目录放到挂载的书库中。默认每 30 分钟定时扫描,每 60 秒检查变化;管理员也可手动触发扫描。可写挂载下,上传入口位于管理员文件管理页。
书架显示可阅读的书籍;书库按磁盘目录浏览。从书架移除书籍与删除磁盘文件是两种不同操作。详见[使用手册](user-guide.zh-CN.md)。
### OPDS
管理员在「书源」添加 OPDS 来源并设置服务地址。需要认证时填写当前账号的来源凭据。打开来源后可浏览或搜索目录,获取书籍并加入书架。
## 5. 更新与备份
更新前备份书籍目录和数据目录。对 SQLite 采用简单文件备份时,先停止服务,再复制完整 `/data`(包括数据库及其辅助文件),避免复制运行中的不一致状态。
```sh
docker compose stop
# 在此备份宿主机上的书籍目录和 ./data
docker compose pull
docker compose up -d
```
不要删除数据目录:其中包含账号、阅读记录、签名密钥和插件状态。插件包需单独升级;替换包前先在界面停用插件。
## 下一步
- [使用手册](user-guide.zh-CN.md):阅读设置、书架、书源、朗读和离线。
- [部署配置](configuration.zh-CN.md):环境变量、HTTPS、目录权限和故障排查。
- [开发指南](development.zh-CN.md):本地运行、验证、构建镜像和 Android。
- [API 文档](api.md):自动化与集成。
+93
View File
@@ -0,0 +1,93 @@
# 使用手册
[项目首页](../README.md) · [文档中心](README.md) · [安装部署](getting-started.zh-CN.md)
本页介绍安装后的日常使用。部署参数见[配置指南](configuration.zh-CN.md),协议细节见 [API](api.md)。
## 书架与书库
- **书架**是当前账号的阅读列表,可搜索、排序、调整密度并查看阅读进度。
- **书库**按挂载目录浏览本地书籍,可将书加入书架。
- **文件管理**供管理员检查磁盘目录。挂载可写且目录权限允许时,可上传、改名、移动、删除和新建目录。
从书架移除只改变该账号的书架记录,不删除书文件。文件管理中的删除会修改磁盘,请按界面确认提示操作。书籍的改名或移动按已有身份更新路径,保留阅读记录。
上传 `.zip/.cbz` 时会作为合集解包,保留必要的目录层级。单纯想把 CBZ 作为书籍扫描时,也可以直接把文件放到宿主机书库目录。重名上传支持保留两份、跳过、覆盖或拒绝整批,具体错误语义见 [上传 API](api.md)。
## 支持格式
| 格式 | 日常使用 |
| --- | --- |
| EPUB | 保留出版方排版、内嵌字体、目录、脚注、竖排和振假名等内容 |
| TXT | 自动识别 UTF-8、GB18030、Big5、UTF-16,支持手动切换编码和章节推断 |
| CBZ / ZIP | 按自然顺序展示页面,支持漫画分页 |
| 图片目录 | 将符合条件的图片目录作为漫画书,支持卷结构 |
| PDF | 基础查看,不解析成可重排正文;功能取决于浏览器或设备查看器 |
`.rar/.cbr` 不支持;ZIP64、加密和分卷压缩包不支持。reader 不提供 EPUB、TXT、PDF 之间的通用转换。
## 扫描与元数据
默认每 30 分钟定时扫描、每 60 秒检查变化,管理员可以手动触发扫描。扫描先比较大小和修改时间,再按需计算内容哈希。`.git`、`@eaDir`、`#recycle` 等目录被跳过,符号链接不跟随。
图片目录会结合图片、卷目录和压缩包结构识别。目录中包含其他书籍文件时,不应将这些书吞并为一个漫画目录;目录识别和分页资源规则见[架构](architecture.md)及 [API](api.md)。
书籍身份与内容有关,不直接依赖文件路径。移动或重命名通常保留身份,相同内容副本会合并;修改文件内容可能改变识别结果。
手动元数据覆盖保存在数据库中,优先于书内信息和文件名解析,重新扫描不会覆盖手动值。撤销手动字段后回到提取值。当前没有内置在线元数据抓取服务;远程书源插件与本地书籍元数据处理是不同功能。
## 阅读设置
字号、主题和界面设置按设备保存;阅读进度和笔记随账号同步。
| 设置 | 作用 |
| --- | --- |
| 分页 / 滚动 | 按屏翻页或连续阅读 |
| 字号、行距、页边距、对齐 | 调整正文,部分设置可选择沿用原书 |
| 字体 | 在原字体栈之前加入选定字体 |
| 主题 | 白、米黄、浅绿、夜间 |
| 亮度 | 界面叠加调暗,不等同于修改设备系统亮度 |
| 图片适配 | 固定版式内容完整显示或适宽 |
| 翻页方向 | 漫画等固定版式内容支持左右方向切换 |
| TXT 编码 | 自动识别异常时手动选择 |
点击正文中央可切换工具栏,左右区域翻页;也可使用目录跳转章节。工具栏、目录和设置面板覆盖正文,避免显隐时重新挤压正文布局。电脑端支持方向键、PageUp/PageDown;Android 可通过外壳处理音量翻页。
EPUB 章节按需加载,长书的目录与传输窗口分开处理,跨窗口跳转会先取相应章节。图标为 Lucide SVG,界面布局与交互规范见 [UI 文档](ui.md)。
## 朗读
可使用三种语音来源:Android 系统 `TextToSpeech`、浏览器 `speechSynthesis`,或管理员配置的 HTTP 语音服务。
- 从当前位置或选区开始,按句朗读并高亮。
- 支持上一句、下一句、暂停、继续和停止。
- 可调整语速、选择可用语音;音调等选项由引擎能力决定。
- 可设置章末继续下一章或停止。
没有中文语音时,先检查设备安装的语音包和当前引擎。需要服务端语音时,参照 [HTTP 朗读配置](configuration.zh-CN.md#http-朗读)。
## OPDS 与远程书源
| Tab | 用途 |
| --- | --- |
| 书源管理 | 筛选、分组、启停和查看书源状态、最近错误 |
| 订阅管理 | 添加链接、设置定时更新、立即更新和暂停 |
| 导入书源 | 导入或编辑书源 JSON |
| 书源调试 | 对指定书源搜索,查看脱敏日志和解析结果 |
搜书与管理分开。按分组或指定书源搜索时,结果通过 Streamable HTTP 陆续合并显示;并发数可在实例设置中调整,搜索过程中可手动停止并保留已收到的结果。
阅读远程章节书时,可刷新目录、配置自动追更,也可在目录中选择其他书源并确认对应章节。换源保留书籍记录、书架和旧缓存;旧笔记、书签不会自动匹配到新源章节,请核对起始章节。
## 多用户与离线
首个账号是管理员,后续成员默认由管理员创建。每个账号的书架、进度、笔记和高亮独立。阅读界面参数属于当前设备,不随账号覆盖其他设备的设置。
离线时可使用已经缓存的内容,重新连通后同步阅读状态。远程章节当前以已读取章节缓存为主,不保证尚未读取的整本书可离线。停用或卸载插件后已有缓存可能仍可读,获取新内容需要恢复来源。整书预下载、缓存配额和自动淘汰尚未实现。
## 进一步阅读
- [配置与故障排查](configuration.zh-CN.md)
- [API:目录、资源、进度、上传和来源](api.md)
- [数据边界与实现决策](architecture.md)
- [书源插件扩展](plugin-extensions.md)