Atsumeru 1.1 API 使用详解

Atsumeru 1.1 REST API 全端点实测详解:HTTP Basic 认证与首次调用、server/users/settings/importer/books/categories 六域逐端点请求示例与真实回显、分页过滤排序通用约定、categories/set JSON 500 与 cover/download 空 200 两则排障实录(2026-11-01 实测)。

AtsumeruAtsumeru漫画媒体服务器NAS 自托管2026/11/01

Atsumeru 1.1 API 使用详解

目标:把 Atsumeru 1.1 的 REST API 逐域讲透——认证方式、六大端点域(server / users / settings / importer / books / categories)每个 endpoint 的方法、参数、请求示例与真实响应回显,外加分页/过滤/排序通用约定与两则实测排障实录。全部请求在 2026-11-01 对本专栏部署篇拉起的 atsumeru-11-web 服务(宿主端口 8138)实测;部署过程见 《Atsumeru Docker Compose 部署指南》,配置项语义见 《Atsumeru 1.1 配置参数详解》,组件背景见 《Atsumeru 介绍与选型指南》。

一、环境信息

  • 被测服务:Atsumeru v1.1(/api/server/info 实测 "version":"1.1","version_name":"Bohrium"),监听 31337、宿主映射 http://localhost:8138
  • 认证形态:纯 HTTP Basic(源码 WebSecurityConfiguration 仅启用 httpBasic(),1.1 无 token/登录端点,详见部署篇 4.3 排障实录)
  • 演示账号:管理员 Admin / q1w2e3r4、普通用户 root / q1w2e3r4(普通用户用于 403 越权实测)
  • 测试数据:书库内 1 个系列 atsumeru-serieb71204461360651e64a359ad591269ac(标题 "Volume 1")与 1 个档案 atsumeru36fae69422e431620cadd1b555d248c9(3 页 CBZ,1384 字节),由部署篇导入流程产生
  • 取证方式:curl 逐条实测(响应回显逐行照抄,过长 JSON 标注节选),端点与参数语义用 jsDelivr 镜像源码 Atsumeru-xyz/Atsumeru@1.1 的 6 个 Controller 交叉核对;1.1 镜像无 Swagger/OpenAPI 可导出,端点图完全来自源码 + 实测
  • 查询日期:2026-11-01

二、前置条件

  1. 服务已按 部署篇 拉起且健康,curl -s -o /dev/null -w '%{http_code}' -u Admin:q1w2e3r4 http://localhost:8138/api/server/ping 返回 200;
  2. 已掌握管理员凭据(本环境为 Admin / q1w2e3r4);除 /api/v1/users/me 与 books 读取类端点外,绝大多数管理端点要求 ADMIN 角色;
  3. 客户端工具仅需 curl;jq 可选(本篇响应回显均为原始输出,未过 jq)。

三、API 总览

维度约定依据
协议/格式HTTP/1.1,请求与响应均为 application/json;charset=UTF-8(图片/档案端点返回二进制流)实测响应头
版本前缀业务端点在 /api/v1/ 下;server 域例外,为 /api/server(无版本号)源码 @RequestMapping
认证HTTP Basic,401 时响应头带 WWW-Authenticate: Basic realm="Realm"实测
授权类/方法级 @PreAuthorize:hasRole('ADMIN')、hasRole('ADMIN') or hasAuthority('IMPORTER') 等源码
限流无(Spring Security 未配置限流;1.1 无 API Key 机制)源码 + 实测
错误信封管理写操作统一返回 {"code":<int>,"message":"..."};未捕获异常返回 Spring Boot 默认错误 JSON(timestamp/status/error/path)实测
文档1.1 无 Swagger UI/OpenAPI(WebUI 与 Swagger 属 2.x jar 发行版)A1 版本线说明

端点域速查(全部实测或源码取证):

