给 KOReader 搭一个云图书馆:Send to KOReader + 进度同步全流程

게시일: 2026-10-12 17:41 수정일: 2026-10-12 17:41 3364자 17 min read

이 게시물은 아직 한국어 번역이 없으므로 원문으로 표시돼요
越狱 Kindle 装上 KOReader 之后,体验上最怀念的其实是亚马逊的 Send to Kindle:手机上看到一本书,点一下分享,过一会儿它就躺在 Kindle 里了。KOReader 本身没有这个能力,而且书、笔记、进度也只存在本机,换台设备就全没了。 折腾了几天,最后搭出了这样一套东西: Send to KOReader:手机上分享电子书,一两分钟后 KOReader...

越狱 Kindle 装上 KOReader 之后,体验上最怀念的其实是亚马逊的 Send to Kindle:手机上看到一本书,点一下分享,过一会儿它就躺在 Kindle 里了。KOReader 本身没有这个能力,而且书、笔记、进度也只存在本机,换台设备就全没了。

折腾了几天,最后搭出了这样一套东西:

  • Send to KOReader:手机上分享电子书,一两分钟后 KOReader 自动下载好并弹出"已收到新书"
  • 云图书馆:所有书放在自己的 WebDAV 网盘里,Kindle 上可以浏览整个云端书库,按需下载
  • 进度和笔记同步:高亮、笔记、阅读进度在多台设备之间同步,按"页面最后一个词"对齐,不同字号也能跳到同一位置

整体结构:

手机 ──分享──▶ Send to KOReader ──PUT──▶ WebDAV/书库 ◀──扫描/下载── RemoteLibrary + 自动刷新补丁
                                         WebDAV/同步 ◀──推送/拉取── AnnotationSync
                                                                     (都在 Kindle 上的 KOReader 里)

手机端 App 和 KOReader 补丁我开源在 Tsukumi233/send-to-koreader(MIT)。下面把完整配置流程和踩过的坑都记下来。

准备

组件作用备注
WebDAV 服务器存书和同步数据我用 OpenList(AList 的分支)挂百度网盘;Nginx、Apache 等也行,必须支持 PROPFIND Depth: infinity
越狱 Kindle + KOReader阅读端进度同步需要 开发版(nightly) KOReader,见下文
RemoteLibrary.koplugin把云端文件夹映射成书库v0.2.0,有两个 bug 要手动修
AnnotationSync.koplugin笔记 / 进度同步v2.0.0,有一个翻页卡顿的 bug 要手动修
Send to KOReader手机分享上传 + KOReader 自动刷新补丁Android App / iOS 快捷指令

服务器端

在 WebDAV 上建两个文件夹,书和同步数据分开放:

/dav/xxx/Kindle书库/   ← 书,RemoteLibrary 和手机上传都指向这里
/dav/xxx/Kindle同步/   ← AnnotationSync 的同步文件

建议单独开一个账号给 Kindle 和手机用,给它上传(PUT)和建目录(MKCOL)权限就够了,不需要删除权限。

KOReader 端

配置 Cloud storage+

开发版 KOReader 自带 Cloud storage+ 插件,统一管理云端账号。在 工具 → Cloud storage+ 里添加你的 WebDAV 服务器,后面两个插件都从这里选服务器。

AnnotationSync 的进度同步依赖 Cloud storage+,稳定版 KOReader 里这些选项全是灰的,只能同步笔记。想要进度同步就得用 nightly。

安装 RemoteLibrary

  1. 把 RemoteLibrary.koplugin 复制到 koreader/plugins/。
  2. 先修两个 bug(见下一节),再重启 KOReader。
  3. 在 Remote Library 设置里选上面添加的服务器,云端文件夹选 Kindle书库/,本地文件夹随意,我映射到了 /mnt/us/documents/Books。

之后在文件管理器里就能看到云端的所有书,没下载的书显示为"仅云端",点开会下载。

RemoteLibrary 的两个 bug

这两个我都提了 PR,写这篇文章时还没合并,每次更新插件后都要重新修。

