Scan IO
- The walk decides whether a path is absorbed (page of a directory book,
stray image on a shelf) before stat'ing it, replacing the separate
recursive listing of every claimed directory.
- Ancestor checks are set lookups along the path instead of prefix scans
over every shelf/claimed directory (quadratic on large libraries).
- Directory handlers may provide fingerprint(); an unchanged comic folder
is settled from its listing instead of re-parsing and re-reading its
cover every scan.
Memory
- File handlers may provide parseFile(ctx, {size, contentHash}). The
scanner then hashes the file as a stream and settles identical bytes
under a touched mtime before parsing. PDF, CBZ and EPUB implement it;
EPUB metadata/cover/spine are read entry by entry through ZipArchive
(falling back to the buffered path for archives it cannot open).
parse(buf) is unchanged for acquisitions.
Synthetic library (1k shelves, 100 comics / 20k pages, one 300MB PDF):
unchanged rescan ~3.6s -> ~0.9s; first-scan peak RSS +352MB -> +78MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- comic-directory: `!isBookCollection()` negated a Promise, so chapter
folders inside a volume (第01卷/第001话/*.jpg) were never collected.
Await it, count only real book files as a collection (ComicInfo.xml
and other sidecars no longer veto a chapter), and keep the root's
loose-page pass non-recursive so volume pages are not counted twice.
- scanner: remember the handler that claimed a directory instead of
re-resolving it with Array.find over an async predicate; drop the
unused directory signature; list the tree once per scan and memoise
"holds a book file" across discovery and the shelf pass.
- ai: one splitPathBatches for jobs and direct scans; folders larger
than batchSize are split instead of overflowing a prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Books, video and audiobooks in 「继续」 are the same card (cover, work title,
episode/chapter, progress), most recent first, up to two books. An episode
resolves to its series for title and poster instead of reading "第 1 集".
- New rows 看点什么 / 听本书 / 听张专辑, each switching between 最近添加 and
随便看看 (with 换一批); hidden when the channel is empty or unavailable.
- Server: browse accepts sort=random and sort=added (newest available file
mtime across the work and its episodes/tracks; no schema change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main.ts registers the routes with a placeholder context and attaches the
services afterwards, so the stats route captured ctx.shelf while it was still
undefined and every summary failed with "Cannot read properties of undefined
(reading 'getMany')". Writes never touched the shelf and were unaffected. The
service is now built on first request; a new test builds the app the way
main.ts does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/api/v1/ai/capabilities now also answers `scan`: 启用 AI and 启用 AI 扫描 on and
Base URL, key and model filled in. 扫描与刮削 shows the AI 扫描 tab only then; while
the answer is unknown there is no tab, a ?tab=ai link falls back to 媒体库扫描,
and a server without the endpoint keeps the tab as before. The answer is
re-asked each time the page opens, so a switch changed in 系统设置 takes effect
on the next visit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading time is counted by the reader: only while the page is visible and the
reader has turned, scrolled or tapped within three minutes, or while 朗读 plays.
It is kept per book per 15-minute UTC slot and per device install, saved
locally until sent, and stored as MAX(stored, reported) in the new
reading_time table, so a replayed report never counts twice.
Media time is derived on the server from the progress saves the web player and
Android Media3 already send: the smaller of the wall-clock gap and the position
advance between two saves of one session, gaps over 30s ignored. It is stored
in media.db (media_watch_time), so neither client changes.
我的 → 时长统计 shows a stacked bar per day (one month), month (one year) or
year, with a legend that filters by kind, a table view and the works ranked for
the range or the bar picked. Administrators also get every account's totals.
Chart colours are new mode-level tokens (--color-chart-1..4), validated for
colour-vision separation on both surfaces. The React UI review now covers the
page and names the overflowing elements when a screen overflows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reading_progress keeps one row per book and last-writer-wins, which answers
"where am I" and loses "where was I": a stale device, a mistapped 目录 entry
or a clock running ahead overwrote the place for good (P0: 进度历史).
Every valid progress write now also lands in reading_progress_history,
including a write that lost the merge. A device reading on through one
chapter updates its latest entry (same chapter, within 5% and 30 minutes)
instead of adding rows; another chapter, a jump, another device or an
out-of-order write is a new entry. The newest 50 per book are kept.
GET /api/v1/sync/progress/:bookId/history lists them, newest first, for the
signed-in account only.
The table is new (CREATE TABLE IF NOT EXISTS), so the schema change is
additive and older builds sharing the database ignore it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The top bar always drew AI 总结, although AI is off by default, so most
readers pressed it only to be told AI_DISABLED (ui.md §4/§5: a control the
reader cannot use is not drawn). GET /api/v1/ai/capabilities tells any signed-in
account whether chapter summaries are enabled and configured, without exposing
the configuration; the reader asks once per book and leaves the button out
when the answer is no. Against an older server (404) the button stays.
The busy line while a summary is generated is a global notice like every other
one, instead of a hand-drawn .floating-notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- browse move/rename/mkdir/delete/metadata now require an administrator
(403 ADMIN_REQUIRED for members).
- Members may upload, but only add books: unsupported formats are skipped,
zip/cbz are stored as comic archives rather than unpacked, and overwrite is
refused. The new business setting library.memberUpload (default on) turns
member uploads off (403 UPLOAD_DISABLED); GET /library/upload reports
`allowed`.
- The library page shows 上传书籍 to members when uploads are allowed.
- Shelf tests read user_books for the owner only, now that a second user exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server: user_books gains reading_status. The shelf list computes each
book's readingState — an explicit 在读/已读 mark wins, otherwise progress
decides (>=99% 已读, started 在读), and 想读 gives way once reading starts —
filters by ?status=, and PUT /api/v1/books/:id/reading-status sets or
clears the mark.
Client: chips 全部/在读/想读/已读 and 分组 (a series or a tag from the
shelf's facets) above the toolbar, all kept in the URL; a state label on
each cover; the ⋯ menu marks 想读/在读/已读, clears the mark, or takes the
book off the shelf.
Docs: API, UI and media user guide for this and the other V3.1 gaps.
Server:
- Scan jobs record the number of files listed (total), so a running scan
can report a real percentage.
- New 扫描设置 business settings, shown as the page's 扫描与元数据 card:
自动扫描新文件 (hourly rescan of every library, default on),
刮削封面与简介 (after a completed scan, scrape items that have no online
metadata and were never attempted, with the first configured provider;
default off), 读取内嵌字幕与歌词 (default on; off hides embedded subtitle
streams and lyrics tags), 扫描隐藏文件夹 (default off: names starting
with '.' are skipped, e.g. ._film.mp4 resource forks).
- MediaAutomation runs the scheduled scans and post-scan scraping as the
first active administrator; MediaScanner gains onFinished/isScanning.
Client:
- Each library card shows its latest scan: 上次扫描 今天 09:12 beside
重新扫描; 正在扫描 · 已处理 62% with a progress bar and 取消; or the
failure (路径不可访问…) with 重试 and 查看详情 (the tasks page).
Starting or cancelling a scan now stays on the page; scan-jobs is polled.
- 扫描与元数据 card beside the list saves each switch with the settings
revision, and hides itself on servers without these settings.
Unify media secondary page chrome, loading states, playback controls, favorites filtering, and source/library screens with the legacy reader experience.
Support local and OpenList libraries, metadata, independent media navigation, themes, and audio/video playback. Preserve approved v2 prototypes and current documentation; archive superseded review artifacts and keep generated validation output outside version control.
Add book information dialogs and compact toolbar actions with source provenance. Reuse Streamable HTTP for alternative discovery and preserve cached text when a forced refresh fails.
A searchCancel deadline previously killed the shared plugin process and prevented subsequent acquisition. Release only the stop request and allow late cleanup to finish. Keep ordinary hard cancellation unchanged. Cover process and HTTP acquisition regressions and compact extension feedback without layout shifts.