域前缀端点
server/api/serverGET /ping、GET /info、GET /clear_cover_cache
users/api/v1/usersGET /me、GET /list、GET /constants、PUT /create、PATCH /update、DELETE /delete?user_id=
settings/api/v1/settingsGET /get、POST /update
importer/api/v1/importerGET /status、GET /list、POST /add、DELETE /remove/{hash}、GET /scan、GET /rescan、GET /rescan/{hash}
books/api/v1/booksGET ""、GET /filters、`GET
categories/api/v1/books/categoriesGET ""、PUT /create?name=、PATCH /edit?id=&name=、POST /set、POST /order、DELETE /delete?id=;另有 GET /api/v1/books/metacategories

四、认证与首次调用

4.1 无凭据与错凭据(401 行为)

不带凭据访问任意 API,返回 401 且响应头携带 Basic 质询:

curl -s -i http://localhost:8138/api/v1/users/me
HTTP/1.1 401 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Set-Cookie: JSESSIONID=9ADBC636D9C922BEF3F27C2A4DE54CE3; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 01 Nov 2026 11:46:50 GMT

密码错误同样只有裸 401(无错误 JSON 信封):

curl -s -o /dev/null -w '%{http_code}\n' -u Admin:wrong123 http://localhost:8138/api/v1/users/me
401

4.2 存活探测与版本确认

curl -s -i -u Admin:q1w2e3r4 http://localhost:8138/api/server/ping
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 01 Nov 2026 11:46:51 GMT

ping 成功即 200 空体(源码 ResponseEntity.ok().build())。取版本与统计:

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/server/info
{"name":"Atsumeru","version":"1.1","version_name":"Bohrium","has_password":true,"debug_mode":false,"stats":{"total_series":1,"total_archives":1,"total_chapters":0,"total_categories":1}}

五、各 API 域详解

5.1 server 域(/api/server)

端点方法权限说明
/pingGETBasic200 空体存活探测
/infoGETBasic名称/版本/统计
/clear_cover_cacheGETADMIN清空封面缓存目录并后台重建

注意 clear_cover_cache 是 GET(源码 @GetMapping),且会删除整个 /app/cache 目录后异步重存封面:

curl -s -i -u Admin:q1w2e3r4 http://localhost:8138/api/server/clear_cover_cache
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 01 Nov 2026 11:48:24 GMT

{"code":200,"message":"Cache cleared"}

5.2 users 域(/api/v1/users)

类级 @PreAuthorize("hasRole('ADMIN')"),但 /me 对任何已认证用户可用(返回调用者自身)。

查自己 / 查全员(password 字段返回的是 BCrypt 散列,非明文):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/users/me
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/users/list
{"id":1,"user_name":"Admin","password":"$2a$10$ne7i/OgxDu6S0HnSUmVlBuZX/XE.l5L93adFeGMOLIPMFEPV.mc26","roles":["ADMIN"],"authorities":["ROLE_ADMIN"]}
[{"id":1,"user_name":"Admin","password":"$2a$10$ne7i/OgxDu6S0HnSUmVlBuZX/XE.l5L93adFeGMOLIPMFEPV.mc26","roles":["ADMIN"],"authorities":["ROLE_ADMIN"]},{"id":2,"user_name":"root","password":"$2a$10$acVoMoHHYm6ft0h6FnHrRenRVgKlTIbtNZinu2BOTMYGVwuDi04.u","roles":["USER"],"authorities":["ROLE_USER"]}]

常量枚举 /constants——roles/authorities/分类/流派/标签全集,建用户前先取合法值(输出较长,节选至流派枚举中段):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/users/constants
{"roles":["ADMIN","USER"],"authorities":["IMPORTER","UPLOADER","METADATA_UPDATER","DOWNLOAD_FILES"],"categories":[{"id":"atsumeru-category966faeabbc679ba4457e2d75e031d487","name":"Unsorted","content_type":"UNKNOWN","order":0}],"genres":[{"name":"Action","id":0},{"name":"Adult","id":1},{"name":"Adventure","id":2},{"name":"Comedy","id":3},{"name":"Doujinshi","id":4},{"name":"Drama","id":5},{"name":"Ecchi","id":6},{"name":"Fantasy","id":7},{"name":"Gender Bender","id":8},{"name":"Harem","id":9},{"name":"Historical","id":10},{"name":"Horror","id":11},{"name":"Josei","id":12},{"name":"Magic","id":13},{"name":"Martial Arts","id":14},{"name":"Mecha","id":15},{"name":"Mystery","id":16},{"name":"One Shot","id":17},{"name":"Psychological","id":18},{"name":"Romance","id":19},{"name":"School Life","id":20},{"name":"Sci-fi","id":21},{"name":"Seinen","id":22},{"name":"Shoujo","id":23},{"name":"Shoujo Ai","id":24},{"name":"Shounen","id":25},{"name":"Shounen Ai","id":26},{"name":"Slice of Life","id":27},{"name":"Sports","id":28},{"name":"Supernatural","id":29},{"name":"Tragedy","id":30},{"name":"Yaoi","id":31},{"name":"Yuri","id":32}],"tags":[]}

创建用户 PUT /create(注意是 PUT 不是 POST)。先测非法短密码——HTTP 状态仍是 200,业务码在信封里:

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X PUT -d '{"user_name":"d5reader","password":"abc","roles":["USER"]}' http://localhost:8138/api/v1/users/create
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 01 Nov 2026 11:48:23 GMT

{"code":400,"message":"Password must be at least 6 characters long!"}

合法创建(message 返回新用户 ID):

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X PUT -d '{"user_name":"d5reader","password":"q1w2e3r4","roles":["USER"],"authorities":["ROLE_USER"]}' http://localhost:8138/api/v1/users/create
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":201,"message":"3"}

更新用户 PATCH /update(别名 /edit):password 传空串保留原密码(源码 StringUtils.isEmpty(password) 分支);下例给 d5reader 追加 DOWNLOAD_FILES 细粒度权限并屏蔽 Adult 流派:

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X PATCH -d '{"id":3,"user_name":"d5reader","password":"","roles":["USER"],"authorities":["ROLE_USER","DOWNLOAD_FILES"],"disallowed_genres":["Adult"]}' http://localhost:8138/api/v1/users/update
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":201,"message":"User successfully created"}

回显两处 API 怪癖(均为 1.1 真实行为):更新成功的 message 竟写作 "User successfully created"、业务码 201——语义与文案不符,脚本判成败应以 HTTP 200 + code 字段为准。复查列表确认更新生效:

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/users/list
[{"id":1,"user_name":"Admin","password":"$2a$10$ne7i/OgxDu6S0HnSUmVlBuZX/XE.l5L93adFeGMOLIPMFEPV.mc26","roles":["ADMIN"],"authorities":["ROLE_ADMIN"]},{"id":2,"user_name":"root","password":"$2a$10$acVoMoHHYm6ft0h6FnHrRenRVgKlTIbtNZinu2BOTMYGVwuDi04.u","roles":["USER"],"authorities":["ROLE_USER"]},{"id":3,"user_name":"d5reader","password":"$2a$10$qNBMLHB9EUhLj06WHTegi.DcmeGgPYM0yQBXRKr0N4Z8hx/W1aq.i","roles":["USER"],"authorities":["ROLE_USER","DOWNLOAD_FILES"],"disallowed_genres":["Adult"]}]

权限边界实测:普通用户 d5reader 访问 ADMIN 专属端点被 403 拦截,但读书库正常:

curl -s -o /dev/null -w '%{http_code}\n' -u d5reader:q1w2e3r4 http://localhost:8138/api/v1/users/list
curl -s -o /dev/null -w '%{http_code}\n' -u d5reader:q1w2e3r4 'http://localhost:8138/api/v1/books?limit=1'
curl -s -o /dev/null -w '%{http_code}\n' -u root:q1w2e3r4 http://localhost:8138/api/v1/settings/get
403
200
403

删除用户 DELETE /delete?user_id=(查询参数不是路径段):

curl -s -i -u Admin:q1w2e3r4 -X DELETE 'http://localhost:8138/api/v1/users/delete?user_id=3'
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"User successfully deleted"}

5.3 settings 域(/api/v1/settings)

类级 ADMIN。GET /get 返回六键当前值(键语义详解见 配置篇第三章):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/settings/get
{"allow_loading_list_with_volumes":true,"allow_loading_list_with_chapters":false,"disable_request_logging_into_console":true,"disable_file_watcher":false,"disable_watch_for_modified_files":false,"disable_chapters":true}

POST /update 必须整包提交六键(@RequestBody ServerSettings,缺键按 false 处理)。下例把控制台请求日志关掉(disable_request_logging_into_console true→false),并立即 cat 宿主侧属性文件验证落盘:

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X POST -d '{"allow_loading_list_with_volumes":true,"allow_loading_list_with_chapters":false,"disable_request_logging_into_console":false,"disable_file_watcher":false,"disable_watch_for_modified_files":false,"disable_chapters":true}' http://localhost:8138/api/v1/settings/update
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"Settings updated successfully"}
cat /opt/data/quzuo/tmp/atsumeru/config/atsumeru.properties
#Auto save properties: Sun Nov 01 11:48:23 UTC 2026
#Sun Nov 01 11:48:23 UTC 2026
allow_loading_list_with_volumes=true
allow_loading_list_with_chapters=false
disable_chapters=true
disable_file_watcher=false
disable_watch_for_modified_files=false
disable_request_logging_into_console=false

改回 true 后再验证一次(回显 Settings updated successfully,属性文件末行恢复 disable_request_logging_into_console=true,时间戳同为 11:48:23,此处不再重复贴出)。两点源码级警告:

  • disable_chapters 值发生变化时,服务端会延迟自动重启(源码 restartServerDelayed() 等待导入/元数据任务空闲后 AtsumeruApplication.restart()),API 调用方应预期短暂不可用;
  • 导入/元数据刷新/封面保存任一进行中时,GET /get 与 POST /update 返回 406 {"message":"Unable to edit server settings"}(源码 serverLockedSupplier,本篇实测时段无导入任务,未触发)。

5.4 importer 域(/api/v1/importer)

类级 hasRole('ADMIN') or hasAuthority('IMPORTER')。

状态与目录注册表:

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/importer/status
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/importer/list
{"import_active":false,"last_start_time":1793531877754,"running_ms":0,"imported":0,"total":0,"percent":0.0}
[{"hash":"63fe8498b2a1b0edec7c2efdc8fd3296","path":"/library/manga","singles":false,"singles_in_root":false,"singles_if_in_root_with_folders":false,"ignore_volume_numbers_detection":false,"series_count":1,"singles_count":0,"archives_count":1,"chapters_count":0}]

hash 即 md5(path)(源码 ImportFolder.createHash()),remove 用它定位。

注册目录 POST /add:请求体是 ImportFolder JSON(最少 path,其余布尔可选)。先用不存在的路径测失败分支——404:

curl -s -o /dev/null -w '%{http_code}\n' -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X POST -d '{"path":"/library/nonexistent"}' http://localhost:8138/api/v1/importer/add
404

宿主先建目录 mkdir -p /opt/data/quzuo/tmp/atsumeru/library/d5tmp(bind 挂载映射为容器 /library/d5tmp),真实注册成功:

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X POST -d '{"path":"/library/d5tmp","singles":true}' http://localhost:8138/api/v1/importer/add
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"Folder added"}
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/importer/list
[{"hash":"63fe8498b2a1b0edec7c2efdc8fd3296","path":"/library/manga","singles":false,"singles_in_root":false,"singles_if_in_root_with_folders":false,"ignore_volume_numbers_detection":false,"series_count":1,"singles_count":0,"archives_count":1,"chapters_count":0},{"hash":"dbcbda0ce560cd26226a7754d5972396","path":"/library/d5tmp","singles":true,"singles_in_root":false,"singles_if_in_root_with_folders":false,"ignore_volume_numbers_detection":false,"series_count":0,"singles_count":0,"archives_count":0,"chapters_count":0}]

新目录 hash dbcbda0ce560cd26226a7754d5972396 与宿主侧 python3 -c "import hashlib;print(hashlib.md5(b'/library/d5tmp').hexdigest())" 计算值一致,印证 md5 规则。

移除目录 DELETE /remove/{hash}(同时把该目录内容从索引摘除);对同一 hash 再删一次得到 404:

curl -s -i -u Admin:q1w2e3r4 -X DELETE http://localhost:8138/api/v1/importer/remove/dbcbda0ce560cd26226a7754d5972396
curl -s -o /dev/null -w '%{http_code}\n' -u Admin:q1w2e3r4 -X DELETE http://localhost:8138/api/v1/importer/remove/dbcbda0ce560cd26226a7754d5972396
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"Folder removed from index"}
404

触发扫描——注意 scan 与 rescan 都是 GET(有副作用的 GET,写脚本时别习惯性用 POST):

curl -s -i -u Admin:q1w2e3r4 http://localhost:8138/api/v1/importer/scan
curl -s -i -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/importer/rescan?fully=false'
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/importer/status
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"Scan started"}
HTTP/1.1 200 
...(响应头同上,省略)...

{"code":200,"message":"Scan started"}
{"import_active":true,"last_start_time":1793533762221,"running_ms":64,"imported":0,"total":1,"percent":0.0}

rescan 不带参数默认 fully=true(回显 "Rescan started"),fully=false 退化为增量扫描(回显 "Scan started");/rescan/{hash} 可只重扫某个目录或系列。add/remove/scan/rescan 均带 @CacheEvict,扫描后 books/filters 缓存自动失效。

5.5 books 域(/api/v1/books)

读取类端点无角色限制(任何已认证用户可读,受用户 allowed_categories/disallowed_genres 数据过滤)。

列表 GET "",11 个查询参数(源码 getBooks 形参):

参数默认说明
type空ContentType(UNKNOWN 等)
category空分类真实 ID
presentationseriesseries/archives/singles/issues 等展示形态
search空标题关键词(非空时内部转走 filtered 查询)
sortcreated_at排序字段(合法值见 6.2)
ascfalse升/降序
page1页码,从 1 起
limit30每页条数
with_volumesfalse内嵌卷列表,受 allow_loading_list_with_volumes 开关约束
with_chaptersfalse内嵌章节,受 allow_loading_list_with_chapters 开关约束
allfalsetrue 时不分页返回全部
curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books?limit=30'
[{"id":"atsumeru-serieb71204461360651e64a359ad591269ac","title":"Volume 1","is_single":false,"folder":"/library/manga/AtsumeruTestSeries","cover":"atsumeru36fae69422e431620cadd1b555d248c9?t\u003d1793531877744","authors":["quzuo"],"genres":["Action"],"content_type":"UNKNOWN","is_mature":false,"is_adult":false,"pagesCount":3,"status":"ONGOING","translation_status":"UNKNOWN","plot_type":"UNKNOWN","censorship":"UNKNOWN","color":"UNKNOWN","rating":0,"created_at":1793531877744,"updated_at":1793531877744}]

搜索按标题匹配——本系列文件夹名是 AtsumeruTestSeries 但标题为 "Volume 1",因此 search=Atsumeru 空、search=Volume 命中:

curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books?search=Atsumeru'
curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books?search=Volume'
[]
[{"id":"atsumeru-serieb71204461360651e64a359ad591269ac","title":"Volume 1","is_single":false,"folder":"/library/manga/AtsumeruTestSeries","cover":"atsumeru36fae69422e431620cadd1b555d248c9?t\u003d1793531877744","authors":["quzuo"],"genres":["Action"],"content_type":"UNKNOWN","is_mature":false,"is_adult":false,"pagesCount":3,"status":"ONGOING","translation_status":"UNKNOWN","plot_type":"UNKNOWN","censorship":"UNKNOWN","color":"UNKNOWN","rating":0,"created_at":1793531877744,"updated_at":1793531877744}]

with_volumes 内嵌卷(allow_loading_list_with_volumes=true 才生效,否则参数被静默忽略):

curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books?with_volumes=true&limit=3'
[{"id":"atsumeru-serieb71204461360651e64a359ad591269ac","title":"Volume 1","is_single":false,"folder":"/library/manga/AtsumeruTestSeries","cover":"atsumeru36fae69422e431620cadd1b555d248c9","authors":["quzuo"],"genres":["Action"],"content_type":"UNKNOWN","is_mature":false,"is_adult":false,"pagesCount":3,"status":"ONGOING","translation_status":"UNKNOWN","plot_type":"UNKNOWN","censorship":"UNKNOWN","color":"UNKNOWN","rating":0,"created_at":1793531877744,"updated_at":1793531877744,"volumes":[{"id":"atsumeru36fae69422e431620cadd1b555d248c9","title":"Volume 01","additional_title":"Volume 1","cover_accent":"#1c1d23","file_name":"AtsumeruTestSeries v001.cbz","file_path":"/library/manga/AtsumeruTestSeries/AtsumeruTestSeries v001.cbz","volume":1.0,"pages_count":3,"is_book":false,"created_at":1793531877744}]}]

详情/卷列表 GET /{hash}、GET /{hash}/volumes(系列与单本档案共用同一 ID 空间):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/atsumeru-serieb71204461360651e64a359ad591269ac/volumes
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/atsumeru36fae69422e431620cadd1b555d248c9
[{"id":"atsumeru36fae69422e431620cadd1b555d248c9","title":"Volume 01","additional_title":"Volume 1","cover_accent":"#1c1d23","file_name":"AtsumeruTestSeries v001.cbz","file_path":"/library/manga/AtsumeruTestSeries/AtsumeruTestSeries v001.cbz","volume":1.0,"pages_count":3,"is_book":false,"created_at":1793531877744}]
{"id":"atsumeru36fae69422e431620cadd1b555d248c9","title":"Volume 1","volume":1.0,"isBook":false,"fileSize":1384,"folder":"/library/manga/AtsumeruTestSeries/AtsumeruTestSeries v001.cbz","cover":"atsumeru36fae69422e431620cadd1b555d248c9?t\u003d1793531877744","authors":["quzuo"],"genres":["Action"],"content_type":"UNKNOWN","is_mature":false,"is_adult":false,"pagesCount":3,"status":"UNKNOWN","translation_status":"UNKNOWN","plot_type":"UNKNOWN","censorship":"UNKNOWN","color":"UNKNOWN","rating":0,"cover_accent":"#1c1d23","created_at":1793531877744,"updated_at":1793531877744}

取页 GET /{archive_hash}/page/{n}(n 从 1 起;?convert=true 可强制转 PNG):

curl -s -D - -u Admin:q1w2e3r4 -o /opt/data/quzuo/tmp/atsumeru/d5_page1.png http://localhost:8138/api/v1/books/atsumeru36fae69422e431620cadd1b555d248c9/page/1
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: image/png
Content-Length: 222
Date: Sun, 01 Nov 2026 11:48:24 GMT
python3 -c "d=open('/opt/data/quzuo/tmp/atsumeru/d5_page1.png','rb').read();print('bytes:',len(d),'magic:',d[:8].hex())"
bytes: 222 magic: 89504e470d0a1a0a

封面与下载的正确路径(详见本章末排障实录):GET /cover/{image_hash}(image_hash 就是列表 cover 字段 ?t= 前的部分)与 GET /download/{archive_hash}——注意都是前缀式路径,不是 /{hash}/cover:

curl -s -D - -u Admin:q1w2e3r4 -o /opt/data/quzuo/tmp/atsumeru/d5_cover.png http://localhost:8138/api/v1/books/cover/atsumeru36fae69422e431620cadd1b555d248c9
curl -s -D - -u Admin:q1w2e3r4 -o /opt/data/quzuo/tmp/atsumeru/d5_dl.cbz http://localhost:8138/api/v1/books/download/atsumeru36fae69422e431620cadd1b555d248c9
HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: image/png
Content-Length: 222
Date: Sun, 01 Nov 2026 11:49:21 GMT

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Disposition: attachment; filename=AtsumeruTestSeries+v001.cbz
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 1384
Date: Sun, 01 Nov 2026 11:49:21 GMT
python3 -c "d=open('/opt/data/quzuo/tmp/atsumeru/d5_cover.png','rb').read();print('bytes:',len(d),'magic:',d[:8].hex())"
python3 -c "d=open('/opt/data/quzuo/tmp/atsumeru/d5_dl.cbz','rb').read();print('bytes:',len(d),'magic:',d[:4].hex())"
bytes: 222 magic: 89504e470d0a1a0a
bytes: 1384 magic: 504b0304

下载体 504b0304(PK ZIP 魔数)与源 CBZ 大小 1384 字节一致。/download 受 DOWNLOAD_FILES 权限约束(源码 FilesHelper.downloadFile 读取 Authentication),管理员默认放行。

过滤元数据 GET /filters(当前库可筛维度与取值,输出较长,节选前 900 字符)与 GET /filtered(GET 单值过滤 / POST 表单多值过滤):

curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books/filters?presentation=series'
curl -s -u Admin:q1w2e3r4 'http://localhost:8138/api/v1/books/filtered?search=Volume'
[{"id":"sort","name":"Sort","has_and_mode":false,"single_mode":true,"values":["CREATED_AT","UPDATED_AT","TITLE","POPULARITY","YEAR","COUNTRY","LANGUAGE","PUBLISHER","SERIE","PARODY","VOLUMES_COUNT","CHAPTERS_COUNT","SCORE","LAST_READ"]},{"id":"type","name":"Content type","has_and_mode":false,"single_mode":true,"values":["UNKNOWN"]},{"id":"status","name":"Status","has_and_mode":false,"single_mode":true,"values":["ONGOING"]},{"id":"translation_status","name":"Translation status","has_and_mode":false,"single_mode":true,"values":["UNKNOWN"]},{"id":"plot_type","name":"Plot type","has_and_mode":false,"single_mode":true,"values":["UNKNOWN"]},{"id":"censorship","name":"Censorship","has_and_mode":false,"single_mode":true,"values":["UNKNOWN"]},{"id":"color","name":"Color","has_and_mode":false,"single_mode":true,"values":["UNKNOWN"]},{"id":"age_rating","name":"Age rating","has_and_mode":false,"sing
...(输出过长,节选至此)...
[{"id":"atsumeru-serieb71204461360651e64a359ad591269ac","title":"Volume 1","is_single":false,"folder":"/library/manga/AtsumeruTestSeries","cover":"atsumeru36fae69422e431620cadd1b555d248c9?t\u003d1793531877744","authors":["quzuo"],"genres":["Action"],"content_type":"UNKNOWN","is_mature":false,"is_adult":false,"pagesCount":3,"status":"ONGOING","translation_status":"UNKNOWN","plot_type":"UNKNOWN","censorship":"UNKNOWN","color":"UNKNOWN","rating":0,"created_at":1793531877744,"updated_at":1793531877744}]

聚合元分类 GET /metacategories(作者/流派等维度计数):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/metacategories
[{"id":"authors","name":"Authors","count":1},{"id":"censorship","name":"Censorship","count":1},{"id":"color","name":"Colors","count":1},{"id":"genres","name":"Genres","count":1}]

另有 DELETE /delete/{book_hash}(删除内容索引,源码取证;本篇测试库为演示数据,未实测删除以免破坏前文回显依赖)。

排障实录:/{hash}/cover、/{hash}/download 返回 200 空体 直觉写法 GET /api/v1/books/{archive_hash}/cover 与 .../download 不报 404,而是 HTTP 200 + Content-Length: 0,极易误判为"封面丢失/文件为空":

curl -s -o /dev/null -w '%{http_code} size=%{size_download}\n' -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/atsumeru36fae69422e431620cadd1b555d248c9/cover

实测回显 200 size=0。真因:这两个端点实际注册为前缀式 /cover/{image_hash}、/download/{archive_hash}(源码 @GetMapping),错误路径落入兜底静态资源处理返回空 200。改正确路径后 222 字节 PNG / 1384 字节 CBZ 正常返回(见上文实测)。

5.6 categories 域(/api/v1/books/categories)

除 GET "" 外全部 ADMIN(@PreAuthorize("hasRole('ADMIN')"))。普通用户越权实测:

curl -s -i -u root:q1w2e3r4 -X PUT 'http://localhost:8138/api/v1/books/categories/create?name=X' | head -1
HTTP/1.1 403 

列表(注意可见性规则:源码 getCategoryList 对带 content_type 的分类要求"至少被 1 个内容引用"才返回):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/categories
[{"id":"atsumeru-category966faeabbc679ba4457e2d75e031d487","name":"Unsorted","content_type":"UNKNOWN","order":0}]

创建 PUT /create?name=(重名返回 409 "Category already exists"):

curl -s -i -u Admin:q1w2e3r4 -X PUT 'http://localhost:8138/api/v1/books/categories/create?name=D5Test'
HTTP/1.1 201 
...(响应头同 5.1,省略)...

{"code":201,"message":"Category created"}
curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/categories
[{"id":"atsumeru-category966faeabbc679ba4457e2d75e031d487","name":"Unsorted","content_type":"UNKNOWN","order":0},{"id":"atsumeru-category91fc5d43a0ea621bdac306045436e310","name":"D5Test","order":1}]

新分类 ID 形如 atsumeru-category<md5>。

把内容挂到分类 POST /set——本域最大的坑,见下方排障实录:请求体必须是 application/x-www-form-urlencoded,参数名=内容 ID,参数值=分类真实 ID(多个用逗号分隔)。

先复现 JSON 写法失败:

curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X POST -d '{"atsumeru-serieb71204461360651e64a359ad591269ac":["atsumeru-category91fc5d43a0ea621bdac306045436e310"]}' http://localhost:8138/api/v1/books/categories/set
HTTP/1.1 500 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 01 Nov 2026 11:49:21 GMT
Connection: close

{"timestamp":"Nov 1, 2026, 11:49:21 AM","status":500,"error":"Internal Server Error","path":"/api/v1/books/categories/set"}

改表单编码成功:

curl -s -i -u Admin:q1w2e3r4 -X POST --data-urlencode 'atsumeru-serieb71204461360651e64a359ad591269ac=atsumeru-category91fc5d43a0ea621bdac306045436e310' http://localhost:8138/api/v1/books/categories/set
HTTP/1.1 200 
...(响应头同 5.1,省略)...

{"code":200,"message":"Categories set for provided content list"}

排障实录:categories/set 用 JSON 请求体必 500 端点形参是 @RequestBody MultiValueMap<String, String>。发 application/json 时,Spring 的 Gson 转换器把对象解析成 LinkedTreeMap,向 MultiValueMap 绑定时值类型不匹配,服务端日志抛 IllegalArgumentException: argument type mismatch,客户端只见裸 500 错误信封(实测回显见上)。解法:用 --data-urlencode '内容ID=分类ID' 表单编码提交(成功回显 "Categories set for provided content list");清空分类挂值则参数值留空('内容ID=',本篇实测同样 200)。

改名 PATCH /edit?id=&name=、排序 POST /order(JSON 数组,元素为含 order 字段的分类对象——此端点反而是 JSON 请求体):

curl -s -i -u Admin:q1w2e3r4 -X PATCH 'http://localhost:8138/api/v1/books/categories/edit?id=atsumeru-category91fc5d43a0ea621bdac306045436e310&name=D5TestRenamed'
curl -s -i -u Admin:q1w2e3r4 -H 'Content-Type: application/json' -X POST -d '[{"id":"atsumeru-category966faeabbc679ba4457e2d75e031d487","name":"Unsorted","content_type":"UNKNOWN","order":1},{"id":"atsumeru-category91fc5d43a0ea621bdac306045436e310","name":"D5TestRenamed","order":0}]' http://localhost:8138/api/v1/books/categories/order
HTTP/1.1 200 
...(响应头同 5.1,省略)...

{"code":200,"message":"Category edited"}
HTTP/1.1 200 
...(响应头同 5.1,省略)...

{"code":200,"message":"Categories ordered"}

排序后列表(Unsorted 因内容已改挂 D5TestRenamed、自身引用数归零而被可见性规则隐藏):

curl -s -u Admin:q1w2e3r4 http://localhost:8138/api/v1/books/categories
[{"id":"atsumeru-category91fc5d43a0ea621bdac306045436e310","name":"D5TestRenamed","order":0}]

解除挂载 → 删除 → 重复删除收尾:

curl -s -i -u Admin:q1w2e3r4 -X POST --data-urlencode 'atsumeru-serieb71204461360651e64a359ad591269ac=' http://localhost:8138/api/v1/books/categories/set
curl -s -i -u Admin:q1w2e3r4 -X DELETE 'http://localhost:8138/api/v1/books/categories/delete?id=atsumeru-category91fc5d43a0ea621bdac306045436e310'
curl -s -i -u Admin:q1w2e3r4 -X DELETE 'http://localhost:8138/api/v1/books/categories/delete?id=atsumeru-category91fc5d43a0ea621bdac306045436e310' | tail -1
HTTP/1.1 200 
...(响应头同 5.1,省略)...

{"code":200,"message":"Categories set for provided content list"}
HTTP/1.1 200 
...(响应头同 5.1,省略)...

{"code":200,"message":"Category deleted"}
{"code":404,"message":"Category not exist"}

5.7 请求审计日志

所有 API 调用自动落 /app/logs/requests.log(INFO:[时间] [客户端IP@用户名] Requested 路径),是 1.1 唯一的调用审计面。本篇取证尾段实测回显:

tail -n 12 /opt/data/quzuo/tmp/atsumeru/logs/requests.log
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/set
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/set
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/edit?id=atsumeru-category91fc5d43a0ea621bdac306045436e310&name=D5TestRenamed
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/order
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories
INFO:[Sun Nov 01 11:49:21 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/set
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/delete?id=atsumeru-category91fc5d43a0ea621bdac306045436e310
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/books/categories/delete?id=atsumeru-category91fc5d43a0ea621bdac306045436e310
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/importer/scan
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/importer/rescan?fully=false
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/users/delete?user_id=3
INFO:[Sun Nov 01 11:49:22 UTC 2026] [10.89.57.1@Admin] Requested /api/v1/users/list

六、分页/过滤/排序通用约定

6.1 分页

page 从 1 起、limit 默认 30;响应是裸 JSON 数组,没有 total 字段——判断翻页结束只能"取到空数组即到底",或 all=true 一次取全量(大库慎用)。

6.2 排序

sort 默认 created_at、asc 默认 false(降序)。合法值以 /books/filters 返回的 sort.values 为准:CREATED_AT、UPDATED_AT、TITLE、POPULARITY、YEAR、COUNTRY、LANGUAGE、PUBLISHER、SERIE、PARODY、VOLUMES_COUNT、CHAPTERS_COUNT、SCORE、LAST_READ(本篇实测 sort=title&asc=true 正常返回)。传枚举外的值会得到 400/500 绑定错误,无容错。

6.3 过滤与缓存

  • 单值过滤走 GET /filtered?status=...&genres=...;多值/AND 模式走 POST /filtered + 表单体(与 categories/set 同款 MultiValueMap 约定,用表单编码别用 JSON);
  • books 与 filters 响应带 Spring Cache(源码 @Cacheable,key 含用户名+全部查询参数);importer/add|remove|scan|rescan 与 categories/set|order 会 @CacheEvict 全量失效——改了文件但看不到变化,先触发一次 scan 再查;
  • 用户级数据过滤:allowed_categories 白名单与 disallowed_genres/disallowed_tags 黑名单在 SQL 层生效,同一端点不同账号返回集不同(见 5.2 权限实测与配置篇 5.2)。

七、常见问题

Q1:所有端点都 401,是不是要先登录换 token? 不是。1.1 是纯 HTTP Basic(无 /auth/login,见部署篇 4.3 排障实录),每个请求直接带 -u 用户名:密码 即可。

Q2:为什么找不到 Swagger? 官方镜像线停在 1.1,无 WebUI/Swagger;OpenAPI 属 2.x jar 发行版(A1 版本线说明)。端点图只能靠源码 + 实测,即本篇方法。

Q3:categories/set 为什么 JSON 提交必 500? 该端点形参是 MultiValueMap,只吃表单编码;JSON 会触发 Gson 类型绑定异常。解法与完整复现见 5.6 排障实录。

Q4:cover/download 返回 200 但文件是 0 字节? 路径写反了。正确为 /api/v1/books/cover/{image_hash}、/api/v1/books/download/{archive_hash}(前缀式),错误路径落兜底返回空 200。见 5.5 排障实录。

Q5:写操作(建用户/挂分类)HTTP 都是 200,怎么判断业务成败? 看响应信封的 code 字段(如 {"code":400,"message":"Password must be at least 6 characters long!"} 时 HTTP 仍是 200)。仅 categories/create 成功时 HTTP 才是 201。脚本建议同时校验 HTTP 与 code。

Q6:settings/update 少传几个键会怎样? 六键之外的缺失键按默认 false 整包覆盖并立即落盘 atsumeru.properties,务必先 GET /get 取全量再改后回传(5.3 实测流程)。

八、参考链接

Atsumeru 1.1 API 使用详解 - Quzuo