bug 1:provider.lua 和 KOReader 核心模块重名

插件目录里有一个 provider.lua,Lua 的 require("provider") 会被它劫持,导致 KOReader 自带的导出等插件报错:

attempt to call method 'size'

修法:把它改名为 remotelibrary_provider.lua,再把 main.lua 里的 require("provider") 改成新名字。见 PR #5。

bug 2:WebDAV 地址带路径时手动下载 404

如果服务器地址是 https://example.com/dav 这种带路径的形式,快速扫描存下来的是服务器绝对路径(dav/xxx/…),下载时又拼了一遍地址,请求变成 /dav/dav/…,结果 404。修法是在 scanner.lua 里把地址里的路径部分剥掉,见 PR #7。

自动刷新补丁

RemoteLibrary 本身只会在你手动刷新时扫描云端,新书不会自动下载。这就是 Send to KOReader 体验的关键缺口,所以我写了一个用户补丁 2-remotelibrary-autorefresh.lua:

  1. 从仓库的 koreader/ 目录下载补丁。
  2. 放到 /mnt/us/koreader/patches/,重启 KOReader。

补丁行为:

  • 设备醒着且在线时每 2 分钟扫描一次;Wi-Fi 连上、从休眠唤醒、刚启动后也各扫一次;两次扫描至少间隔 60 秒。
  • 扫描和下载都在子进程里跑,不卡界面。
  • 只自动下载上次扫描之后新出现的书(单本上限 300 MB)。云端原有的书保持"仅云端",不会第一次扫描就把整个书库拖下来。
  • 下载完成后弹通知"已收到新书"。

补丁文件名的前缀必须是 2-。1- 前缀的补丁加载得太早,那时 G_reader_settings 还不存在。另外补丁是 hook PluginLoader:createPluginInstance 来拿 RemoteLibrary 实例的,这里的 plugin.name 是文件夹名 RemoteLibrary,不是类名。

安装 AnnotationSync

  1. 把插件复制到 koreader/plugins/,文件夹名必须是 AnnotationSync.koplugin。
  2. 先打翻页卡顿的补丁(见下一节),再重启 KOReader。
  3. 工具 → Annotation Sync → 设置 → 云设置,选 Cloud storage+ 里的服务器,文件夹选 Kindle同步/。
  4. 我用的设置:
    • 给每台设备起一个好认的名字(比如 Kindle PW6),同步菜单里好区分。
    • 同步文件名用默认的路径 MD5 哈希。如果书是 Calibre 管理的,可以改用真实文件名。
    • 打开进度同步,并打开按页面最后一个词对齐。不同屏幕、字号、边距下比按页码准得多。

AnnotationSync 的翻页卡顿

这是整套方案里最难查的一个问题。

症状

开了进度同步之后,翻页有时立刻响应,有时要等 1~2 秒,非常随机。一开始以为是 Kindle 性能问题,但看了一下负载只有 0.17,内存也很空。翻日志才发现每次进度同步都是这两行:

AnnotationSync: starting progress sync
AnnotationSync: background progress sync failed/unsupported, falling back to in-process sync

后台同步每一次都失败了,然后退回到主线程同步。

原因

插件的设计是在子进程里推送进度,但装了 Cloud storage+ 之后,Cloud:sync 并不在当前调用里同步完成,而是把下载、合并、上传整个包进 UIManager:nextTick:

function Cloud:sync(server, file_path, sync_cb, is_silent, caller_pre_callback)
    -- ...
    provider.run(function()
        UIManager:nextTick(function()
            -- download, sync_cb, upload
        end)
    end)
end

fork 出来的子进程没有 UI 事件循环,这个 tick 永远不会执行,回调不会被调用,子进程返回失败,插件就退回主线程推送:一次 GET 加一次 PUT。我在 Kindle 上用 KOReader 自己的 WebDavApi 测了一下:

GET 200 1.25s  PUT 201 1.02s  total 2.28s
GET 200 0.82s  PUT 201 1.33s  total 2.15s
GET 200 0.92s  PUT 201 1.43s  total 2.35s

