Skip to content

About

豆备 (Doubak) 的对外导出适配器:结构化数据 → NeoDB / Letterboxd / Goodreads 的导入文件。纯函数,不联网,零依赖。

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

48 Commits

Folders and files

Repository files navigation

doubak-export-adapters

test Coverage Status

本仓库为项目源码。 项目官方主页请访问 https://doubak.com。

豆备(Doubak)的对外数据导出适配器。负责将 解析器(doubak-data-parser) 产出的标准 canonical 数据转换为面向 NeoDB、Letterboxd 与 Goodreads 的导入文件。

node bin/export.js <canonical 目录> [输出目录] [--account=<uid>] [--all-accounts] [--target=…] [--sample=N] [--no-shelf-history]
#              [--visibility=0|1|2] [--notes-visibility=0|1|2] [--unknown-visibility=0|1|2]
node tools/check-export.mjs <canonical 目录> <导出目录>       # 上传前离线自查
node tools/check-roundtrip.mjs <导出目录> <NeoDB 导出的 zip>  # 导入后与服务端导出数据的比对核验
npm test                                                     # 运行测试套件(使用 Node 内置 test runner,零外部依赖,214+ 个测试)

--target 参数可选 neodb(推荐的 NDJSON 格式)、neodb_csv(旧版 CSV 格式)、letterboxd 与 goodreads;若未显式指定,默认同时产出 neodb、letterboxd 与 goodreads(neodb_csv 需显式指定)。

可见性采用三级层级控制,每一级的 0 均表示「继承上一级」,遵循「仅收紧、不放松」的继承安全原则:

--visibility          所有 NDJSON 记录的默认基础可见性(默认 0,即公开)
  └ --notes-visibility    日记记录专用的可见性控制(默认 0;影评与书评不受此项影响)
      └ --unknown-visibility  针对未能识别可见性状态的日记(默认 2,即仅提及者可见)

--notes-visibility=0 表示继承总基线:例如 --visibility=1 --notes-visibility=0 会使日记与其他记录同为仅关注者可见;同理,--unknown-visibility=0 继承日记专用的可见性级别而非总基线,避免无法识别的日记意外扩大公开范围。

上述三个开关均无法覆盖「用户在豆瓣上主动设定为私密」的日记(该项强制锁定为 2)——避免提供绕过隐私保护机制的安全后门。详情参见下文「在豆瓣上『仅自己可见』的日记」章节。

日记独立可见性档位的引入源于实际使用场景:早期版本中部分用户将日记导入 NeoDB 后发现全部呈现为公开状态,耗费大量时间逐篇手动调整或删除。若为了保护少数私密日记而将全局设为 --visibility=2,会导致数千条常规标记记录一同受限。扩展面板中亦提供了对应的常驻单选设置(公开/私密,默认公开,并明确提示“将转换为公开文章并在联邦网络传播”)。

运行环境要求 Node ≥ 20。全流程纯离线运行,不发起任何网络请求 —— 产物为本地文件,何时上传或是否上传均不影响已生成的归档。

node bin/export.js ~/downloads/canonical ~/downloads/export

执行后产出目录中除了各目标平台的文件外,还会附带一份 怎么导入.md,详细列出本次导出的具体条目统计与各平台的实际导入步骤。完整的首次导入检查流程另见 docs/manual-testing.md。建议初次使用时先通过 --sample=20 抽取样本并在目标平台小范围验证,确认无误后再执行全量导入。

多账号防线与子目录自动识别

输出目录命名:默认单账号及显式 --account <uid> 均直接写入指定输出目录,不自动追加 UID。--all-accounts 批量分流多个账号时使用 <输出目录>/<uid>/,内部文件名保持原格式;单个输入数据集只有一个账号时仍保留原目录布局。

  • 子目录自动定位:输入的 <canonical 目录> 可以直接指向 canonical 根目录,也可以指向包含多个 canonical 存档的上层父目录,导出器会自动向下探测。
  • Fail-Fast 防污染铁律:如果 Canonical 数据中检测到混杂了多个不同账号的记录且未指定导出目标,导出器会立即报错中断(退出码 1),杜绝将两个不同账号的历史合并导出至同一份文件、进而污染第三方平台的可能。
  • 单账号隔离(--account <uid>):只筛选并导出指定 UID 的标记、动态、长文与豆列。
  • 批量自动分治(--all-accounts):自动按账号分流,分别导出到 <输出目录>/<uid>/ 下各自独立的导入包中。

三个平台不是一回事

以包含 2950 条有效标记的真实归档样本为例,三个平台对不同数据维度的接收支持能力对比如下:

数据维度 归档实际包含 NeoDB (NDJSON) NeoDB (CSV) Letterboxd Goodreads
电影 1470 ✅ ✅ ✅ ✖︎
剧集 641 ✅ ✅ ✖︎ ✖︎
图书 145 ✅ ✅ ✖︎ ✅
音乐 84 ✅ ✅ ✖︎ ✖︎
游戏 605 ✅ ✅ ✖︎ ✖︎
舞台剧 5 ✅ ✅ ✖︎ ✖︎
书评影评 2 ✅ ✅ ✖︎ ✖︎
日记 3 ✅ ✖︎ ✖︎ ✖︎
豆列 6 ✅ ✖︎ ✖︎ ✖︎
状态历史 3179 ✅ ✖︎ ✖︎ ✖︎
标签 ✅ ✅ ✅ ✅ 转换为书架
相册 — ✖︎(不支持) ✖︎ ✖︎ ✖︎
小组话题与加入的小组 — ✖︎(不支持) ✖︎ ✖︎ ✖︎

注:相册与小组为豆瓣特有的个人相册空间与社区模块,第三方书架/文化条目记录平台(NeoDB、Letterboxd、Goodreads)在数据模型上均无对应实体支持(豆列中若包含小组/照片/人物也会被自动滤出至旁挂清单)。这部分数据完整保留于标准 canonical 结构化数据与 Markdown 静态站点中。

「状态历史 3179」是归档中实际收录的条目数 —— 3411 条广播中有相应数量能够关联至特定作品。写入最终 ZIP 文件的为 2519 条:其中 1688 条与标记条目的初始状态重合,已直接并入标记所在行(补上当时的评分与短评);另有 657 条广播未附带评分或文本内容,依规整略过。详见下文「标记自己就会生成一条历史」章节。游戏条目同理:归档中收录 605 个,实际导出 598 个,差额的 7 个为豆瓣官方已彻底下线且未保留有效链接的条目。

一次真实导出的标准终端输出示例:

NeoDB  → neodb/neodb-ndjson-import.zip  (NDJSON)
  标记 2961 条(game 603 · movie 1477 · tv 646 · book 145 · performance 5 · music 85)· 评分 1795 · 短评 2127
  标签 717 个(贴了 7258 次)· 书评影评 2 篇 · 笔记 0 篇
  豆列 6 份(73 条) · 不挂作品的日记 4 篇 · 条目 2997 个
  状态历史 2539 条(从广播还原,豆瓣自己已经不显示了)
  · 其中 1703 条是并进标记那一行的(标记本身那件事,NeoDB 导标记时自己就会建一行,我们只把当时的星和短评补上去)
  · 另有 659 条广播什么都没冻住(没星也没字),整条略过
  · 1 篇日记是被豆瓣锁成「仅自己可见」的,这一份里按公开导入——它本来就是公开的,是豆瓣把它关掉的
      · 想看的被河蟹的电影
    ⚠ NeoDB 的 Article 会联邦出去,这一步撤不回来。要收起来:--visibility=2
  · 1 篇日记在豆瓣上不公开,写成 visibility=2(仅提及者)——东西照样在你账号里,只是不对外
      · 测试一下私密日记?(你自己设的)
  ⚠ 5 条没读到详情页,分不出电影还是剧集,按电影处理
  ⚠ 5 条连豆瓣链接都没有(条目已被豆瓣删除),没有放进 zip——见 neodb-needs-check.csv
  ⚠ 8 条没有标记日期,这几条不写 published
  ⚠ 豆列里有 61 条不是作品条目(影评/小组/人物/照片…),收藏单装不下