每次推送卡住界面 2 秒多。推送是在最后一次翻页 3 秒后触发的,正好落在读者要翻下一页的时候,所以体感就是"随机卡顿"。

另外还有个小问题:原实现在等子进程时用 TrapWidget 盖住阅读界面,这时点一下翻页会把正在进行的推送取消掉,正常阅读节奏下推送经常被打断。

修复

  • 子进程里改用 KOReader 核心的 SyncService.sync。它是同步执行的,支持 WebDAV 和 Dropbox,.temp / .sync 握手协议和 Cloud:sync 相同。
  • 用 ffiutil.runInSubProcess fork,主进程用 UIManager:scheduleIn 每 0.25 秒轮询一次结果,60 秒超时就杀掉子进程。不再遮挡界面,翻页也不会取消推送。
  • 只有遇到 SyncService 不支持的后端(比如通过 Cloud storage+ 配的 FTP)才退回主线程推送,这和原来这些情况下的实际行为一致。

修完后推送进行中翻页,每页 0.3~0.4 秒,和没有同步时一样,也没有僵尸进程。

问题报告见 #107,修复是 PR #108,同样还没合并。在合并之前可以从我的 fork Tsukumi233/AnnotationSync.koplugin 的 fix/progress-sync-nonblocking 分支拿 remote.lua,覆盖插件里的同名文件。如果不想动代码,临时办法是把"每 N 页同步一次"调大,或者关掉进度同步。

子进程继承端口

上面两个地方都用了子进程,修完卡顿之后又冒出一个新问题:KOReader 的 HTTP Inspector 在从文件管理器切到阅读界面时报

Failed to start server: address already in use

原因是 fork 出来的子进程会继承父进程所有的文件描述符,包括 HTTP Inspector 监听 8080 端口的 socket。父进程关掉了自己那一份,但只要子进程还活着,端口就还被占着,父进程重新绑定就失败了。我抓到过自动刷新补丁的扫描子进程就占着这个端口。

修法是在子进程一开始,从 /proc/net/tcp 和 /proc/net/tcp6 里找出处于 LISTEN 状态的 socket,把继承来的对应 fd 用 dup2 指向 /dev/null。用 dup2 而不是 close,是为了让这个 fd 编号继续被占着,防止子进程里旧的 LuaSocket 对象被垃圾回收时误关一个已经被复用的 fd。其他继承来的 socket(比如 lipc 的)不动。

PR #108 和自动刷新补丁里都已经加了这一步。凡是在 KOReader 里 fork 子进程的代码,都应该做这个处理。

手机端