Letterboxd → letterboxd/
  看过 953 部 · 想看 509 部(含在看 0 部)
  跳过 剧集 641 · 非影视 839(Letterboxd 只收电影)
  ⚠ 8 条没读到详情页,分不清是电影还是剧集,没有导出
  ⚠ 34 部没有 IMDb 号,见 letterboxd-needs-check.csv
  ⚠ 4 条的标签里有逗号,Letterboxd 会按逗号拆成多个标签

Goodreads → goodreads/
  图书 145 本(读过 45 · 在读 18 · 想读 82)

「无法导出的条目及其成因统计」与「成功导出的数据」具有同等的工程价值。 导出报告会统计因平台能力限制而无法导出的条目数量,并针对部分需要人工处理的异常生成明细文件(如 neodb-needs-check.csv 与 letterboxd-needs-check.csv),避免静默遗漏。

NeoDB 那一路现在出 NDJSON,CSV 降成 --target=neodb_csv

根据 NeoDB 官方维护者的反馈建议:

可以生成NDJSON吗?旧的CSV格式只是为了兼容NiceDB和Doufen,限制太多了。

旧版 CSV 格式主要是为了兼容第三方历史工具而保留的过渡形态。NeoDB 原生支持其自身使用的 NDJSON 归档导入格式,能够承载 CSV 在结构上无法支持的四项关键数据:豆列收藏单、不挂载具体作品的独立日记、每条记录的细粒度可见性状态,以及状态演变历史。

其中状态演变历史尤为重要:豆瓣个人主页仅呈现作品当前的最终标记状态,但广播动态在发布时即被固化,因此 3411 条历史广播中完整记录了从「想看/想读」到「在看/在读」再到「看过/读过」的时间轨迹与当时评分。实测在 2373 个作品条目中包含此类历史记录,其中 678 个条目的标记状态经历过多次流转。默认情况下系统会自动导出此部分数据(从 2026-08-25 起默认启用;可通过 --no-shelf-history 显式关闭)。这一默认设定基于容错成本的权衡:不导出将导致历史流转信息永久遗漏,而导出的 ShelfLog 仅作为书架背景历史,不影响当前标记状态、不触发时间线广播亦不会对外联邦传播,且导入逻辑具备幂等性。

旧版 CSV 导出格式仍完整保留,需通过 --target=neodb_csv 显式启用。

两处倒退,说清楚