Android

  1. 从 Releases 下载 APK 安装(Android 10+,约 25 KB,无第三方依赖)。
  2. 打开 App,填写 WebDAV 文件夹的完整地址(比如 https://example.com/dav/xxx/Kindle书库)、用户名、密码,点"保存并测试"。文件夹不存在时会提示创建。
  3. 之后在任意 App 里分享电子书,选"发送到 KOReader"。微信、QQ 里用"用其他应用打开"。支持多文件、进度显示、失败重试。

支持 epub、pdf、mobi/azw、fb2、cbz/cbr、djvu、txt、docx 等格式。凭据只存在 App 的私有存储里,并关闭了系统备份。

Android 的 HttpURLConnection 不支持 PROPFIND 和 MKCOL 这类 WebDAV 方法,所以"测试连接"是靠上传一个探测文件再删掉来实现的。如果账号没有删除权限,云端会留下一个 .send-to-koreader-test,忽略即可。

iOS

iOS 不需要装 App,用快捷指令就能实现,同样出现在分享菜单里。核心就是对每个分享进来的文件发一个 PUT:

从 共享表单 接收 文件
文本 [文件夹地址]
文本 [用户名:密码]
Base64 编码 [文本](换行:无)
对 [快捷指令输入] 中的每个项目重复
    文本 [重复项目.名称].[重复项目.文件扩展名]
    URL 编码 [文本]
    文本 [文件夹]/[URL 编码后的文本]
    获取 URL 内容  PUT  Authorization: Basic [Base64 编码结果]  请求体: 重复项目
结束重复
显示通知 "已发送到 KOReader"

几个容易出错的地方:

  • Base64 编码一定要把换行设成无,否则长密码会被折行,认证失败。
  • 地址里有中文时先转成百分号编码。
  • 快捷指令遇到 401、404 不一定报错,第一次请去云盘确认文件真的上传了。
  • 文件夹不存在时不会自动创建,要先在云盘里建好。

逐步搭建的详细说明见仓库的 docs/ios-shortcut.md。

调试技巧和其他坑

别用 kill 杀 KOReader

改完插件想远程重启 KOReader,最直觉的做法是 SSH 上去 kill 掉再启动。千万别这么做:

  • Kindle 上端口 2222 的 SSH 是 KOReader 自己的 SSH 插件(dropbear)起的。杀掉 KOReader,SSH 会话跟着断,后面的重启命令根本不会执行,Kindle 掉回亚马逊原生界面,还可能断网。
  • SIGTERM 不会让 KOReader 保存设置,内存里没写盘的改动全丢。我就因此丢过一次刚改好的手势设置。

正确做法是在 KOReader 菜单里重启,或者开启 HTTP Inspector 插件后:

curl --noproxy "*" http://<kindle-ip>:8080/koreader/event/Restart

这样会正常保存设置,5 秒左右就回来了。如果要直接改 settings.reader.lua 之类的文件,要在 KOReader 停止运行时改,否则它下次保存设置时会把你的修改覆盖掉。

HTTP Inspector 的注意事项

  • 电脑开了代理时 curl 要加 --noproxy "*",否则代理会返回 502。开了 TUN 模式时端口探测永远显示"开放",判断 Kindle 在不在线要用 ping。
  • 不要随便浏览 /koreader/ui/menu/ 这类 UI 对象,我遇到过 HTTP Inspector 抛异常后 KOReader 直接崩回亚马逊界面。
  • 不要远程触发 /koreader/event/AnnotationSyncSyncAllIncludingUnread(全部同步)。它会弹一个确认框,在有人在设备上点掉之前,后续所有 HTTP 事件(包括 Restart)都会被挡住。

Windows 上 SSH 连不上 Kindle

Git Bash 自带的 OpenSSH 可能读不了某些格式的私钥,报 invalid format。换用 Windows 自带的 C:\Windows\System32\OpenSSH\ssh.exe 就可以。

给插件写测试

如果你也打算给这些插件提 PR:

  • 插件测试要在 KOReader 的 CI 镜像里跑(koreader/kobase:0.5.3-22.04,用 ./run_tests.sh)。我第一次没在本地跑就推了,CI 直接挂。
  • ffiutil.sleep 只接受整数秒,sleep(0.3) 一点都不等,亚秒级等待用 ffiutil.usleep。
  • 单元测试里 fastforward_ui_events() 会因为没有显示中的 widget 而调用 UIManager:quit(),把任务队列清空。需要轮询的测试要自己捕获 scheduleIn,然后手动执行轮询。

维护清单

在上游合并 PR 之前,每次更新插件后检查一遍:

插件要重新打的补丁上游 PR
RemoteLibraryprovider.lua 改名#5
RemoteLibraryscanner.lua 路径修复#7
AnnotationSyncremote.lua 非阻塞推送 + 释放继承的监听 socket#108

自动刷新补丁放在 patches/ 里,不受插件更新影响。

搭完之后的日常就是:手机上刷到一本书,分享,等一两分钟,Kindle 上"已收到新书"。换一台装了 KOReader 的设备打开同一本书,进度和高亮都在。总算找回了当年用 Send to Kindle 的感觉。

마음에 드신다면, 댓글을 남겨주세요~

© 2023 - 2026 Tsukumi233 @Der Mond das Meer
Powered by theme astro-koharu · Inspired by Shoka