相较于旧版 CSV,NDJSON 格式在特定场景下存在两处功能权衡:

  • 缺少基于 ISBN / IMDb 文本的 info 兜底匹配:parse_catalog 接口调用的是 get_item_by_info_and_links("", "", links) —— 在标题与 info 为空时,仅依据 URL 链接执行条目匹配。在旧版 CSV 模式下,info 列中的 isbn: 能够辅助找回在豆瓣已下线的图书;而在 NDJSON 中,仅有具备标准 URL 格式的外部标识(如写入 external_resources 的 IMDb 链接)可被识别,ISBN 无法通过纯文本注入。

  • 上传界面缺少可见性选项:NeoDB 的 data.html 页面在检测到 NDJSON 文件时会隐藏全局可见性单选项,服务端 request.POST.get("visibility", 0) 将默认置为 0(按公开处理)。因此可见性配置需在导出阶段由文件内部指定:支持通过 --visibility=1(仅关注者可见)或 --visibility=2(仅提及者可见)进行预先声明。

  • 对于删除后重新标记的作品,导出时仅保留最新有效的一条:在豆瓣上若删除某条标记后重新添加,会生成全新的条目 ID,解析器会忠实记录为两条历史事件 —— 在 canonical 事件日志中这是完全正确的。然而导出目标反映的是当前最终状态,一个作品在书架上应仅对应一行记录:若不执行合并,实测《盗梦空间》会导出两条 ShelfMember;NeoDB 对同一作品只有一个书架条目,后写入的记录会覆盖先写入的记录,覆盖顺序取决于文件先后而非语义判据。合并判定的依据是「最后一次观测到的时间戳」(last_observed_at)而非用户标记日期 —— 补标老电影可能标注较早的历史日期,但该记录依然是当前现存的有效条目。合并详情会在导出报告中明确提示(更早期的短评与标签仍完整保留于 canonical 原始归档中)。

  • 在豆瓣上「仅自己可见」的日记,六种情形,两种处置(参见 #4):「仅自己可见」在页面表现上一致,但成因截然不同:

    豆瓣上的状态 判定依据 导出结果
    公开日记 存在隐私容器,且无私密标记 继承基线配置
    用户主动设为私密 包含私密标记,且无平台系统拦截通告 visibility=2
    豆瓣平台锁闭 包含私密标记 + notice-info-type-4 违规通告 继承基线配置
    无法识别 无法唯一定位隐私容器(豆瓣网页模板调整) visibility=2(可通过 --unknown-visibility 微调)
    历史老版本 canonical 缺少上述三项可见性字段(解析器 0.12.0 之前生成) 同上,默认收紧为 visibility=2
    影评 / 书评 页面结构不适用隐私容器划分 继承基线配置

    「基线配置」由 --visibility 参数控制(默认 0 即公开),而非强制不可修改的公开:若用户显式传入 --visibility=2,被平台锁闭的日记也会同步收紧。

    平台锁闭与作者私密的语义差异:被豆瓣官方锁闭的日记之所以处于“仅自己可见”状态,是因为其原本为公开发布的内容,被平台因审查机制进行了后置隐藏;将其导出并在开放的去中心化网络中恢复,符合数据备份的核心意义。系统在导出报告、怎么导入.md 以及扩展界面中均会明确提示此类文章的标题与联邦分发提醒。

    作者主动隐藏的内容则采取绝对保护:用户在源站主动设置为私密的内容,系统默认设为受限(visibility=2),数据完整进入个人账号,但不对外广播分发,由用户在目标平台自行决定是否公开。

    针对未识别状态与老版本归档的保守策略(--unknown-visibility=0|1|2,默认 2):默认向隐私保护倾斜。若用户明确了解该部分日记的公开属性,可通过参数调整;其中 0 表示跟从基线配置而非直接公开。无论如何微调,用户主动私密的内容均严格维持私密属性。

    遇到无法识别的页面结构时的反馈机制:当捕获页面无法定位隐私容器时,CLI、使用指引文档及扩展导出面板均会统一提示排查链接(指向固定的 FEEDBACK_URL),方便用户反馈适配。此类异常通常仅在豆瓣服务端调整页面模板时触发,补齐解析规则重新运行即可恢复。

    影评与书评的缺省处理:影评书评在豆瓣原生架构中不提供私密设置功能,页面中不存在隐私容器,因此 canonical 中该字段统一置为 null,导出时直接跟从基础公开基线。

content.updated:第二次导入能不能认出「你改过了」

上游于 2026-08-23 为 NDJSON 引入了 content.updated 字段支持,导入器的 _is_current 逻辑优先比对目标记录的 edited_time 与更新时间,仅在无法比对时退回比对 created_time 与 published。

传统仅比对发布时间的缺陷:豆瓣的 marked_at 仅代表初始标记时间,用户后续编辑短评或评分时不会推进该时间戳。若第二次导入时 published 未发生变动,系统会误判目标记录已是最新的,导致后续的短评编辑无法生效更新。

canonical 架构基于字段哈希摘要生成修订版本,从最新修订沿链向前追溯,只要字段内容未发生变动则持续回溯,终止点修订的 first_observed_at 即代表该内容被首次观测到的真实时间戳 —— 这正是 content.updated 所需的精确语义。系统严禁使用 last_observed_at(该时间戳随每次抓取更新,会导致每条记录均被误判为编辑)。

按字段独立判定更新时间:各字段(短评、评分、状态)独立计算 updated 时间戳。若用户仅修改了短评内容,只有短评相关的记录会更新时间,状态与评分的时间戳保持不变,避免伪造未发生的修改。

系统仅对真正消费该字段的七类记录(Collection、ShelfMember、Article、Review、Note、Comment、Rating)注入该属性,其他无关实体保持干净。同时修正了豆列 published 字段的取值逻辑,改用最早观测时间以防止重复导入时新建冗余收藏单。

标记自己就会生成一条历史

该项机制基于对真实 NeoDB 服务运行特性的分析与适配:

NeoDB 在执行 import_shelf_member 时会调用 ensure_log_entry(),基于 (owner, shelf_type, item, created_time) 自动为当前标记状态生成一条 ShelfLogEntry,并写入当下的评分与短评。

豆瓣的 marked_at 仅包含日期信息(固定记录为 00:00:00+08:00),而广播动态包含精确到秒的时间戳,两者并不相等。由于 ShelfLogEntry 包含时间戳维度,若直接分别写入,会导致同一天的同一次操作在时间轴中呈现为重复的两行。若跨时区渲染,甚至可能被误判为隔日重复标记。

为此,系统将与当前标记操作吻合的广播动态(相同状态且属于同一天)合并到标记对应的历史记录,统一对齐时间戳,使后置逻辑命中同一条记录,并将广播中保留的评分与短评完整注入。在真实全量数据验证中,约四分之三的历史广播均通过此机制完成合并。

关键安全保障细节:

  • 避免元数据覆盖丢失:采用标记当前数据垫底、广播快照增量补充的合并策略;
  • 过滤无有效内容的空广播:若广播未附带独立的文字或评分,系统不生成额外的空记录,避免覆写抹除标记中已有的评语。

从服务器把数据导回来对一遍

tools/check-export.mjs 负责在上传前执行离线比对,确保导出的中间文件与源 canonical 数据的一致性;而 tools/check-roundtrip.mjs 则用于执行端到端双向往返验证,比对服务端最终落盘数据是否与导出预期完全一致:

node tools/check-roundtrip.mjs <我们的导出目录或 zip> <NeoDB 导出的 zip>

在 NeoDB 的「设置 → 数据 → 导出」中获取服务端的全量 NDJSON 归档后,工具可自动执行逐条字段核验。在 17305 条全量记录的生产测试中,验证了条目数量、评分短评、标签关联及自动补齐的状态历史均实现严格对齐:

条目             我们  2979 个 · 服务器 2979 个 · 对上 2979 个
标记             我们  2943 条 · 对上 2943 条
                      其中 8 条档案里没有标记日期,服务器拿导入那一刻当日期(不比日期)
评分             我们  1788 条 · 对上 1788 条
短评             我们  2110 条 · 对上 2110 条
标签             我们   712 条 · 对上 712 条
标签关联         我们  7222 条 · 对上 7222 条
书评影评         我们     2 条 · 对上 2 条
不挂作品的日记   我们     3 条 · 对上 3 条
豆列             我们     6 份 · 对上 6 份
状态历史         我们  2519 条 · 对上 2519 条 · 服务器自己建的 1255 条(导标记时顺带的)
                      多出来的 1255 条都对得上标记本身那件事,没有一条是重复的

条目总数精确匹配:服务器多出的 1255 条历史完全等同于标记当日无广播动态的 1247 条补齐项加上 8 条缺少日期的条目。

核心核验准则:

  • visibility 值为 0 时,导出文件省略该键,服务端解析自动缺省为 0;
  • 针对源数据缺失标记日期的少数条目,系统在比对报告中单独说明而不计入异常阻断列表;
  • 服务端导出的 actor.ndjson 包含账号私钥凭据,严禁将其直接提交至公开代码仓库或测试用例中。

五个静默的坑

根据 NeoDB 导入器实现(journal/importers/ndjson.py)规范,规避了五处容易静默引发数据异常的结构差异:

  1. ShelfLog 数据结构位于顶层:字段直接读取 item / status / timestamp,而非嵌套于 content 结构内;
  2. ShelfMember 不包含标签字段:NDJSON 格式需显式输出 Tag 与 TagMember 实体,否则会导致标签关联静默丢失;
  3. progress 具备三态语义:键不存在表示保持现状,null 会重置用户手动填写的阅读进度,有值时更新;因豆瓣不记录进度,该键统一省略;
  4. 舞台剧广播类型标识规范:广播模型中的 target_type 应映射为 loc 而非 drama;
  5. 豆列中特定游戏分类的 URL 修正:豆列收录中分类为 3114 且 URL 模式为 /subject/<id>/ 的条目实际为游戏作品,需规范化映射至 /game/<id>/ 以确保站点规则识别。

上述关键契约在 test/neodb-ndjson.test.js 中均通过静态测试进行强制校验。

跟 CSV 那一路对一遍

为验证新旧格式间的行为一致性,系统使用已在线上全量验证过的 CSV 数据作为基准参照:

对齐测试验证表明,在作品总数(2943 对 2943)、状态、评分、短评、标签、分类及外部链接等方面,NDJSON 与 CSV 输出保持逐条逐字段完全一致。

  • tools/check-export.mjs 在同时检测到两个目标目录时会自动执行交叉比对;
  • test/neodb-ndjson.test.js 对测试 Fixtures 保持同样的基准检查。

需要说明的是,分类比对一致性验证的是输出逻辑与目标平台规则表的匹配性,并不直接代表豆瓣源数据中电影与剧集划分的绝对准确性。

三件故意不做的

系统在设计上有意不执行以下操作:

  • 不生成 actor.ndjson:避免意外覆写用户在目标平台已配置的个人昵称与自我介绍;
  • 不导出 attachments/ 多媒体文件:多媒体资源存储于原始 WARC 归档中,适配器保持纯离线运行,文章正文中的媒体引用保持源站原始链接;
  • 不生成 posts 嘟文记录:上游暂未开放广播至社交动态的导入支持。

NeoDB 不需要 API,也不需要谁批准

项目早期曾将 API 作为主要导出目标,实际 NeoDB 原生支持基于标准 ZIP 压缩包的本地数据导入(journal/importers/csv.py): 无需申请 OAuth 凭证、无调用频次限制,完全避免了为离线工具引入动态的外部网络依赖。

上游协同进展:

  • 在 NeoDB 官方仓库提交了文档与页面指引 PR(neodb#1810),引导用户了解数据导入规范;
  • 确认当前版本完全兼容 NeoDB 现有的「导入 NeoDB 备份」入口,无需等待上游功能合并即可直接使用。

列的形状是读源码定的,不是读文档定的

各目标平台的导入格式出处均直接对齐自其开源代码实现或权威规范:

目标平台 规范与源码出处
NeoDB(NDJSON) journal/importers/ndjson.py + exporters/ndjson.py
NeoDB(CSV) journal/importers/csv.py + exporters/csv.py
Letterboxd 官方导入文档 + 经实测验证的标准列字段
Goodreads 兼容 Goodreads 官方导入规范的 14 项标准字段定义

代码核验明确的关键细节:

  • NeoDB 书评 CSV 的表头必须保留两个 title:csv.DictReader 会让后一个同名列覆盖前一个,因此 row["title"] 实际只读取第 5 列的书评标题;第 1 列作品名不参与匹配,作品通过 links 定位;
  • NeoDB 文件名分类主要用于路由分发,条目匹配以链接为准,但文件名必须匹配官方预设的枚举集合。

豆瓣的「电影」里三成是剧集

在豆瓣数据模型中,电影与连续剧统一归属于影视分类。在 2111 个影视条目样本中,实测有 641 个条目实际为电视剧集。

适配器依据条目元数据中是否包含 集数、首播、季数 字段执行启发式识别(三项指标的覆盖率分别为 627、640、273,合并并集为 641)。

平台处理策略差异:

  • NeoDB 依据链接在后台执行条目关联,分类容错率较高;
  • Letterboxd 仅收录电影,若提交剧集可能导致同名电影误匹配。因此对于未能获取详情页、无法明确区分分类的影视条目,Letterboxd 适配模块会选择主动跳过并输出至 letterboxd-needs-check.csv,避免污染观影数据。

电影与剧集的判定:基于 NeoDB 全量目录对齐校验

针对全量 2111 条影视记录执行的比对分析显示,本地分类规则与 NeoDB 全量目录的一致性达 98.8%(仅 25 条存在差异):

  • 3 条缺失详情页:因元数据不足按安全策略降级,已列入核对清单;
  • 8 条为电视特别篇或 OVA(如特别篇、完结篇动画等):豆瓣采用电影模板展示,而 TMDB 及 NeoDB 多挂载于母剧集下,属于平台分类边界的客观差异;
  • 14 条为元数据格式边缘情况(如部分电影误标“首播”,或系列动画剧集分类差异)。

比对结果表明分类规则具备坚实的客观依据,差异反映了不同元数据体系对边缘条目的判定取舍。

匹配全靠 IMDb 和 ISBN,中文标题帮不上忙

Letterboxd 与 Goodreads 均使用英文条目库,中文标题无法用于有效检索:

  • IMDb:实测在电影(97%)与剧集(92%)中保持高覆盖率;
  • ISBN:实测图书条目达到 100% 覆盖;
  • 标题输出:针对外文作品提取原始标题字段输出;
  • 导演信息:因中文译名无法与外文数据库精确匹配,系统避免在导出中写入不匹配的中文导演文本,防止破坏基于标题与年份的模糊匹配效果。

三处「宁可少送」

为避免向目标服务提交未经验证的推断数据,适配器遵循三项保守设计原则:

  • Letterboxd 的已看与想看清单拆分输出:分别生成独立文件,防止清单混淆;
  • Goodreads 仅对已读条目输出 Date Read:避免将标记行为的日期误标为阅读完成日期;
  • 未评分条目输出空值而非 0 分:明确区分“未打分”与实际的数值评分。

导出是有损的,损失的正是这个项目的卖点

canonical 归档的核心优势在于完整保留了版本演变历史与可溯源的捕获证据。外部平台导入规范大多仅接收最终的静态快照,因此导出过程必然会执行截断处理(保留最新的版本记录)。即使仅有少量条目存在多版本修订,系统亦会在导出报告中明确披露此项信息。

还没做的

  • Letterboxd 与 Goodreads 生产端全量往返验证:两家平台的适配目前基于格式规范与小样本采样测试,待进行进一步的大规模双向回验;
  • NeoDB 生产环境验证状态:已基于 40 条样本及全量 17305 条记录完成端到端导入并验证通过(详见公开用户主页 https://neodb.social/users/immewx/);
  • 豆列成员短评导入验证:已确认通过 ItemList.append_item 的元数据扩展机制实现支持;
  • 重看标记与书籍装帧:因源数据缺少直接对应字段,暂不作推断输出。

零依赖

项目遵循纯 ES 模块规范开发,使用 JSDoc 注解与 Node.js 原生测试运行器,保持零外部运行时依赖与零编译步骤。内置确定性 ZIP 打包组件,直接通过系统的 unzip -t 命令执行兼容性验证。

同一份代码也跑在浏览器扩展里

扩展面板中集成的 NeoDB 导出功能复用了本仓库的核心模块(src/vendor/export-adapters/),并通过 CI 保持双向代码同步。

模块设计实现了输入输出与业务逻辑的解耦:

  • record.js 专注于内存记录的纯计算,与文件系统读写解耦;
  • zip.js 仅维护文件格式封装,具体压缩算法支持由环境注入(Node 环境调用同步接口,浏览器环境接入 Web Streams API)。

静态契约测试(test/portable.test.js)确保所有跨端模块均不引用特定平台的内置依赖,实现安全的可移植性。

About

豆备 (Doubak) 的对外导出适配器:结构化数据 → NeoDB / Letterboxd / Goodreads 的导入文件。纯函数,不联网,零依赖。

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages