接口文档
更新于 2026年10月31日
目录99 节
用接口令牌,你可以在自己的程序里做网站上能做的大部分事:浏览和搜索作品、整理书架、发评论、看通知、签到、投稿……本页是全部接口的说明:第 1、2 节是准备工作和一个 Python 客户端,第 3 节起按令牌的用途分节。投稿的完整流程(传文件、排结构、提交审核、更新作品)另见「投稿接口」。
1. 准备
1.1 建一个接口令牌
在网站上登录,打开「接口令牌」页(个人中心里),点「新建令牌」:
起个名字(比如「同步书架」),勾选要用的用途(见下表,可以多选),选到期时间(最长多久页面上写着,默认 90 天;到期以后再建一个)。
输入当前密码确认。还没设过密码的(用论坛账号快捷注册的)先在账号设置里加一个密码;邮箱要先验证过。
页面上显示的
acgp_…就是令牌,只显示这一次,马上存好(放进环境变量或密码管理器,不要写进代码、不要发给别人)。同一个页面上写着接口地址(到/api/v1为止),下文写作$ACG_API。
令牌绑着你的账号,程序带着它调用接口就是以你的身份操作。它只能调用你勾选的用途里的接口:
| 用途 | 能做什么 |
|---|---|
| 浏览 | 以你的身份读站内内容:作品、搜索、标签、排行、评论、片单、阅读和播放(第 3 节)。不选也能读公开内容,但按未登录的访客处理 |
| 投稿 | 投稿、上传、更新自己的作品(自己创建的、共创的)、作品共创的申请和邀请、小说的写作和导入(第 4 节和「投稿接口」页);账号能投稿时才能选 |
| 书架 | 追番追更、关注、收藏夹、历史、看过的集、阅读进度、书签、关注动态(第 5 节) |
| 互动 | 评论、点赞、打分、补标签和投票、弹幕、纠错、举报(第 6 节) |
| 通知 | 查看通知、标为已读、删除(第 7 节) |
| 积分 | 余额和变动、签到、解锁、下载、打赏、收入(第 8 节);会花费你的积分 |
| 求档 | 发布求档、追加悬赏、应征、采纳(第 9 节);会花费你的积分 |
| 分发节点 | 查看你的节点、申请新节点(第 10 节) |
| 资料和设置 | 个人设置、显示名、头像、年龄确认(第 11 节) |
查看当前账号(
GET /me)任何用途都能调用。用途不对时:要登录的接口返回
10003(data.reason是token_scope);公开的接口不报错,按未登录的访客处理。只勾选程序用得到的用途。令牌泄露时,别人能做的就是这些用途里的事。
改密码、改邮箱、管理令牌只能在网站上登录后操作,令牌做不了(第 12 节)。
「接口令牌」页上看得到每个令牌最后一次使用的时间和 IP;不认识的 IP、不再使用、可能泄露的,点「吊销」立即失效。改密码、找回密码时全部令牌自动作废。每建一个令牌都会往你的邮箱发一封提醒:不是你建的,马上吊销并改密码。
同时能用的令牌有上限(默认 5 个),不用的吊销掉。
试一下:
export ACG_API=https://api.neosek.ai/api/v1 # 「接口令牌」页上写的接口地址
export ACG_TOKEN=acgp_…
curl -s -H "Authorization: Bearer $ACG_TOKEN" "$ACG_API/me"返回 "code": 0 和你的账号信息(第 11.1 节)就通了。
1.2 请求和响应
地址是
$ACG_API加上本页写的路径,如$ACG_API/works。每个请求都带请求头Authorization: Bearer <令牌>。请求体是 JSON(
Content-Type: application/json),字段名用snake_case。账号的字段和路径叫
account(/accounts/…、account、account_id)。以前的user写法(/users/…、user、user_id)已停用:旧路径返回10004,旧的字段名和查询参数不再识别。时间是 RFC 3339,带时区(
2026-10-20T23:30:00+08:00);只有日期的参数写YYYY-MM-DD,按站点时区。id 是数字,图片、视频这类文件的uid是字符串。查询参数要多个值时用逗号分开(
?content_type=manga,novel);不认识的参数会被忽略。语言:名称、说明这类给人看的文字按
?lang=> 请求头Accept-Language> 你账号的语言设置 >zh-CN的顺序选,支持zh-CN、en-US。接口里的name就是选出的那种语言的名称。响应一律是 HTTP
200(维护中、上传文件的断点续传等少数情况除外),成败看code:
{ "code": 0, "message": "success", "data": { } }message 是给开发者看的英文。各节写了各自接口特有的错误码;通用的有:
code | 意思 | 怎么办 |
|---|---|---|
10000 | 服务器出错 | 稍后重试;一直出错就联系站务,带上响应头里的 X-Request-Id |
10001 | 参数不对 | data.errors 按 JSON 路径给出哪个字段、什么原因,如 {"form.category_id": "unknown"} |
10002 | 令牌不对、过期了或被吊销了 | 在「接口令牌」页看看状态,需要的话再建一个 |
10003 | 没有权限 | data.reason 是 token_scope:令牌没有这个用途;session_only:只能在网站上登录后操作;没有 reason:你的账号不能做这件事 |
10004 | 不存在 | 也可能是你看不到(别人的草稿、未公开的内容),或者路径写错了 |
10005 | 冲突 | 重复了、状态不对,或者 version 对不上;data 是最新的对象(第 1.4 节) |
10006 | 太频繁 | 等响应头 Retry-After(也在 data.retry_after)那么多秒再试 |
10009 | 维护中(HTTP 503) | 等 Retry-After 秒再试 |
找站务问问题时带上响应头里的 X-Request-Id,查得到那一次请求。
1.3 列表和分页
列表接口大多按游标分页:查询参数 limit(每页几条,默认 20、最大 100)和 cursor(第一页不带),响应是:
{ "items": [ ], "next_cursor": "…" }下一页把 next_cursor 原样放进 cursor 再请求;next_cursor 为 null 就是到头了。空列表是 []。少数接口用页码(page 从 1 开始、page_size,响应带 pagination),个别接口不分页、一次给全,各节会写明。
1.4 修改、重试和频率
写操作成功时返回改好的完整对象。
几处可能同时修改的对象(投稿、收藏夹这类)带
version:修改时带上你拿到的version,对不上返回10005,data是最新的对象,换上它的version再来。写明支持
Idempotency-Key的接口:请求头带一个随机串(不超过 64 个字符),网络断了重发时,24 小时内同一个键返回第一次的结果,不会重复执行(响应头带Idempotent-Replayed: true)。接口按账号限制频率(发评论、签到这类各有各的上限),超过时返回
10006。批量处理时一条一条来,10006、10009等一会儿重试就好;别的错误要改了请求再来。
1.5 图片、视频、音频的地址
接口里每个文件都是同一个形状:
{ "url": "https://…", "fallbacks": ["https://…"], "expires_at": null }主地址打不开(连不上、5xx)时按顺序换
fallbacks里的;都打不开,或者快到expires_at了,用POST /media/urls重新要地址(第 3.7 节)。expires_at为null是不过期。图片另有
width、height、placeholder(主色和模糊预览)和各种尺寸的地址。地址都由接口给出,不要自己拼。站点设成要登录才能看的分级(
GET /meta的ratings[].requires_login),未登录时不给地址:带restricted: "login_required",图片只有尺寸和placeholder。令牌带「浏览」用途就按你本人算。
2. Python 客户端
Python 3.8 以上,只依赖 requests(pip install requests)。存成 acg_client.py,本页和「投稿接口」页的例子都用它。别的语言照着它发同样的 HTTP 请求就行。
"""站点接口的最小客户端:Python 3.8+,只依赖 requests(pip install requests)。
环境变量:
ACG_API 接口地址(「接口令牌」页上写着,到 /api/v1 为止)
ACG_TOKEN 在网站「接口令牌」页建的令牌(acgp_ 开头)
"""
import os
import time
import uuid
import requests
API = os.environ["ACG_API"].rstrip("/")
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["ACG_TOKEN"]
RETRYABLE = {10006, 10009, 72012} # 太频繁、维护中、文件服务暂时不可用:等一会儿再试
class APIError(Exception):
def __init__(self, code, message, data=None):
super().__init__(f"{code} {message} {data if data is not None else ''}".strip())
self.code, self.message, self.data = code, message, data
def unwrap(resp):
"""拆开 { code, message, data }:code 不是 0 就抛 APIError。"""
env = resp.json()
if env.get("code") != 0:
raise APIError(env.get("code"), env.get("message"), env.get("data"))
return env["data"]
def call(method, path, body=None, params=None, files=None, idempotent=False):
"""调接口,返回 data。params 是查询参数;带文件时 body 当表单字段发(multipart)。"""
headers = {"Idempotency-Key": uuid.uuid4().hex} if idempotent else {}
for attempt in range(6):
kw = {"data": body, "files": files} if files else {"json": body}
resp = session.request(method, API + path, params=params, headers=headers, timeout=120, **kw)
try:
return unwrap(resp)
except APIError as e:
if e.code not in RETRYABLE or attempt == 5:
raise
time.sleep(float(resp.headers.get("Retry-After") or 10))
def pages(path, params=None):
"""按游标翻页的列表接口:逐条产出 items,翻到 next_cursor 为 null 为止。"""
params = dict(params or {})
while True:
data = call("GET", path, params=params)
yield from data["items"]
if not data.get("next_cursor"):
return
params["cursor"] = data["next_cursor"]call(..., idempotent=True) 会带上一个随机的 Idempotency-Key,自动重试时沿用同一个键(解锁、打赏这类必须带它的接口用)。用法:
from acg_client import APIError, call, pages
me = call("GET", "/me")
print(me["name"], me["permissions"])
for work in pages("/works", {"content_type": "manga", "limit": 100}):
print(work["id"], work["title"])
try:
call("GET", "/me/points")
except APIError as e:
print(e.code, e.data) # 令牌没有「积分」用途时:10003 {'reason': 'token_scope'}3. 浏览(用途「浏览」)
公开的内容不登录也能读。令牌带「浏览」用途时按你本人算:看得到要登录才能看的标签和作品、你解锁过的内容,作品详情里的 viewer(关注、阅读进度、打分、收藏)是你的;没有这个用途时按未登录的访客算,不报错。
3.1 站点、分类和页面
GET /site 站点配置
| 字段 | 说明 |
|---|---|
name、url | 站名、站点地址 |
features | 功能开关,如 upload、download、comment、search、danmaku:网站按它显示或隐藏入口,有的功能关掉以后对应的接口也会拒绝(见各节);不认识的键忽略 |
limits | 限额,单位写在键名里,如下文用到的 search_query_max_length、search_max_terms、suggest_min_length、ranking_max,另有上传大小、评论字数等 |
pages | 站点页面的列表:[{ "slug", "title" }] |
announcements | 正在生效的公告:id、level、title、body(Markdown 原文)、url、starts_at、ends_at 等 |
maintenance | 维护模式:{ "enabled", "message" } |
另有 menus、footer、seo 等给网页显示用的字段;菜单和公告按你的身份过滤。
站点维护时(
maintenance.enabled为true),除了GET /site、GET /meta、GET /pages/{slug},别的接口都返回10009。GET /site、GET /meta带ETag:请求头If-None-Match对得上时回 HTTP 304(没有响应体)。
GET /meta 元数据
各种选项的代码和名字。选项由站务设置、会增减,程序里不要写死。带 ?names=all 时每项另给 names(全部语言的名字)。
| 键 | 内容 |
|---|---|
content_types | 内容类型:code(如 manga、novel、anime、asmr)、name、enabled、unit_name 和 item_name(单元、条目的叫法,如「话」「页」)、default_sort(站内这种类型的列表页默认用的排序,如动画是 hot)、like_on(在哪一级点赞:unit 每一话、每一集、每一组,item 小说的每一章,见第 6.3 节) |
categories | 分类:id、content_type、code、name、sort |
ratings | 分级:code、name、order 和每档的规则(requires_login 要登录才能读、noindex 不给搜索引擎收录等)。数组的顺序就是展示的顺序(限制从少到多);order 是这一档在数组里的位置,0 是第一档(全年龄),作品上标分级只标 order 大于 0 的;hidden_by_default 为 true 的一档默认隐藏(见下面的 default_hidden_tags) |
languages、translation_statuses | 作品语言、翻译状态 |
tag_namespaces | 标签的命名空间:code、name、order、aliases(搜索语法里的简写)、creator_role(不为 null 时这一类挂的是创作者,值是角色代码) |
default_hidden_tags | 站点默认屏蔽的标签:id、namespace、name。带这些标签(连同下位)的作品、默认隐藏的分级的作品,没带令牌(未登录)时一律不在列表、搜索、推荐里,filter=off 也一样;带令牌时默认不列,个人设置里解除了的照列(content_filter.shown_tags、shown_ratings),filter=off 时照列。直接按 id 取作品不受影响 |
creator_roles | 创作者的角色 |
work_code_kinds | 作品号的种类(作品的 codes[].kind) |
work_sorts | 排序项:code、name、contexts(能用在哪:list 作品列表、search 搜索、series 系列)、content_types(有这个键时只对这些类型有意义) |
ranking_metrics、ranking_periods | 排行的指标(periods 是它能用的时段,第一个是默认)、时段的名字 |
ui_languages | 站点支持的界面语言 |
另有下载、打赏、求档、通知、举报等用到的选项。筛选参数用各项的 code,只有 category 用分类的 id;按分类找 id 时用内容类型和分类代码(名字随语言变):
meta = call("GET", "/meta")
cats = {(c["content_type"], c["code"]): c["id"] for c in meta["categories"]}
doujinshi = cats[("manga", "doujinshi")] # 漫画的「同人志」分类
for ns in meta["tag_namespaces"]:
print(ns["code"], ns["aliases"], ns["name"], ns["creator_role"])GET /pages/{slug} 站点页面
→ { "slug": "terms", "title": "使用条款", "body": "(Markdown 原文)", "updated_at": "…" }。有哪些页面看 GET /site 的 pages;不存在 10004。
站点地图
给搜索引擎的站点地图用的 id 列表,总是按访客算。
| 接口 | 说明 |
|---|---|
GET /seo/sitemaps | 目录:{ "sets": [{ "kind", "shards": [{ "shard", "count", "updated_at" }] }] }。kind 是 works、tags、creators、series、pages;shard 从 0 起,一片最多 10000 项(以站点设置为准) |
GET /seo/sitemaps/{kind}/{shard} | 一片:{ "items": [{ "id", "updated_at" }] },按 id 排;works 每项另有 content_type;pages 每项是 slug 而不是 id,按 slug 排。种类不认识、分片号超出范围 10004 |
只收能给搜索引擎收录的:访客看得到也能读、分级不是 noindex 的已发布作品,至少有一部这样的作品的标签、创作者、系列,以及全部站点页面。
3.2 作品列表、筛选面板、新番时间表、随机和相关
GET /works 作品列表
列已发布的作品,游标分页 → { "items": [作品列表项], "next_cursor" }。参数都可以不带:
| 参数 | 说明 |
|---|---|
content_type | 内容类型代码,可以多个 |
category | 分类 id(一个) |
rating、language、translation_status | 分级、作品语言、翻译状态的代码,都可以多个 |
serial_status | ongoing 连载中 / completed 已完结 |
tag | 标签 id,多个时要都有;连同下位标签一起算 |
exclude_tag | 排除的标签 id,可以多个 |
creator、creator_role | 创作者 id;再带角色代码时只算这个角色 |
series | 系列 id |
uploader | 投稿人(作品的创建者)的用户 id |
co_creator | 共创者的用户 id:这个人当共创者的作品(自己创建的不算,那用 uploader) |
items_min、items_max | 条目数(漫画是页数)的范围 |
published_from、published_to | 上站日期的范围(YYYY-MM-DD,含两端,按站点时区) |
released_from、released_to | 发行日期(作品的 released_on)的范围,含两端;带了的话没填发行日期的作品不在结果里。按年份筛就是这一年的头尾:released_from=2024-01-01&released_to=2024-12-31 |
sort | 排序:/meta 的 work_sorts 里 contexts 有 list 的,如 latest 最新(默认)、updated 最近更新、hot 热度(发行时期新的在前,同一时期里近期热度高的在前;没填发行日期的在最后)、released 发行日期、views 人气(总浏览多的在前)、words 字数、score 评分、favorites 收藏数、likes 点赞数(各集、各章的点赞加起来);带了 series 时还能用 series(系列里的顺序)。近期热度算的是一段时间里的浏览、新收藏、评论、新评分和新点赞 |
filter | default(默认):套用你在个人设置里的内容过滤(content_filter:隐藏的分级、语言、标签,屏蔽的投稿人;站点默认隐藏的分级、默认屏蔽的标签里没解除的);off:这次不套用(站点默认的那些对未登录的请求照样不列) |
cursor、limit | 游标分页 |
代码不存在、范围反了、日期格式不对都是
10001,如released_to: invalid。游标里带着排序和筛选,换了参数要从第一页重新取。
作品列表项(列表、搜索、排行、首页里的作品都是这个形状):
| 字段 | 说明 |
|---|---|
id、category | 作品 id;分类 { "id", "code", "name" } |
content_type、rating、language、translation_status | 内容类型、分级、作品语言、翻译状态的代码 |
title、title_lang | 按你的标题偏好(个人设置的 title_preference)挑出的标题和它的语言(按访客算时按请求的语言) |
cover | 封面图片;没有为 null |
unit_count、item_count | 已公开的单元数、条目数(漫画是话数、页数) |
creators | 创作者 { "id", "name", "roles" },最多 5 人(详情里是全部) |
tag_highlights | 列表上显示的几个标签 { "id", "namespace", "name" } |
stats | views 浏览数、favorites 收藏人数、score 平均分(1 到 10,评分人数不够时为 null)和 score_count 评分人数、comments 评论数、likes 点赞数(各集、小说各章的点赞加起来)、plays 和 completions(动画、音声全部集数加起来的播放、看完听完次数,含 PV、花絮,换掉、删掉的文件也算;别的类型为 null) |
uploader | 投稿人(作品的创建者){ "id", "name", "avatar", "deleted" };站务导入的为 null |
serial_status | ongoing / completed;不适用为 null |
word_count、duration | 小说的字数;动画、音声的总时长(秒,文件处理好以后最多晚 10 秒算上);别的类型为 null |
latest | 最新公开的一话或一章:unit_id、item_id、title、published_at;还没有为 null |
published_at、updated_at | 发布时间;内容最后更新的时间 |
preview | true 是还没公开(只有创建者、共创者和站务看得到) |
for w in pages("/works", {"content_type": "manga", "tag": "45,46", "sort": "updated"}):
print(w["id"], w["title"], w["stats"]["views"])GET /works/facets 筛选面板
按类型标签、年份筛作品时用:给这两排的选项和各有几部 → { "tags", "years" }。筛选参数同 GET /works(sort、cursor、limit 不用)。
| 字段 | 说明 |
|---|---|
tags | 类型标签 { "id", "name", "count" }:站务给这种内容类型固定了标签时按固定的顺序(count 可能是 0),否则是用得多的在前;按它筛用 GET /works?tag=<id> |
years | 有作品的发行年份 { "year", "count" },新的在前;按它筛用这一年的 released_from、released_to |
每一排按除了它自己以外的条件数:类型标签不看
tag,年份不看发行日期的范围。选了一项以后另一排的数跟着变,自己这一排照旧列全。套用你的内容过滤(
filter=off这次不套用)。
f = call("GET", "/works/facets", params={"content_type": "anime", "released_from": "2024-01-01", "released_to": "2024-12-31"})
print([(t["name"], t["count"]) for t in f["tags"]])GET /schedule 新番时间表
一周七天,列连载中、填了放送时间的动画,每部作品一周一项;套用你的内容过滤。
week:ISO 周YYYY-Www(如2026-W41),不带是本周。前后能翻的周数有限(默认各 8 周,以站点设置为准),超出或格式不对10001。rating:只列这几档分级的作品(分级代码,多个用逗号分开,同GET /works);不认识的代码10001。
{ "week": "2026-W41", "prev_week": "2026-W40", "next_week": "2026-W42", "timezone": "Asia/Shanghai",
"days": [ { "date": "2026-10-10", "weekday": 6, "items": [
{ "work": { "…": "作品列表项" }, "at": "2026-10-10T22:30:00.000+08:00",
"unit": { "id": 305, "title": "第2集", "published_at": null }, "state": "scheduled" } ] } ] }timezone是站点时区,一周按它算;days星期一在前,weekday1 到 7 是星期一到星期日;prev_week、next_week到了能翻的边界为null。at是放送时间推出来的这一档;unit是这一档对应的那一集,还没有为null;state:aired已上线 /scheduled定时了、还没到点 /expected还没传。
GET /works/random 随机作品
在符合条件的已发布作品里随机挑一部(在作品编号的范围里随机取一个起点、取它后面第一部符合的,不完全等概率)→ { "id": 123, "content_type": "manga" };一部都没有时两项都是 null。筛选参数同 GET /works(sort、cursor、limit 不用)。
GET /works/{id}/related 相关作品
和这部作品相似的已发布作品,最像的在前 → { "items": [作品列表项] },不分页。
limit:默认 12、最多 50(以站点设置为准),超出范围10001。rating:只要这几档分级的作品(同GET /works),从这几档里挑够条数;不认识的代码10001。手填的关联作品(详情的
relations)不在里面;套用你的内容过滤。结果不是实时的。同一部作品的不同版本(不同汉化、不同语言、重复上传)只出一个,优先和这部作品同语言的,机翻的排在后面;这部作品自己的别的版本不在里面。
作品不存在、看不到
10004。
3.3 搜索和联想
搜索语法
搜索文字按空白(含全角空格)切成几个条件,条件之间是「并且」:
| 写法 | 含义 |
|---|---|
夏日 | 关键词:匹配标题、别名、简介、标签名、创作者名、作品号 |
"夏日 约定" | 短语:引号里的空格不切开(全角引号也认) |
female:眼镜 | 某个命名空间的标签:命名空间写代码或简写(/meta 的 tag_namespaces),不分大小写,全角冒号也认 |
artist:某某 | 挂创作者的命名空间(creator_role 不为 null):这个人以这个角色参与的作品;名字有空格时加引号,如 artist:"某 某" |
-female:眼镜 | 排除 |
~female:眼镜 ~female:双马尾 | 「或」:带 ~ 的条件合成一组,满足其中一个就行 |
female:眼镜$ | 只算这个标签本身,不带下位标签(默认带) |
RJ01234567 | 作品号:直接命中那部作品 |
标签、创作者写任何一种语言的名字或别名都行。
GET /search 搜索
| 参数 | 说明 |
|---|---|
q | 搜索文字,最长 limits.search_query_max_length(默认 200)个字符,太长 10001;空串表示只按筛选列作品 |
sort | /meta 的 work_sorts 里 contexts 有 search 的;有关键词时默认 relevance(相关度),否则默认 latest |
| 筛选 | content_type、category、rating、language、translation_status、serial_status、items_min、items_max、published_from、published_to、released_from、released_to、filter,同 GET /works |
cursor、limit | 游标分页 |
返回 items、next_cursor,另有:
query:q和解析出来的条件terms[]:raw(原样写法)、kind(keyword/tag/creator/code)、认出来的tag或creator(没认出为null),另有negated、or_group、exact、phrase、namespace、text、matched_alias。query.warnings[]:没按预期理解的条件{ "raw", "code" }:unknown_namespace(没有这个命名空间,整条当关键词)、unknown_tag、unknown_creator(正向的条件什么也匹配不到,排除的不起作用)、empty_term(如单独一个-,忽略)。total:结果数;total_exact为false时是估计值。degraded:true时全文搜索暂时不可用,这次关键词只匹配了标题、别名、作品号,relevance按latest排。
条件超过 limits.search_max_terms(默认 20)个返回 77001。
r = call("GET", "/search", params={"q": "female:眼镜 -male:眼镜", "content_type": "manga"})
print([(t["raw"], t["kind"]) for t in r["query"]["terms"]], r["query"]["warnings"])
print(r["total"], [w["title"] for w in r["items"]])要全部结果时用 pages("/search", {...}) 逐条取。
GET /search/suggest 联想
按正在输入的一个条件给标签、创作者、作品的候选。
| 参数 | 说明 |
|---|---|
q | 必填。正在输入的那一个条件,至少 limits.suggest_min_length(默认 1)个字符;写成 female:眼 时只在这个命名空间里找 |
scope | all(默认)/ tags / creators / works,可以多个。作品的候选由站内搜索给;搜索暂时不可用时按标题、别名、作品号的开头找,q 至少 2 个字符 |
namespace | 只找这个命名空间的标签 |
role | 只找有这个角色的创作者 |
limit | 默认 10、最大 20(不分页) |
{ "items": [
{ "kind": "tag", "tag": { "id": 45, "namespace": "female", "name": "眼镜" }, "matched": "眼鏡", "work_count": 1234, "query": "female:眼镜" },
{ "kind": "creator", "creator": { "id": 3, "name": "某某", "roles": ["author"] }, "matched": null, "work_count": 12, "query": "artist:某某" }
] }matched:用别名或别的语言的名字匹配上时是匹配上的写法,直接按名字匹配上为null。work_count:标签连同下位标签的已发布作品数;创作者的已发布作品数。kind为work时给work(简短的作品),没有work_count,query为null。query:选中后放进搜索框的写法。遇到不认识的kind跳过。
GET /search/trending 热搜
一段时间里(默认 7 天)搜过同一个词的人数,多的在前;同一个人同一个词只算一次,人数太少的不列。所有人看到的一样,几分钟更新一次。
| 参数 | 说明 |
|---|---|
limit | 给几条:默认、最多都是站点设置的条数(默认 10) |
{ "items": [
{ "q": "female:眼镜", "heat": 128, "mark": "hot", "pinned": false },
{ "q": "侦探", "heat": 42, "mark": "new", "pinned": false }
] }q:搜索词,原样;拿它当GET /search的q就是这个词的结果。heat:这段时间里搜过的人数;站务手动加的词可能为null。mark:hot(前几名)、new(刚有人搜的)或null;以后可能加,不认识的当null。pinned:站务置顶、手动加的,排在最前面。热搜关着或者还没有够人数的词时
items是空的。用接口令牌搜的不算进热搜。
for t in call("GET", "/search/trending")["items"]:
print(t["q"], t["heat"], call("GET", "/search", params={"q": t["q"], "limit": 1})["total"])3.4 作品详情、标签投票、价格、收录的片单
GET /works/{id} 作品详情
作品列表项的全部字段,另有:
| 字段 | 说明 |
|---|---|
status | 作品状态;公开的作品是 published,投稿人看自己还没公开的作品时是 draft、pending、rejected |
titles | 全部标题 { "text", "lang", "original" },第一个是主标题,original: true 的是原名 |
aliases、description | 别名;简介(Markdown 原文) |
codes | 作品号 { "kind", "value" } |
links | 外部链接 { "kind", "url", "title" },kind:official / store / source / other |
released_on | 首发日期(YYYY-MM-DD),不知道为 null |
tags | 按命名空间分组:[{ "namespace": { "code", "name" }, "entries": [{ "kind", "id", "name" }] }];kind 为 tag 时 id 是标签 id,为 creator 时是创作者 id。只有显示着的标签 |
creators | 全部创作者 |
co_creators | 共创者 { "id", "name", "avatar", "deleted" },按加入的先后,没有为 [](创建者是 uploader;见第 4 节的「作品共创」) |
series | 所属的系列 { "id", "name", "position" } |
relations | 关联作品 { "kind", "work" }:kind 是 newer_version / older_version / other_language / prequel / sequel / related,work 是简短的作品(id、content_type、title、title_lang、rating、cover) |
size_bytes | 原档总大小(字节),还在算为 null;文件处理好以后最多晚 10 秒算上 |
extra | 动画有 broadcast:放送时间 { "weekday", "time", "timezone" }(不放送为 null);别的类型是 {} |
created_at | 建立时间 |
viewer | 你和这部作品的关系,见下表 |
viewer 的字段 | 说明 |
|---|---|
can_read、read_block_reason | 能不能读、看;按访客算、分级要登录才能读时为 false,原因是 login_required |
progress | 阅读进度(读到哪一单元、哪一条、第几秒);没读过为 null |
following | 关注了没有(追更) |
rating | 你打的分(1 到 10),没打过为 null |
collection_ids | 在你的哪几个收藏夹里 |
unlocked、price | 要花积分的下载现在能不能下;现在解锁要付多少(没有要买的为 null) |
can_edit | 你是投稿人(创建者)、作品是草稿或被退回时为 true |
co_creation | 作品共创(第 4 节的「作品共创」):role 你是 owner 创建者 / co_creator 共创者 / null;requests 关于你、还在等的申请、邀请、转让;apply、takeover 能不能申请加入、申请接管(allowed,不能时 reason,要等的 available_at 是能申请的时间)。按访客算时整个为 null |
另有 can_manage、manage_url,只对站务有意义。
不存在、下架、删除的,你看不到的(别人还没公开的作品)都是
10004。取详情不记浏览。浏览只在有人真的打开作品页时由浏览器报(下面的
POST /works/{id}/views),程序取数据不用报。
GET /works/batch 一次取几部作品
GET /works/batch?ids=12,13,14 → { "items": [作品详情], "missing": [14] }:一次取几部作品的详情,每项同 GET /works/{id},按 ids 的顺序(重复的只给一次);看不到、没有的 id 放进 missing,不报错。不记浏览,适合程序批量刷新作品信息。
ids必填,逗号分开,最多 50 个(GET /site的limits.works_batch_max);没带、不是数字、太多都是10001(ids: required/invalid/too_many)。默认每分钟 60 次(以站点设置为准)。
POST /works/{id}/views 报一次浏览
给自己做的阅读、播放界面用:页面真的显示给人看的时候报一次,→ { "recorded": true }。同一个人同一部作品一天只算一次,算过的、没公开的预览、User-Agent 像爬虫或脚本的都是 false;看不到的作品 10004。带「浏览」用途的令牌按本人算,不带的按访客算。批量取数据、刷新缓存的程序不要调。
GET /works/{id}/tags 作品的标签和投票
这部作品的候选标签和票数 → { "items": [{ "tag", "score", "shown", "locked", "source", "viewer_vote" }], "thresholds": { "show", "remove" } },tag 是 { "id", "namespace", "name" }。
作品上的标签由投票决定显不显示:
score到thresholds.show显示,显示着的掉到thresholds.remove以下撤下;locked是站务锁定的,不能投。显示着的在前,其余按分数高的在前;撤下的也列出(
shown: false)。source:uploader投稿人 /staff站务 /user用户补的;viewer_vote是你投的:1赞成 /-1反对 /0没投。作品看不到
10004。投票、补标签要「互动」用途。
GET /works/{id}/price 作品的价格
要花积分的下载(解锁)现在多少钱,按你本人算:
| 字段 | 说明 |
|---|---|
scope | 按什么解锁:work 整部 / unit 一集(动画);这种类型不卖为 null |
unlocked | 要花积分的下载现在都能下了(解锁过,或者你是投稿人) |
free_reason | 不用花积分的原因:owner 投稿人 / free 免费 / event 限时免费 / member 会员免费 / daily_free 会员今天的免费次数;要花为 null |
list_price、price | 打折以前的价(不卖时为 null);你现在要付的(折扣都算上,unlocked 时为 0) |
discounts | 用上的折扣:kind(event 活动 / member 会员 / bundle 多集一起买)、name、percent(减百分之几)、ends_at |
daily_free | 会员每天的免费次数 { "limit", "left" };不适用为 null |
units、remaining | 动画:每一集的 id、title、list_price、price、unlocked、renew_price;还没解锁的集一起买 { "unit_ids", "list_price", "price" }。别的类型分别是空数组和 null |
renew_price | 解锁过的作品再解锁一次(下载次数重新算)要付多少;不适用为 null |
balance | 你的积分余额;按访客算时为 null |
作品不存在、看不到 10004。
GET /works/{id}/collections 收录这部作品的片单
收录了这部作品的公开收藏夹(片单)。页码分页:page(从 1 起)、page_size(默认 20、最大 100)→ { "items": [片单], "pagination": { "page", "page_size", "total", "total_capped" } }。
sort:popular赞多的在前(默认)/new新建的在前 /updated最近改过的在前。片单的字段:
id、name、description、item_count、covers(最近放进来的几部作品的封面)、account(主人{ "id", "name", "avatar", "deleted" })、like_count、liked(你赞了没有)、created_at、updated_at,另有public、is_default、item_order。作品看不到
10004。
3.5 标签、创作者、系列
| 接口 | 说明 |
|---|---|
GET /tags/{id} | 标签;default_hidden_by 不为 null 时这个标签被站点默认屏蔽(它自己或者上位在 GET /meta 的 default_hidden_tags 里):未登录时、登录了但没在个人设置里解除时,列表里没有它的作品(work_count 照样数全部) |
GET /tags/lookup | 按命名空间和名字找标签:namespace(必填,命名空间的代码)、name(必填,任何一种语言的名字或别名)→ 同 GET /tags/{id};找不到 10004 |
GET /creators/{id} | 创作者 |
GET /series/{id} | 系列 |
标签:
id、namespace({ "code", "name" })、name、aliases、description(Markdown 原文)、parent和children(上位、下位标签,下位最多给 50 个)、work_count(带这个标签或它任何一个下位标签的已发布作品数,不一定是实时的)、query(它在搜索语法里的写法,如female:眼镜)、redirected_from、viewer。创作者:
name、aliases、roles(出现过的角色代码)、description、links({ "kind", "url", "title" })、work_count、redirected_from、viewer。系列:
name、aliases、description、work_count、cover(系列里排第一的作品的封面,没有作品为null)、redirected_from、viewer。viewer.following:你关注了没有。redirected_from:请求的是合并掉的旧 id 时,返回合并后的那个,这里是旧 id(正常为null),存下来的 id 请换成新的。禁用的、还在审核的标签,还在审核的创作者,都是
10004。按名字找创作者用GET /creators?q=(下一小节)或者联想GET /search/suggest(scope=creators)。它们的作品:
GET /works?tag=45、GET /works?creator=3&creator_role=author、GET /works?series=5&sort=series。
t = call("GET", "/tags/lookup", params={"namespace": "female", "name": "眼镜"})
print(t["id"], t["work_count"], t["query"])
for w in pages("/works", {"tag": t["id"], "sort": "score"}):
print(w["id"], w["title"])列出全部标签、创作者、系列
| 接口 | 说明 |
|---|---|
GET /tags | 标签列表:namespace(命名空间的代码,不带是全部)、q(按名字、别名找);每项 { "id", "namespace", "name", "aliases", "work_count", "query" } |
GET /creators | 创作者列表:role(只要担任过这个角色的)、q;每项 { "id", "name", "aliases", "roles", "work_count" } |
GET /series | 系列列表:q;每项 { "id", "name", "aliases", "work_count" } |
三个接口都是页码分页:
page(从 1 起)、page_size(默认 20、最大 100)→{ "items", "pagination": { "page", "page_size", "total", "total_capped" } };sort:works作品多的在前(默认)/new新建的在前。只列正常的(合并掉的、禁用的、还在审核的不列);按访客算时不列要登录才能看的标签。
work_count同详情页。namespace、role不认识,sort不对,都返回10001。
3.6 首页和排行
GET /home 首页
一个请求取完首页的全部模块,已按你的身份和内容过滤处理好 → { "is_default", "modules": [...] }(is_default 为 true 是默认的首页)。
每个模块有 id、kind、title(null 时按种类用默认标题)、display(grid / list / compact)、pc_count、mobile_count(电脑上、手机上各显示几条)、columns(一行建议显示几本,为 null 时自己定),另按 kind 多出:
kind | 多出的字段 |
|---|---|
carousel 幻灯片 | slides:{ "image", "title", "subtitle", "url", "new_tab" } |
featured 精选 | works:编辑选的作品 |
works 作品区 | list_query(content_type、category、tag、sort,就是 GET /works 的参数,没设的为 null)、works |
ranking 排行 | metric、periods:[{ "code", "works" }] |
遇到不认识的 kind 跳过。
带 rating(分级代码,多个用逗号分开,同 GET /works)时只要这几档分级的作品:精选、作品区、排行都先按分级筛再取条数,幻灯片关联的作品不在这几档的那一张不给,筛完没有作品的模块不给。不认识的代码 10001。
GET /rankings 排行
| 参数 | 说明 |
|---|---|
metric | 指标:/meta 的 ranking_metrics[].code(如 views 浏览数、favorites 收藏数、likes 点赞数:时段里新点的赞,总榜是作品的点赞数);不带是第一个 |
period | 时段:这个指标的 periods 里的(如 day 今日、week 近 7 天、month 近 30 天、all 总榜);不带是它的第一个 |
content_type | 只排这种内容类型;不带是全部类型混着排 |
limit | 1 到 limits.ranking_max(默认 100),不带时默认 50(以站点设置为准) |
rating | 只排这几档分级的作品(分级代码,多个用逗号分开,同 GET /works):名次按这几档的作品排,不是从全部作品的排行里筛 |
不认识的、这个指标不能用的返回 10001:metric: unknown、period: unknown、content_type: unknown、rating: unknown、limit: invalid。
返回 { "metric", "period", "content_type", "generated_at", "items": [{ "rank", "value", "backfill", "work" }] },不分页,work 是作品列表项:
排行每小时算一次,
generated_at是这一份的时间;content_type是全部类型时为null。rank按这次返回的顺序连着排;value是这个指标在这个时段里的数(all是累计);数一样时 id 小的在前。backfill为true:今日的不够数,这一项是拿昨天的补上的,value是昨天的数。套用你的内容过滤,过滤掉的不补,所以可能不到
limit个。
3.7 目录、阅读和播放
作品分成几个单元,单元里是条目:
| 内容类型 | 单元 | 条目(kind) |
|---|---|---|
漫画 manga | 话 | 页(image) |
小说 novel | 卷 | 章(text) |
动画 anime | 集 | 视频(video):正片、PV、片头片尾、花絮 |
音声 asmr | 分组(本编、特典……) | 音轨(audio)、插图(image)、剧本这类文本(text) |
条目的用途在 role 里(如 page、chapter、main、pv、track、script),全部取值和名字看 GET /meta 的 content_types[].item_roles。
这一节的接口都不花积分、不用解锁(下载见第 8.3 节);记阅读进度、续播见第 5.6 节。
看不到的一律返回
10004:作品没公开、下架、删除、不存在,单元定时还没到点、暂不公开、被下架,按访客算时还有带「要登录才能看」标签的作品。你自己投的、还没公开的内容也能取,带preview: true。有的分级要登录才能读(
GET /meta里这一档的ratings[].requires_login为true)。按访客算时(没带令牌、令牌无效,或者没有「浏览」用途),这种作品的目录照样能取,取条目、正文、文本、播放列表、弹幕返回10002(换成带「浏览」用途的有效令牌就能取),作品详情的viewer.can_read为false;图片、视频、音频带restricted: "login_required",不给地址,只有尺寸和placeholder。取条目、正文、文本、播放列表、地址按用户和 IP 限频,超过返回
10006;正常阅读碰不到。
目录
GET /works/{id}/units:作品的全部单元,按顺序一次给完,不分页 → { "items": [单元] }。
GET /units/{id}:一个单元和它前后的单元 → { "unit", "work", "prev", "next" }。prev、next 是上一个、下一个单元,没有为 null;work 是简短的作品(id、content_type、title、title_lang、rating、cover)。
| 单元的字段 | 说明 |
|---|---|
id、work_id | 单元 id、所属的作品 |
sort | 在作品里的位置,0 起 |
title | 单元名(如「第1话」);只有一个单元的作品常是空串 |
item_count | 条目数 |
published_at | 公开的时间 |
kind | 动画:main 正片 / special / ova;别的类型为 null |
duration | 动画:正片的时长(秒);音声:这个分组音轨的总时长;不知道、别的类型为 null |
cover | 动画:正片的封面帧(图片);没有为 null |
word_count | 小说:这一卷已公开的字数;别的类型为 null |
danmaku_count | 动画:这一集的弹幕数;别的类型为 null |
plays、completions | 动画、音声:这一集(一组)的播放数、看完听完的次数(挂过这一集的视频、音频加起来,换掉、删掉的文件也算);别的类型为 null |
likes、viewer | 这一集(一话、一组)的点赞数和你点过没有 { "liked" }(第 6.3 节);小说的卷 likes 为 null。没带令牌、令牌没有「浏览」用途时 liked 为 false |
另有 preview、status、publish_at,投稿人看自己还没公开的单元时用。
条目
GET /units/{id}/items?start=&limit=:一个单元的条目,按下标取(不是游标)。start 是从第几个开始(0 起,默认 0;负数返回 10001),limit 默认 40、最大 100 → { "items": [条目], "total", "next_start" }。total 是这个单元的条目总数,next_start 是下一批的 start,取完了为 null。取整个单元:从 0 开始,每次把 next_start 当作下一次的 start,直到它为 null。
条目有 id、unit_id、index(在单元里的下标)、kind、role、title(投稿人起的名字,没起为 null),另有 image、text、video、audio 四个字段:和 kind 同名的那个有值,别的为 null。遇到不认识的 kind 跳过。
页、插图:
image是图片,原图在image.original(可能为null),各规格在image.styles。小说的章:
text是{ "title", "word_count", "published_at" }(章名、字数、公开时间),不带正文。音声的文本:
text是{ "name", "size" }(文件名、字节数)。
视频(video)和音频(audio)的主要字段:
| 字段 | 说明 |
|---|---|
uid | 文件的 id,重新要地址时用(图片也有) |
status | processing 处理中(还没有播放地址)/ ready / failed |
duration | 时长(秒,可以有小数);视频另有 width、height |
container | 视频是 mp4 或 hls,音频是 m4a 或 hls;处理完之前为 null |
mp4、m4a、hls | 播放地址(文件地址的形状),没有的为 null:mp4、m4a 是单个文件,hls 是 HLS 的主播放列表。视频用和 container 同名的那个;音频一般只有 m4a,很长的另有 hls |
original、source | 视频的原画:original 是投稿人上传的原视频(不另压一档),source 是它的容器、类型、编码、尺寸、大小(format、mime、codec、width、height、size);没有为 null。原视频不一定能在浏览器里直接播,地址的有效期也较短(看 expires_at) |
source_kept、stream | 视频、音频:投稿时留没留源文件(false 的下载不到源文件),能不能在线播放原档(POST /downloads 的 kind: stream):ok 能 / no_source 没留源文件 / not_playable 原文件浏览器不能直接播;不知道为 null。不是 ok 的不用去要,要了也是 76012 |
subtitles | 字幕:id、language、label、default、forced、vtt(WebVTT 文件;PGS 图形字幕没有,为 null)、ass(原来是 ASS 的另给一份,没有为 null)、pgs(PGS 图形字幕的 .sup 文件,文字字幕为 null) |
fonts | 视频:字体(视频文件附带的、投稿人另传的),按原样显示 ASS 字幕时用:id、name(文件名)、families(字体里写的家族名,ASS 的样式按它找字体)、size、source(embedded 附带的 / upload 另传的)、file(文件地址);没有为空数组 |
poster、cover | 视频的封面帧、音频文件里内嵌的封面(图片);没有为 null |
renditions、audio_tracks | 视频:各档清晰度(name、width、height、bandwidth)、HLS 里的各条音轨 |
chapters、skips | 视频:章节(start、end、title)、可以跳过的段(kind 是 intro 片头 / outro 片尾 / recap 前情回顾,带 start、end) |
peaks | 音频:波形数据(JSON 文件,peaks 是 1000 个 0 ~ 1 的峰值);没有为 null |
preview、restricted | 同图片,见本节开头 |
小说:目录和正文
GET /works/{id}/toc?script=:小说带章名的完整目录,一次给全 → { "work_id", "word_count", "units", "preview" }。units 里每卷是 { "id", "title", "word_count", "items" },items 里每章是 { "id", "index", "title", "word_count", "published_at", "updated_at", "likes" }(likes 是这一章的点赞数)。
只列已公开的章,按阅读顺序;一章都没公开的卷不列。
index是章在这一卷已公开的章里的位置。响应带
ETag:请求带上If-None-Match并且目录没变时回 HTTP304,没有响应体。
GET /items/{id}/text?script=:一章的正文 → { "item", "unit", "work", "script", "blocks", "author_note", "prev", "next", "preview", "text_version" }。
item是这一章:id、unit_id、index、title、word_count、published_at、updated_at、likes点赞数、viewer.liked你点过没有;unit是{ "id", "title" };work是简短的作品。prev、next:上一章、下一章(跨卷,只算已公开的){ "id", "unit_id", "title" },没有为null。text_version:这份公开正文的版本(一串字符,正文改了就变);发段评时放进anchor.text(第 6.1 节)。blocks、author_note:正文和「作者的话」解析好的块(没有作者的话为空数组)。块的type是p段落 /blank空行 /hr分隔线 /image插图,n是块的序号(0 起)。段落的content是一串片段:text普通文字、ruby注音(text是正文、ruby是读音)、em着重号、note注释(text是注释的文字:在这里画一个记号,点了弹出;不算字数)。插图块有uid、image(图片,可能为null)、caption(说明,没有为null)。不认识的块跳过,不认识的片段当普通文字。样式:段落和片段可以有
style(原书或作者设的,只有设了的键):align(left/center/right,段落)、size(相对字号)、bold、italic(false是取消)、font(serif/sans/mono)、color(#rrggbb)、indent、space_before、space_after(段落,单位 em)、letter_spacing(em)、valign(sup/sub)、underline、strike;插图块可以有style:align、width、height(em,按阅读器的字号;只有一个时另一个按比例),没有的整宽。不认识的键忽略。只有小说的章有正文,别的条目返回
10004;已发布作品里还没公开的章(定时、下架这类)谁都取不到,也是10004。默认每分钟最多 120 章(以站点设置为准)。取正文不记浏览:浏览只在有人真的打开阅读页时由浏览器报(
POST /works/{id}/views),程序取数据不用报。
两个接口的 script:original 原文(默认)/ hans 简体 / hant 繁体,在服务端按词转换,卷名、章名、正文、作者的话、插图说明都跟着转,注音的读音不转;别的值返回 10001。
data = call("GET", "/items/5013/text", params={"script": "hans"})
print(data["item"]["title"])
for b in data["blocks"]:
if b["type"] == "p":
print("".join(s["text"] for s in b["content"] if s["type"] != "note"))
for s in b["content"]:
if s["type"] == "note":
print(" 注:" + s["text"])
elif b["type"] == "blank":
print()
next_id = data["next"]["id"] if data["next"] else None # 下一章小说:书内搜索
GET /works/{id}/text-search?q=&script=&cursor=&limit=:在一部小说里搜正文 → { "script", "items", "total", "total_exact", "next_cursor" }。能读这部小说才能搜(同 GET /items/{id}/text):看不到、不是小说 10004,要登录才能读的分级没登录 10002。
q:要搜的字,去掉首尾空白以后 1 到 50 个字(以/site的limits.novel_text_search_query_max为准);西文不分大小写。没带10001(q: required),太长10001(q: too_long)。script:同上,在转换以后的正文里搜,q也照它转一遍;位置、前后文、章名都是转换以后的样子。items按阅读顺序(卷、章、块),每一处是{ "item": { "id", "unit_id", "title" }, "n", "offset", "length", "before", "match", "after" }:n是块的序号,offset、length是在那一块纯文字里的位置和字数(纯文字是段落各片段的文字接起来:注音只留正文、着重去掉标记、注释不算;按 Unicode 码点数),before、after是前后各最多 20 个字,match是搜到的原文。一处不跨块,不重叠。一次最多数 1000 处(以站点设置为准):
total_exact为false时total是「至少这么多」。翻页:
limit默认 50、最多 200,next_cursor带到cursor取下一页,为null是没有了。换了q、script,或者正文改了,旧的cursor返回10001(cursor: invalid),从第一页重新搜。默认每分钟最多 30 次(翻页也算,以站点设置为准)。
page = call("GET", "/works/12/text-search", params={"q": "星海"})
for h in page["items"]:
print(h["item"]["title"], h["n"], h["before"] + "[" + h["match"] + "]" + h["after"])音声:播放列表和文本
GET /works/{id}/playlist:一部音声的全部分组和条目,一次给完 → { "work_id", "duration", "units" },units 里每个分组是 { "id", "title", "items": [条目] }。duration 是全部音轨的总时长(秒),有还在处理的为 null。条目按投稿人排的顺序,音轨、插图、文本混在一起,只要音轨就取 kind 为 audio 的。没有音频条目的类型返回 72016(动画按集用 GET /units/{id}/items 取)。
GET /items/{id}/content:音声文本条目(剧本、台本)的内容 → { "id", "name", "content" },content 是原样的纯文本。别的条目返回 72016(小说的章用 GET /items/{id}/text)。
重新要地址
POST /media/urls:图片、视频、音频的地址取不到(主地址和 fallbacks 都失败,或者返回 403、410),或者快到 expires_at 时,按 uid 换新的地址。请求体 { "uids": ["…"] },1 ~ 100 个 → { "items", "missing" }。
items每项有uid、kind(image/video/audio)、status(processing/ready/failed,failed的没有地址),和kind同名的字段里是完整的图片、视频或音频。按请求的顺序给,重复的只给一次。只给你看得到的:下架、删除、不存在、看不到的 uid 都在
missing里,不分原因。平时不用调:上面这些接口返回时已经带着地址。
3.8 评论和弹幕
评论
评论跟着作品:看得到作品就看得到它的评论,看不到的返回 10004。发评论、点赞见第 6.1 节。
| 评论的字段 | 说明 |
|---|---|
id、work_id、bounty_id | 评论 id、作品(求档下面的评论为 null)、求档(作品的评论为 null) |
unit_id、item_id | 评论在哪:都为 null 是作品下面;unit_id 是一话、一集;item_id 是小说的一章 |
unit、item | 评论在的那一话(一集)、那一章:{ "id", "title", "n" },title 是名字(没起名为空串),n 是在你看得到的话(章)里排第几(1 起,同目录的顺序);作品下面的、你看不到的那一处为 null |
parent_id | 回复挂在哪条一级评论下(只有两层);一级评论为 null |
reply_to | 回复楼里别的回复时,回的是哪一条:{ "id", "account" };别的为 null |
account | { "id", "name", "avatar", "deleted" }(deleted 为 true 是已注销);留位置的为 null |
is_uploader、is_co_creator、is_staff | 写的人是这部作品的投稿人(创建者)、共创者、站务 |
body | 正文,纯文本,保留换行 |
mentions | 正文里 @ 到的用户 |
spoiler | 有剧透 |
anchor | 锚点,没有为 null:kind 是 page(漫画的一页)、paragraph(小说一章的第 n 块)、time(视频、音频的第 seconds 秒),都带 item_id |
likes、reply_count | 点赞数、回复数 |
replies | 一级评论带前几条回复(默认 3 条,早的在前);回复里是空数组 |
created_at、edited_at | 发的时间、改正文的时间(没改过为 null) |
deleted | true:删了、但下面还有回复的一级评论留的位置(body 是空串、account 为 null) |
viewer | 你的状态:liked、can_edit、can_delete;按访客算时都是 false |
GET /works/{id}/comments?unit_id=&item_id=¶graph=&all=&sort=&cursor=&limit=:一级评论,游标分页 → { "items", "next_cursor", "total" },total 是一级评论的条数。
不带
unit_id、item_id是作品下面的;带了是那一话、一集、一章的。小说的item_id是这一章的「本章说」,不列段评;再带paragraph(块的序号)是这一段的段评,没带item_id、负数返回10001。各内容类型能在哪几处评论看GET /meta的content_types[].comment_scopes(work/unit/item)。不是这部作品的、这种类型不能在那一处评论的返回10001,看不到的集、章返回10004。all=true:这部作品全部的评论(作品、各集、各章混在一起,看unit、item区分;不列段评);只列你看得到的那几处(定时、暂不公开、下架的集,没公开的章上的不列,total也不算);再带unit_id或item_id返回10001。sort:hot热门(默认:赞和回复多的在前,新发的也往前排)/new新的在前 /old早的在前,别的返回10001。换了sort要从第一页取,拿旧的cursor返回10001。
GET /items/{id}/paragraph-comments:小说一章各段的段评数 → { "item_id", "counts": [{ "n", "count" }] },只列有段评的段、按序号;数的是一级评论(删了但下面还有回复的也算,同列表的 total)。打不开这一章的返回同 GET /items/{id}/text(10004、10002)。
GET /bounties/{id}/comments?sort=&cursor=&limit=:求档下面的一级评论,规矩同作品的(没有 unit_id、item_id、all),is_uploader 是求档的发起人;求档不存在返回 10004。
GET /comments/{id}/replies?cursor=&limit=:一条一级评论下的回复,游标分页、早的在前 → { "items", "next_cursor", "total" }。不是一级评论的返回 10004。
GET /comments/{id}:一条评论;是回复时另带 parent(它所在的一级评论,不带那条的 replies)。删了的返回 10004。
弹幕
弹幕只在视频条目(kind 为 video)上,别的条目返回 72016。站点关了弹幕(GET /site 的 features.danmaku 为 false)时返回 10003;分级要登录才能看、按访客算时返回 10002。发弹幕见第 6.5 节。
GET /items/{id}/danmaku?segment=:按时间分段取一个视频的弹幕 → { "segment", "segment_seconds", "segments", "total", "items" }。
segment:第几段(0 起,默认 0),是视频从第segment × segment_seconds秒到第(segment + 1) × segment_seconds秒(不含)的弹幕;负数、不是整数返回10001,超出的给空的items。segment_seconds是一段多少秒(默认 360,以站点设置为准),segments是这个视频一共几段,total是这个视频一共多少条。items按time排。一段最多给 3000 条(默认,以GET /site的danmaku.segment_max为准),超了按时间均匀抽取;你自己发的总在里面。
一条弹幕:id、time(第几秒,一位小数)、mode(scroll 滚动 / top 顶部 / bottom 底部)、color(#RRGGBB)、text(一行纯文本)、sender(发的人的代号,12 个十六进制字符:同一个人全站一样,查不出是谁)、created_at、mine(是不是你发的)。
seg, segs = 0, 1
while seg < segs:
data = call("GET", "/items/7001/danmaku", params={"segment": seg})
segs = data["segments"]
for d in data["items"]:
print(d["time"], d["mode"], d["text"])
seg += 13.9 片单和用户主页
片单和收藏夹
公开的收藏夹就是片单(管理自己的收藏夹、给片单点赞见第 5.4 节)。收藏夹的字段:id、name、description(纯文本)、public、is_default(是不是默认收藏夹)、item_count(一共几部)、covers(最近放进来的最多 4 部作品的封面)、user(主人)、item_order(夹里默认的排法:added 新放进来的在前 / manual 主人手排)、like_count、liked(你赞过没有)、created_at、updated_at。
GET /collections?q=&sort=&page=&page_size=:片单广场,只列公开的、至少有 3 部作品(默认,以站点设置为准)的收藏夹。q 在名字、简介里找;sort:popular 赞多的在前(默认)/ new 新建的在前 / updated 最近改过的在前,别的返回 10001。
这个接口是页码分页:page 从 1 起(默认 1),page_size 默认 20、最大 100(超过按 100,小于 1 按默认,不报错)→ { "items", "pagination": { "page", "page_size", "total", "total_capped" } }。total 最多数到 10000,超过时 total_capped 为 true。pages() 不适用,按下面的例子自己翻页。
GET /collections/{id}:一个收藏夹。私有的只有主人看得到,别人返回 10004。
GET /collections/{id}/items?q=&content_type=&sort=&cursor=&limit=:夹里的作品,游标分页 → { "items", "next_cursor", "total" },total 是符合条件的部数。
q按标题、别名找;content_type只看一种内容类型(不认识的返回10001);sort:added放进来的时间(新的在前)/manual主人手排的顺序 /updated作品最近更新的在前 /title按标题,不带时按夹的item_order。换了参数要从第一页取。每项是
{ "work", "work_status", "note", "added_at" }:work是作品列表项,note是主人写的备注。只列你看得到的作品,也套用你的内容过滤。
page = 1
while True:
data = call("GET", "/collections", params={"sort": "new", "page": page, "page_size": 100})
for c in data["items"]:
print(c["id"], c["name"], c["item_count"], c["like_count"])
if len(data["items"]) < data["pagination"]["page_size"]:
break
page += 1用户主页
GET /accounts/{id} → { "account": { "id", "name", "avatar", "deleted" }, "created_at", "stats": { "works", "co_created_works", "collections", "comments" } }。created_at 是注册时间;stats 是已发布的投稿数(你看得到的)、共创的已发布作品数、公开的收藏夹数、评论数。没有这个人、已注销的返回 10004。
GET /accounts/{id}/comments?cursor=&limit=:他写的评论(一级和回复都算,新的在前),游标分页 → { "items", "next_cursor" },每项是 { "comment", "work" }(评论、作品列表项)。只列你看得到的作品上的,删了的不列。
GET /accounts/{id}/collections:他公开的收藏夹,一次给全 → { "items": [收藏夹] };没有这个人、一个都没公开的是空数组。
他投的作品用 GET /works 的 uploader 参数取,他当共创者的作品用 co_creator 参数取。
3.10 求档
求档是用户挂积分求一部站里还没有的作品(规则和发起、应征见第 9 节)。这两个接口不用「求档」用途;带「浏览」用途时 my_stake 按你算。
| 求档的字段 | 说明 |
|---|---|
id、account | 求档 id、发起人 |
content_type、rating | 求的内容类型、分级(代码同 GET /meta) |
title、description | 标题、说明(原文) |
status | open 开着 / fulfilled 采纳了 / expired 到期退回了 / cancelled 取消了 |
amount、stakers | 悬赏一共多少积分(发起人和加码的加起来)、几个人挂了积分 |
answer_count | 应征了几次(撤回的不算) |
my_stake | 你挂了多少(没挂、按访客算为 0) |
expires_at、created_at、closed_at | 到期、发起、结束的时间(open 的 closed_at 为 null) |
accepted | 被采纳的那次应征;没有为 null |
GET /bounties?status=&content_type=&rating=&q=&sort=&page=&page_size=:求档列表,页码分页(同 GET /collections)。
status默认open,可以逗号分隔几个,all是全部;content_type、rating可以逗号分隔几个;q在标题、说明里找。sort:new新发的在前(默认)/amount悬赏多的在前 /expiring快到期的在前。status、sort不认识返回10001。
GET /bounties/{id}:一个求档,另加 stakes 和 answers。没有这个求档返回 10004;取消了的照样能看。
stakes:挂了积分的人[ { "account", "amount", "created_at" } ],一个人加了几次合成一条,多的在前。answers:应征,新的在前,撤回的不列。每项有id、account(应征的人)、work(作品列表项,看不到了为null)、status(pending待处理 /accepted采纳了 /rejected拒了)、given(采纳时拿到的积分)、reject_reason、auto_accept_at、created_at、decided_at,含义见第 9.2 节。
data = call("GET", "/bounties", params={"status": "open", "sort": "amount", "page_size": 50})
for b in data["items"]:
print(b["id"], b["amount"], b["content_type"], b["title"], b["expires_at"])4. 投稿(用途「投稿」)
投稿的完整流程——建草稿、传文件、排结构、填信息、提交审核,发布以后加新的一集、改作品信息、改已经上线的集,小说的写作、导入和按章发布——写在「投稿接口」页,那里有上传文件用的客户端代码和每一步的例子。这里列出全部投稿接口备查。只有账号能投稿时,令牌才能选「投稿」用途;免不免审跟着账号走。
投稿:从草稿到提交
| 接口 | 说明 |
|---|---|
POST /submissions | 建投稿(草稿)。发新作品带 content_type;更新已发布的作品带 kind 和 work_id(new_units 加新的一集、work_edit 改作品信息和封面、unit_edit 改已上线的集:带 unit_id 或 unit_ids,一部作品同时只有一份能改的,再改别的集会加进这一份)。作品的创建者、共创者都能建;改作品信息一部作品同时只有一份没完成的,别人(创建者或别的共创者)正在改时是 72025(data.user 是谁在改)。支持 Idempotency-Key |
GET /submissions | 我的投稿(游标分页),可按 status、content_type、category(分类 id;更新作品的投稿按那部作品的分类,还没选分类的草稿不出现)、work_id、kind 筛选 |
GET /submissions/{id} | 投稿详情:状态、填的信息、还差什么(problems)、审核意见 |
PATCH /submissions/{id} | 填信息:form 里只改带了的字段;publish_at 定时上线 |
DELETE /submissions/{id} | 删掉草稿或被退回的投稿 |
GET /submissions/{id}/similar | 查重:先给完全重复的(标题、创作者、语言、翻译状态、页数都一样;常见标题、没有创作者的还要文件逐页一样)、有更全的版本盖住这一份的(标题里的话数范围、合集字样、文件包含)、这一份盖住的,再给同一种类型里标题一样或者一边以另一边开头、作品号一样、链接一样的作品和投稿(别人的草稿也查,给最近编辑时间)。每项的 relation 是 exact / outdated_by / covers / similar,exact、outdated 是比对;只是提示,不拦提交 |
POST /submissions/duplicates | 传之前查重:{ content_type, form(标题、语言、翻译状态这些), tags, creators: [{ id, role }], items: [{ kind, size_bytes, width, height, duration_ms }] }(items 是要传的文件按顺序的字节数和图片的宽高),回完全重复的、盖住它的、它盖住的,形状同上 |
GET /bangumi/subjects | 搜 Bangumi 的条目 |
GET /bangumi/subjects/{subject_id}/episodes | Bangumi 条目的各集(本篇、SP、OP……,原名、中文名),用来填每集的标题 |
POST /submissions/{id}/bangumi | 按 Bangumi 的条目填表单 |
POST /submissions/{id}/submit | 提交审核;免审的账号提交即发布。站点可以打开对标题、别名、简介和小说正文的词检查(默认不检查):打开以后含不允许的词返回 75006,含需要审核的词时免审的账号也要审核 |
POST /submissions/{id}/withdraw | 撤回审核中的投稿(撤回后才能再改) |
文件
| 接口 | 说明 |
|---|---|
POST /submissions/{id}/upload-tokens | 要上传令牌:图片、视频、音频、压缩包、封面各有用途,令牌一小时有效 |
POST /submissions/{id}/media | 把传好的图片按顺序挂进投稿 |
POST /submissions/{id}/archives | 登记传好的压缩包,解压成页 |
GET /submissions/{id}/archives | 压缩包的导入记录 |
GET /submissions/{id}/archives/{archive_id} | 一个压缩包的解压报告 |
POST /submissions/{id}/archives/{archive_id}/encoding | 文件名乱码时换编码重新解 |
POST /submissions/{id}/remote-downloads | 填一个链接让站点去下载,或者登记一个传完的视频、音频(upload_id);下完、收好以后自动挂进那一集 |
GET /submissions/{id}/remote-downloads | 链接下载、登记的上传的进度(结束了的记录默认留 30 天) |
DELETE /submissions/{id}/remote-downloads/{download_id} | 取消没下完的链接下载(登记的上传在收尾时不能取消) |
PUT /submissions/{id}/cover | 设封面:选作品里的一页、一个视频,或另传一张图 |
结构和条目
| 接口 | 说明 |
|---|---|
GET /submissions/{id}/structure | 结构:单元(话、集、卷)、每个单元里的页和视频、封面 |
PUT /submissions/{id}/structure | 改结构(整份提交:分话、顺序、去掉的条目),带 version |
POST /submissions/{id}/units | 改集的修改里再加几集:{ "unit_ids": [305, 306] }(待审时不能加) |
DELETE /submissions/{id}/units/{unit_id} | 把一集移出改集的修改(这一集的改动不要了,新传的文件一起删掉) |
PATCH /submissions/{id}/items/{item_id} | 改条目信息:名字、用途、默认音轨、视频的封面帧 |
GET /submissions/{id}/items/{item_id}/subtitles | 字幕列表 |
POST /submissions/{id}/items/{item_id}/subtitles | 传字幕(视频还收 PGS 图形字幕 .sup,大小上限另算) |
PATCH /submissions/{id}/items/{item_id}/subtitles/{subtitle_id} | 改字幕的名字、是否默认、是否强制显示 |
DELETE /submissions/{id}/items/{item_id}/subtitles/{subtitle_id} | 删字幕 |
GET /submissions/{id}/items/{item_id}/subtitles/{subtitle_id}/vtt | 改已经上线的一集时新传、还没生效的字幕转成 WebVTT,提交前预览用 |
GET /submissions/{id}/items/{item_id}/subtitles/{subtitle_id}/source | 同上,取原文(PGS 是原样的文件) |
GET /submissions/{id}/items/{item_id}/fonts | 视频的字体列表(scope:video 这一集自己的 / work 作品的字体带来的) |
POST /submissions/{id}/items/{item_id}/fonts | 传字体(TTF、OTF、TTC、WOFF2;同名的换掉) |
DELETE /submissions/{id}/items/{item_id}/fonts/{font_id} | 删字体(作品的字体带来的删不掉,在作品的字体里删) |
GET /submissions/{id}/fonts | 作品的字体:传一次,作品里的各集都带上(只有动画这类有视频的) |
POST /submissions/{id}/fonts | 传作品的字体(同视频的字体;同名的换掉)。只认新作品的投稿,作品发布以后用「我的作品」的 /me/works/{id}/fonts |
DELETE /submissions/{id}/fonts/{font_id} | 删作品的字体 |
GET /submissions/{id}/items/{item_id}/chapters | 视频的章节和片头片尾 |
PUT /submissions/{id}/items/{item_id}/chapters | 设章节和片头片尾(不用等转码完成) |
POST /submissions/{id}/texts | 加文本条目(音声的台本这类) |
GET /submissions/{id}/items/{item_id}/content | 文本条目的内容 |
PUT /submissions/{id}/items/{item_id}/content | 改文本条目的内容 |
POST /submissions/{id}/items/{item_id}/replace | 换视频、音频文件:要上传令牌 |
DELETE /submissions/{id}/items/{item_id}/replace | 换文件失败时清掉失败的记录 |
POST /submissions/{id}/items/{item_id}/reprocess | 视频、音频按现在的转码设置重新转码(同一个文件有间隔限制) |
我的作品
| 接口 | 说明 |
|---|---|
GET /me/works | 我创建的作品(各种状态都在)和我共创的作品,role 是 owner 创建者 / co_creator 共创者(游标分页,按最近改动排,content_type 可以多个)。带 q 按名字搜:各语言的标题、别名、作品号里含这几个字的(不分大小写、全角半角),最长 255 个字 |
GET /me/works/{id}/structure | 自己创建的、共创的已发布作品现在的全部单元,包括定时、暂不公开的 |
PUT /me/works/{id}/structure | 调整已发布作品各集的顺序、把视频或页挪到别的集(直接生效,不用审核):{ "version": 12, "units": [{ "id": 305, "item_ids": [5001, 5002] }, …] },每一集、每个条目都要出现且只出现一次,不删、不加,集名照旧;正在改的集、被下架的集里的条目不能动 |
PUT /me/works/{id}/units/{unit_id}/schedule | 定时的一集改时间或马上上线(直接生效,不用审核) |
POST /me/works/{id}/items/{item_id}/reprocess | 自己已发布的作品里的视频、音频重新转码(直接生效,不用审核) |
GET / POST /me/works/{id}/fonts、DELETE /me/works/{id}/fonts/{font_id} | 自己已发布的作品的字体(各集共用,直接生效,不用审核) |
作品共创
一部作品有一个创建者(就是投稿人,作品的 uploader)和最多 10 个(默认)共创者(作品详情的 co_creators)。共创者和创建者一样管内容:加新的一集、改集、改作品信息、定时、重新转码、小说的写作和导入,看没公开的部分,自己的作品免费看、下载不算次数。只有创建者能做的:改价格、邀请和移出别人、转让(共创者调是 10003,data.reason 是 owner_only);分成、打赏、悬赏归创建者。
只能对已发布的作品申请加入、邀请、转让、申请接管。站点关了共创(GET /site 的 features.co_creation 为 false)时,申请、邀请、转让、接管和同意、拒绝、撤回都返回 10003(data.reason 是 feature_off),移出、退出照样能用。申请加入、申请接管、邀请、转让按人限频(默认每小时 30 次)。作品页能不能申请看作品详情的 viewer.co_creation(第 3.4 节)。
| 接口 | 说明 |
|---|---|
POST /works/{id}/co-creation/apply | 申请加入:{ "message": "想一起做" }(可以不带,最多 200 字),创建者同意才加入 |
POST /works/{id}/co-creation/takeover | 向站务申请接管没人管的作品:{ "reason": "作者一年没更新了" }(最多 500 字);作品要停更满 30 天(默认),站务批准时定成创建者还是共创者 |
GET /me/co-creation/requests | 我的共创:box=incoming(默认,等我处理的:邀请我的、转给我的、申请加入我的作品的)/ outgoing(我发起的);status=pending(默认,还在等的)/ all;游标分页 |
POST /me/co-creation/requests/{id}/accept | 同意(申请加入由创建者同意,邀请、转让由对方同意;接管由站务处理) |
POST /me/co-creation/requests/{id}/decline | 拒绝 |
POST /me/co-creation/requests/{id}/cancel | 撤回我发起的 |
GET /me/works/{id}/co-creators | 创建者和共创者:role、owner、members(account、joined_at、via 怎么加入的、added_by)、max、requests(还在等的申请、邀请、转让,只有创建者看得到) |
POST /me/works/{id}/co-creators/invitations | 邀请:{ "account_id": 9, "message": "来帮忙" },对方接受才加入;对方已经申请了的当场同意那份申请。找人用 GET /accounts/search |
DELETE /me/works/{id}/co-creators/{account_id} | 创建者移出共创者;共创者移出自己就是退出 → { "removed": true } |
POST /me/works/{id}/transfer | 转让创建者:{ "account_id": 9, "message": "交给你了", "keep_creator": true },对方接受了才换;keep_creator 为 true(默认)时我留下当共创者 |
申请、邀请、转让、接管是同一个形状:
id、kind(apply/invite/transfer/takeover)、status(pending在等 /accepted/declined/canceled撤回了或没法再同意 /expired过期)、work、user(申请的、被邀请的、接收的人)、created_by、message、keep_creator、granted(接管批准成了owner还是co_creator)、note(站务的说明)、created_at、expires_at(申请、邀请、转让 14 天,接管 30 天,默认)、decided_at。不能申请、邀请、转让时是
72026,data.reason:owner(自己是创建者)/member(已经是共创者)/self(邀请、转给自己)/pending(已经有一份在等的,data.request_id)/cooldown(被拒以后 7 天内(默认)不能再申请,data.available_at)/full(共创者满了,data.max)/active(作品还没停更满 30 天,不能申请接管,data.available_at)/no_owner(作品没有创建者,可以申请接管)/not_published(作品没发布)。已经不在等了的申请再同意、拒绝、撤回是
72027(data.status是它现在的状态);不归你处理的、不是你发起的是10004。移出、退出、转让时不再管这部作品的人,他在这部作品上没完成的修改(加集、改集、改信息)作废。
有人申请、邀请、转让、结果出来时发通知
co_creation(第 7 节)。
小说:写作、导入、发布
| 接口 | 说明 |
|---|---|
GET /me/works/{id}/chapters | 卷章概览 |
GET /me/works/{id}/chapters/{item_id}/nav | 一章在全书里的位置:作品的状态、这一章、所在的卷、全书第几章 / 几章、上一章和下一章(不用取整份概览) |
POST /me/works/{id}/units | 建卷 |
PATCH /me/works/{id}/units/{unit_id} | 改卷名 |
DELETE /me/works/{id}/units/{unit_id} | 删空卷 |
POST /me/works/{id}/chapters | 建章 |
PATCH /me/works/{id}/chapters/{item_id} | 改章名 |
DELETE /me/works/{id}/chapters/{item_id} | 删章 |
POST /me/works/{id}/chapters/{item_id}/merge | 把草稿章并进同一卷里的上一章或下一章:{ "into": "previous", "version": 10 }(正文、作者的话接进去,这一章删掉) |
PUT /me/works/{id}/order | 调整卷和章的顺序 |
GET /me/works/{id}/chapters/{item_id}/body | 取正文的工作稿 |
PUT /me/works/{id}/chapters/{item_id}/body | 存正文的工作稿(读者看到的不变) |
POST /me/works/{id}/preview | 预览正文的显示效果 |
POST /me/works/{id}/chapters/{item_id}/publish | 发布一章(马上或定时) |
POST /me/works/{id}/chapters/publish | 一次发布几章(最多 50 章,各自返回结果) |
POST /me/works/{id}/chapters/{item_id}/withdraw | 撤回审核中的章、取消定时 |
PUT /me/works/{id}/chapters/{item_id}/schedule | 改定时:{ "status": "scheduled", "publish_at": "…" } 改时间,{ "status": "published" } 马上公开;只认定时的章、审核中的章,不重新送审 |
POST /me/works/{id}/chapters/schedule | 一次改几章的定时(最多 50 章,各自返回结果) |
POST /me/works/{id}/chapters/{item_id}/body/publish | 用工作稿更新已发布的正文 |
GET /me/works/{id}/chapters/{item_id}/body/published | 取已发布正文的原文 |
GET /me/works/{id}/chapters/{item_id}/revisions | 正文的修订记录 |
GET /me/works/{id}/chapters/{item_id}/revisions/{revision_id} | 一条修订 |
POST /me/works/{id}/upload-tokens | 插图的上传令牌 |
GET /me/works/{id}/images | 插图列表 |
POST /me/works/{id}/images | 登记传好的插图 |
DELETE /me/works/{id}/images/{uid} | 删插图 |
PUT /me/works/{id}/cover | 设封面 |
POST /me/works/{id}/import-uploads | 断点续传要导入的 TXT、EPUB 文件(tus 1.0.0,另有对应的 HEAD、PATCH) |
POST /me/works/{id}/imports | 从传好的文件切出大纲 |
GET /me/works/{id}/imports | 导入记录 |
GET /me/works/{id}/imports/{import_id} | 一次导入:大纲和切章规则(导完、失败或过了保留期,数据清掉以后大纲是空的) |
PATCH /me/works/{id}/imports/{import_id} | 改编码、切章规则、大纲 |
GET /me/works/{id}/imports/{import_id}/segments | 导入文件的段落 |
POST /me/works/{id}/imports/{import_id}/apply | 按大纲导入 |
5. 书架(用途「书架」)
这一节读写的都是你自己的数据,令牌要有「书架」用途。下文数字后面标了「(默认)」的,以站点设置为准;「10001(follows: too_many)」表示返回 10001,data.errors 里 follows 的原因是 too_many。
5.1 追番追更
| 接口 | 说明 |
|---|---|
PUT /me/follows/{work_id} | 关注一部作品 → { "following": true };已经关注了也不报错 |
DELETE /me/follows/{work_id} | 取消关注 → { "following": false } |
GET /me/follows | 你关注的作品,游标分页;有更新的在前,再按最近更新排。带 updated=true 只列有更新的 |
POST /me/follows/read | 标为已读:把关注的作品的「有更新」清零,不改阅读进度、历史 → { "cleared": 3 }(清掉的部数)。不带 body 是全部;{ "work_ids": [12, 34] } 只标这几部 |
列表每项是 { "work", "followed_at", "new_units" },work 是作品列表项(同 GET /works 的 items):
new_units:你上次报这部作品的阅读进度以后(没报过的从关注时算;标为已读过的从标的时候算)新公开的单元数,小说是章数;没有为0。只有报进度(PUT /me/progress/{work_id},第 5.6 节)和标为已读(POST /me/follows/read)会把它清零,读作品详情、单元、条目都不会。下架、删除的作品不在列表里,恢复后回来。最多关注 2000 部(默认),超出
10001(follows: too_many)。
5.2 关注标签、创作者、系列
| 对象 | 关注 / 取消 | 你关注的(游标分页,新关注的在前) |
|---|---|---|
| 标签 | PUT /me/follows/tags/{id}、DELETE /me/follows/tags/{id} | GET /me/follows/tags,每项 { "tag": { "id", "namespace", "name" }, "followed_at" } |
| 创作者 | PUT /me/follows/creators/{id}、DELETE /me/follows/creators/{id} | GET /me/follows/creators,每项 { "creator": { "id", "name", "roles" }, "followed_at" } |
| 系列 | PUT /me/follows/series/{id}、DELETE /me/follows/series/{id} | GET /me/follows/series,每项 { "series": { "id", "name", "position" }, "followed_at" },position 为 null |
PUT返回{ "following": true },DELETE返回{ "following": false };取消本来没关注的也不报错。每种最多关注 1000 个(默认),满了
10001(follows: too_many);关注着的里后来禁用了的、还在审核的不算。不存在、禁用了、还在审核的不能关注(10004)。合并掉的标签、创作者、系列:用旧 id 关注或取消,作用在合并进的那个上;已有的关注会转过去(
followed_at不变)。关注后被禁用的不列出,重新启用后回来。
5.3 关注动态和订阅链接
GET /me/feed:关注动态,游标分页,新的在前。包括最近 30 天(默认)里:
你关注的标签(连同下位标签)、创作者、系列的新作品(这段时间里首次发布的);
你关注的作品新公开的集、话、章(作品首次发布时一起公开的不算)。
两种都按你的内容过滤设置筛过;一共最多 500 条(默认),更早的不列。每项的字段:
| 字段 | 说明 |
|---|---|
id | 这一条的标识(字符串,去重用):新作品是 w:作品id,更新是 u:作品id:日期 |
kind | new_work 新作品 / new_units 关注的作品有更新 |
work | 作品列表项 |
units、chapters | new_units 时这次公开的集、话(units)或小说的章(chapters),每个 { "id", "title" };同一部作品同一天公开的合成一条;用不上的是空数组 |
reasons | 为什么出现在你的动态里:{ "kind", "id", "name" },kind 是 tag / creator / series / work;因为几个关注出现时全部列出,标签是你关注的那个(作品上的可能是它的下位标签) |
at | 公开的时间 |
订阅链接:GET /feeds/me?token=…&limit= 读同一份关注动态。
这个接口不认个人令牌,不用带
Authorization;认的是查询参数token里的订阅令牌(acgf_开头)。订阅令牌只能登录网站后获取、更换、删除(第 12.1 节),它只能读你的关注动态;泄露了就在网站上换一个,旧的立即失效。返回
{ "account": { "id", "name", "avatar", "deleted" }, "items": [ … ] }:account是令牌的主人,items同GET /me/feed的第一页(按主人的内容过滤)。不分页,limit不带时 20,最多 100。令牌不对、已更换或删除,或者主人被封禁、注销:
10002。按 IP 限频(失败的也算),每小时 120 次(默认)。响应带
Cache-Control: private, max-age=300,可以缓存五分钟。
curl -s "$ACG_API/feeds/me?token=$ACG_FEED_TOKEN&limit=50" # 订阅令牌,不是个人令牌5.4 收藏夹和片单
每人有一个默认收藏夹(第一次调 GET /me/collections 时自动建好,is_default 为 true,能改名,不能删),另外最多能建 100 个(默认)。公开的收藏夹就是片单,别人能看到、点赞。收藏夹对象的字段见第 3.9 节;自己的夹 liked 总是 false。
| 接口 | 说明 |
|---|---|
GET /me/collections | 你的全部收藏夹 → { "items": [ … ] },默认的在前,其余按你排的顺序;不分页 |
POST /me/collections | 新建:name(必填,1 ~ 50 字)、description(0 ~ 500 字,纯文本)、public(布尔)→ 新的收藏夹。数量到上限 10001(collections: too_many) |
PATCH /me/collections/{id} | 只改带了的字段:name、description、public、item_order(added / manual)→ 改后的收藏夹。不是你的 10004 |
DELETE /me/collections/{id} | 删除,连同里面的作品 → { "deleted": true };默认收藏夹不能删(10005) |
PUT /me/collections/order | 排顺序:{ "ids": [5, 3, 8] } 按顺序列出全部非默认的收藏夹(默认的总在最前,不用带)→ { "items": [ … ] };少了、多了、重复、不是你的 10001(ids: invalid) |
GET /me/collections/{id}/items | 夹里的作品,游标分页,另给 total(符合条件的部数),参数见下 |
POST /me/collections/{id}/items | 一次往这个夹放几部:{ "work_ids": [12, 13, 99] } → { "added": [12], "existing": [13], "missing": [99] }:added 是这次放进去的(排在最后),existing 本来就在,missing 看不到、没有的(不报错)。最多 100 部(默认);放完会超过一个夹的上限时一部都不放,10001(work_ids: collection_full);夹不是你的 10004 |
PUT /works/{id}/collections | 设定一部作品在你的哪几个夹里:{ "collection_ids": [1, 3] },之后它正好在这几个夹里(没列的夹会把它拿出来),空数组就是取消收藏 → { "collection_ids": [1, 3] }(按 id 排) |
PATCH /me/collections/{id}/items/{work_id} | 改备注:{ "note": "…" }(0 ~ 500 字,公开的夹别人也看得到)→ 改后的这一项;夹不是你的、作品不在夹里 10004 |
DELETE /me/collections/{id}/items/{work_id} | 从这个夹拿出来 → { "deleted": true };本来不在也算成功,夹不是你的 10004 |
POST /me/collections/{id}/items/{work_id}/move | 手排顺序里挪一部:{ "before": 12 } 挪到作品 12 前面,null 挪到最后 → { "moved": true }。这部不在夹里 10004,before 不在夹里 10001(before: unknown);新放进来的排在最后 |
名字:空的
name: required,太长too_long,有换行这类控制字符invalid;简介、备注太长too_long(都是10001)。PUT /works/{id}/collections:有不是你的夹10001(collection_ids: unknown);一个夹最多 5000 部(默认),满了10001(collection_ids: too_many);作品看不到10004(收藏后才下架的照样能拿出来、改备注)。只想往一个夹里放时,用上面的POST /me/collections/{id}/items;要用这个接口,先从作品详情的viewer.collection_ids取现在的夹(令牌要同时有「浏览」用途),加上新的一起提交。
GET /me/collections/{id}/items 的参数:q 按标题、别名找;content_type 只看一种内容类型(不认识的 10001);sort:added 放进来的时间(新的在前)/ manual 手排 / updated 作品最近更新的在前 / title 按标题(中文按拼音),不带按夹的 item_order,别的值 10001(sort: unknown)。换了参数要从第一页重新取。每项是 { "work", "work_status", "note", "added_at" }:work 是作品列表项,note 是你的备注,work_status 是作品现在的状态(draft / pending / rejected / published / taken_down / deleted),下架、删除的作品照样列在你的夹里。
片单的赞:
| 接口 | 说明 |
|---|---|
POST /collections/{id}/like | 赞一个片单 → { "liked": true, "like_count": 5 };赞过再赞也成功 |
DELETE /collections/{id}/like | 取消赞,返回同上(liked 为 false);没赞过也成功 |
GET /me/liked-collections | 你赞过的片单,后赞的在前;后来改成私有、删除的不列 |
片单不存在或是私有的
10004,赞自己的10005。GET /me/liked-collections是页码分页:page(从 1 起)、page_size(不带时 20,最大 100)→{ "items": [ … ], "pagination": { "page", "page_size", "total", "total_capped" } }。pages()不适用,按page自己翻。
default = call("GET", "/me/collections")["items"][0]
for it in pages(f"/me/collections/{default['id']}/items", {"sort": "added"}):
print(it["work"]["title"], it["work_status"], it["note"])5.5 历史和看过的集
历史就是你的阅读进度(第 5.6 节)按最近读的排。「看过」按单元(一集、一话)记。
| 接口 | 说明 |
|---|---|
GET /me/history | 历史,游标分页,最近读、看、听的在前;每项 { "work", "progress" }:作品列表项和阅读进度。可选 content_type 只看一种内容类型(不认识的 10001)。只列现在看得到的作品(下架、删除的不列,恢复后回来),不受内容过滤影响 |
DELETE /me/history | 清空历史,连同全部「看过」记号 → { "deleted": 42 }(清掉了几部作品的进度)。只删一部用 DELETE /me/progress/{work_id} |
GET /me/seen | 一部作品里你看过的单元:?work_id=12 → { "work_id": 12, "unit_ids": [101, 102] },按目录顺序,看不到的单元不列;不分页 |
PUT /me/seen/{unit_id} | 手动标成看过 → { "seen": true } |
POST /me/seen | 一次标几个:{ "unit_ids": [101, 102, 999], "seen": true }(seen 必填,false 是取消)→ { "seen": true, "unit_ids": [101, 102], "missing": [999], "recorded": true }:unit_ids 是处理了的,看不到的进 missing(不报错);最多 200 个(默认);暂停记录时标看过不保存,recorded 为 false |
DELETE /me/seen/{unit_id} | 取消看过 → { "seen": false } |
GET /me/seen的work_id必填:没带10001(work_id: required),不是数字10001(work_id: invalid),作品看不到10004。标、取消返回的
seen是现在的状态;重复标、重复取消都不报错;单元看不到10004。报进度时也会自动标:请求体带
"finished": true就把那个单元记成看过,false不记;不带时按站点的规则:漫画报到这一话的最后一页;动画的正片(花絮、PV 不算)、音声这一组的最后一条音轨,播到离结尾 2 秒(默认)以内。小说报进度不会自动标,可以手动标。暂停记录:你在个人设置(第 11.2 节)里暂停了历史记录(
history.paused为true)时,报进度、标看过都不保存:报进度照样返回进度对象,但recorded为false;PUT /me/seen/{unit_id}返回原来的状态。取消看过、删历史照常能用。
5.6 阅读进度和书签
每部作品只有一条进度,后报的覆盖先报的(不比较客户端时间),报得再频繁也不会出错。进度对象除了 work_id、updated_at,还有:
| 字段 | 说明 |
|---|---|
unit_id、item_id | 读到的单元(话、集、卷、分组)和条目(页、视频、章、音轨);item_id 为 null 是单元开头 |
item_index、item_count | 记下时条目在单元里的下标(0 起)和单元的条目数 |
paragraph | 小说:读到这一章的第几块(正文分块的序号 n,0 起);别的为 null |
position | 视频、音频:播到第几秒;别的为 null |
unit_index、unit_title、item_title | 按现在的目录算:单元的位置(0 起)、单元名、章名或视频音轨名;没了或看不到时 unit_index、item_title 为 null,unit_title 为空串 |
recorded | 存了没有(只有暂停记录时为 false) |
| 接口 | 说明 |
|---|---|
PUT /me/progress/{work_id} | 报进度 → 进度对象。请求体:unit_id(必填)、item_id(不带是单元开头)、paragraph(小说)、position(视频、音频的秒数,超过时长按时长记)、finished(布尔,见第 5.5 节)。同时把这部作品的 new_units 清零 |
GET /me/progress | 一次取多部作品的进度:?work_ids=12,13 → { "items": [ … ] },按作品 id 排,没读过的不在里面;一次最多 100 个(默认),多了 10001(work_ids: too_many) |
DELETE /me/progress/{work_id} | 清掉这部作品的进度,连同它的看过记号 → { "deleted": true };本来没有也一样 |
报进度:单元不在这部作品里、条目不在这个单元里
10001(unit_id: unknown或item_id: unknown);paragraph、position是负数10001(invalid);作品看不到10004。paragraph只用于小说;position只对视频和音频有用,别的条目、单元开头带了不报错也不记。
call("PUT", "/me/progress/12", {"unit_id": 301, "item_id": 9001, "position": 128.4})小说的书签
书签:{ "id": 77, "work_id": 12, "unit_id": 101, "item_id": 6001, "paragraph": 42, "excerpt": "星海的尽头是另一片星海", "created_at": "…" }。
| 接口 | 说明 |
|---|---|
GET /me/bookmarks | 一部小说的全部书签:?work_id=12 → { "items": [ … ] },按卷、章、段的顺序;不分页;work_id 的规则同 GET /me/seen |
POST /me/bookmarks | 加书签:{ "item_id": 6001, "paragraph": 42, "excerpt": "选中的字" }(item_id、paragraph 必填,item_id 是章,paragraph 是块的序号;excerpt 可选)→ 书签。同一章同一段已经有了就原样返回那一个,不重复建,摘录也不改 |
DELETE /me/bookmarks/{id} | 删书签 → { "deleted": true };别人的 10004 |
excerpt从那一段自动截取:纯文字(注音只留正文、着重去掉标记、注释不算),前 100 字;插图、分隔线和超出范围的段号是空串。加书签时带了
excerpt(读者选中的字):去掉首尾空白以后不为空、是那一段纯文字里连着的一截(阅读时开了简繁转换的,转换以后的字也认)就用它当摘录,最多 100 字;不然照旧自动截取,不报错。一部小说最多 100 个书签(默认),满了
10001(bookmarks: too_many);paragraph没带或是负数10001;item_id不是小说的章、作品看不到10004。作者改了正文,书签跟着挪到那一段新的段号(几秒内;摘录不变);那一段删了的留在原来的段号上(超出这一章的放在最后一块);同一个人两个书签挪到同一段的只留早加的那个。
6. 互动(用途「互动」)
这一节的操作都以你的账号进行:评论和弹幕公开显示(弹幕只带一个匿名代号,不显示名字),评分、点赞和标签投票计入作品的公开统计,纠错和举报交给站务处理。
被禁言时(GET /me 的 restrictions 里有 kind 为 mute 的一项),发评论、改评论、补标签、提纠错、发弹幕都返回 75005,data 是 { "kind": "mute", "until", "reason" }(until 为 null 表示永久);给标签投票、评分、举报不受影响。
6.1 评论
POST /works/{id}/comments 发评论或回复,返回新评论(同 GET /comments/{id});求档下面用 POST /bounties/{id}/comments,请求体相同,但不能带 unit_id、item_id、anchor。请求体:
| 字段 | 说明 |
|---|---|
body | 必填。纯文本,保留换行,网站照原样显示,不当作 Markdown 或 HTML;去掉两头空白后 1 ~ 2000 字(以 GET /site 的 limits.comment_max_chars 为准)。空的 required,太长 too_long,含换行和制表符以外的控制字符 invalid |
unit_id、item_id | 评论的位置:都不带是作品下面;unit_id 是某一集(话),item_id 是小说的某一章。各内容类型能评论的位置见 GET /meta 的 content_types[].comment_scopes(work / unit / item) |
parent_id | 回复时填一级评论的 id。只有两层:回复别人的回复也挂在同一条一级评论下。回复的位置跟着一级评论,unit_id、item_id 不用带,带了不一致报 10001 |
reply_to_id | 回复楼里的某一条回复时填它的 id,必须同时带 parent_id |
spoiler | true 表示有剧透(网站上先折叠),默认 false |
anchor | 可选,指到具体位置:{ "kind": "page", "item_id" } 漫画的一页;{ "kind": "paragraph", "item_id", "n", "text" } 小说一章的第几块(GET /items/{id}/text 里块的 n;text 可选,是那次取到的 text_version);{ "kind": "time", "item_id", "seconds" } 视频、音频的第几秒。回复不带(带了 10001 anchor: invalid) |
@ 提醒:正文里的
@名字按显示名或用户名识别(不分大小写),名字后面要跟空白、标点或结尾(_、-、.算名字的一部分),@前面紧挨着字母或数字的不算;一条最多识别 10 个(以站点设置为准),结果在评论的mentions里。回复会通知被回复的人,@到的人另有通知;回复自己、@自己不通知。段评:在小说一章(带
item_id)发一级评论、anchor是{ "kind": "paragraph", "item_id", "n" }就是那一段的段评,n要是这一章公开的正文里有字的段落(分隔线、空行、插图、超出范围的返回10001anchor: invalid)。带上anchor.text时,正文在你读的时候被改过(版本对不上)返回10005:重新取这一章、再发。作者改了正文、段的序号变了,段评跟着挪到新的序号(anchor.n一起改);那一段删了的变成本章说(anchor为null),那一段回来了再挂回去。其他参数错误:
parent_id: unknown(不是这部作品的一级评论,或已删除);reply_to_id: unknown(不在这条楼里)或invalid(没带parent_id);unit_id: unknown(不是这部作品的集);unit_id/item_id: invalid(这种内容不能在那一处评论);anchor: invalid(种类和条目对不上,或不在这一集、这一章里);anchor.item_id: unknown(不是这部作品的条目)。作品看不到、没发布,或者那一集、那一章看不到,返回
10004。正文含站点不允许的词返回74001,data.hits是[{ "word", "count" }](普通词按包含匹配,不分大小写和全角半角;按规则匹配的,word是匹配到的原文)。发得太快返回10006(默认每人每分钟 10 条)。
| 接口 | 说明 |
|---|---|
PATCH /comments/{id} | 改自己的评论:{ "body", "spoiler" },带了的才改;返回改后的评论。正文真的变了 edited_at 才更新,@ 重新识别,只通知新 @ 到的人 |
DELETE /comments/{id} | 删自己的评论 → { "deleted": true }。下面有回复的一级评论留一个位置(deleted 为 true,body 为空串,account 为 null),回复照样保留。站务删掉一集、一章或删除作品时,那里的评论连回复一起删掉 |
PUT /comments/{id}/like | 点赞 → { "liked": true, "likes": 4 };重复点赞不报错。不能给自己的评论点赞(10003) |
DELETE /comments/{id}/like | 取消点赞,返回同上(liked 为 false);重复取消不报错 |
改、删别人的评论返回 10003;评论已删除、作品看不到了返回 10004。
c = call("POST", "/works/12/comments", {"body": "这一话的分镜很好看", "unit_id": 101})
call("POST", "/works/12/comments", {"body": "同感", "parent_id": c["id"]})6.2 打分
| 接口 | 说明 |
|---|---|
PUT /works/{id}/rating | 打分或改分:{ "score": 8 },1 ~ 10 的整数(网站上的 5 星,每半星一档)→ { "score": 8, "stats": { "score": 8.6, "score_count": 12 } } |
DELETE /works/{id}/rating | 撤回 → { "score": null, "stats": { … } };没打过也算成功 |
stats是这次改完以后的统计:score是平均分(1 ~ 10,保留一位小数),score_count是打分人数;人数不到 3(以站点设置为准)时score为null。给自己投稿的、自己共创的作品打分返回
10003;分数不是 1 ~ 10 的整数返回10001(score: invalid);作品看不到或没发布返回10004。你打过的分也在
GET /works/{id}的viewer.rating里(令牌要有「浏览」用途;没打过为null)。
6.3 点赞一集、一章
给作品的一集点赞。点赞和收藏是两回事:收藏放进自己的收藏夹(第 5.4 节),点赞只给这一集加一个数。在哪一级点看 GET /meta 的 content_types[].like_on:漫画、动画、音声是 unit(每一话、每一集、每一组),小说是 item(每一章,卷不能点)。作品本身不能直接点,作品的 stats.likes 是各集、各章的点赞加起来。
| 接口 | 说明 |
|---|---|
PUT /units/{id}/like | 点赞一集 → { "liked": true, "likes": 13, "stats": { "likes": 120 } }:likes 是这一集的点赞数,stats.likes 是作品改完的点赞数;重复点赞不报错 |
DELETE /units/{id}/like | 取消,返回同上(liked 为 false);没点过也算成功 |
PUT /items/{id}/like、DELETE /items/{id}/like | 小说的一章,同上 |
那一集、那一章看不到或者还没公开(定时、暂不公开、被下架的,作品没发布的),或者那一级不能点(小说的卷、漫画的页这类),返回
10004。给自己投稿的、自己共创的作品点赞返回
10003。点得太快返回
10006(点赞和取消一起算,默认每人每分钟 30 次,以站点设置为准)。你点过没有在单元的
viewer.liked(GET /works/{id}/units、GET /units/{id})和章节正文的item.viewer.liked里(令牌要有「浏览」用途)。
r = call("PUT", "/units/101/like")
print(r["likes"], r["stats"]["likes"])6.4 补标签和投票
作品上显示哪些标签由投票决定。GET /works/{id}/tags 列出候选标签和两条线 thresholds(默认 show 为 3、remove 为 -3):没显示的候选分数到 show 才显示,显示着的掉到 remove 以下才撤下。每一票有权重(按投票人以往补的标签被采纳的比例算),所以 score 不一定等于票数;站务锁定的标签(locked 为 true)不能投票。
PUT /works/{id}/tags/{tag_id}/vote 投票:{ "vote": 1 },1 赞成、-1 反对、0 撤回;返回这个标签的最新状态(同 GET /works/{id}/tags 的 items 里的一项)。没带 vote 报 vote: required,别的值报 vote: invalid;标签被锁定返回 72023;作品看不到、没发布,或者标签不是这部作品的候选,返回 10004。
POST /works/{id}/tags 补标签:已有的标签带 { "tag_id": 45 },新提一个带 { "namespace": "female", "name": "眼镜" }(命名空间代码见 GET /meta 的 tag_namespaces);返回同上。别名会换成正式标签,补的人自动投一票赞成。新提的标签要站务通过以后才算数。你补的标签第一次显示时会发积分奖励(同一部作品的同一个标签只发一次,每天有上限)。参数错误(10001):
| 原因 | 说明 |
|---|---|
tag_id: required | tag_id 和 name 都没带 |
tag_id: unknown | 没有这个标签 |
namespace: unknown | 命名空间不认识,或者是挂创作者的命名空间(tag_namespaces 里 creator_role 不为 null 的,如 artist) |
name: invalid | 名字是空的或超过 100 字 |
name: new_tags_not_allowed | 这个命名空间不接受新提的标签 |
tag_disabled | 标签已禁用(报在 tag_id 或 name 上,下同) |
exists | 已经是这部作品的候选 |
tags: too_many | 这部作品的候选满了(默认 300 个) |
作品的标签列表被锁定、或者这个标签被锁成不显示时返回 72023;作品看不到、没发布返回 10004。投票和补标签合计默认每人每分钟 60 次,超过返回 10006。
call("POST", "/works/12/tags", {"tag_id": 45})
call("PUT", "/works/12/tags/46/vote", {"vote": -1})6.5 弹幕
只有视频条目(GET /units/{id}/items 里 kind 为 video 的)有弹幕,别的条目返回 72016。站点关闭弹幕时(GET /site 的 features.danmaku 为 false),弹幕接口都返回 10003。
POST /items/{id}/danmaku 发一条弹幕,返回刚发的这一条(同 GET /items/{id}/danmaku 的 items 里的一条,mine 为 true):
| 字段 | 说明 |
|---|---|
time | 必填,出现在视频的第几秒:0 到视频时长,按一位小数保存;没带 required,超出范围 invalid |
text | 必填,一行纯文本,去掉两头空白后 1 ~ 100 字(以 GET /site 的 limits.danmaku_max_chars 为准);空的 required,太长 too_long,含换行等控制字符 invalid |
mode | scroll 滚动(默认)/ top 顶部 / bottom 底部;要在 GET /site 的 danmaku.modes 里,否则 unknown |
color | #RRGGBB,要是 GET /site 的 danmaku.colors 里的一种(不分大小写),否则 unknown;不带是列表的第一种(白色) |
字段错误是
10001,键是上面的字段名。看不到这个视频(包括作品没发布、这一集还没公开)返回10004;含屏蔽词返回74001(同评论);发得太快返回10006(默认每人每分钟 20 条)。DELETE /danmaku/{id}删自己的一条 →{ "deleted": true };别人的返回10003,已删除或不存在返回10004。站务删掉一个视频或删除作品时,上面的弹幕一起删掉。
d = call("POST", "/items/7001/danmaku", {"time": 615.5, "text": "前方高能", "mode": "top"})
call("DELETE", f"/danmaku/{d['id']}")6.6 纠错和举报
POST /works/{id}/corrections 给作品信息纠错,交给站务处理:
fields:必填,错在哪里,至少一项:titles标题、description简介、creators创作者、series系列、released_on首发日期、codes作品号、links外部链接、other其他。body:必填,说明,去掉两头空白后 1 ~ 1000 字。
返回纠错对象 { "id", "work", "fields", "body", "status", "note", "created_at", "decided_at" }:status 是 pending 待处理 / accepted 已采纳 / rejected 未采纳;note 是站务的回复(没写为 null);work 是作品现在的摘要,看不到了为 null。处理后你会收到 correction 通知。参数错误:fields: required、fields[i]: unknown、body: required、body: too_long;没处理的纠错每人最多 5 条(以站点设置为准),多了报 corrections: too_many。作品看不到、没发布返回 10004。站点打开了纠错的词检查时(默认不检查),body 含站点不允许的词返回 75006,data.hits 同 74001。
GET /me/corrections 列出我提的纠错(游标分页,新的在前)→ { "items": [纠错对象], "next_cursor" }。
POST /reports 举报,请求体:
| 字段 | 说明 |
|---|---|
kind | 必填:work 作品 / comment 评论 / user 用户 / danmaku 视频弹幕 |
target_id | 必填:对象的 id(作品、评论、用户、弹幕的 id) |
reason | 必填:原因代码,从 GET /meta 的 report_reasons 里选 kinds 包含这种对象的 |
body | 可选,补充说明,0 ~ 500 字 |
unit_id、item_id | 可选,只有 kind 为 work 时能带,指到某一集(话)或小说的某一章,两个不能同时带。能指到哪一级同评论的位置(comment_scopes):漫画、动画用 unit_id,小说用 item_id,音声只能举报整部作品 |
默认的原因如下(站务可以调整,以 GET /meta 为准):
code | 原因 | 能用于 |
|---|---|---|
spam | 广告、刷屏 | comment、user、danmaku |
abuse | 人身攻击、骚扰 | comment、user、danmaku |
minor | 涉及未成年人 | work、comment、danmaku |
illegal | 违法内容 | work、comment、danmaku |
rating | 分级不对 | work |
spoiler | 剧透没折叠 | comment、danmaku |
copyright | 侵权 | work |
other | 其他 | 全部 |
返回举报对象:
id、kind、target_id、unit_id、item_id、reason、body、status(open待处理 /resolved已处理 /dismissed不成立)、created_at、handled_at,以及work、comment、user、danmaku:按kind给出被举报对象现在的样子,其余为null。作品是作品摘要(看不到了为null),用户是{ "id", "name", "avatar", "deleted" },评论和弹幕是{ "id", "excerpt", "deleted" }(评论取前 50 字,弹幕是全文,已删除的excerpt为空串)。同一个对象你只能有一条待处理的举报:再举报会更新原来那条的原因和说明(
id、created_at不变);处理过以后再举报是新的一条。处理后你会收到report通知。对象不存在或看不到(已删除的评论和弹幕、已下架的作品、已注销的用户)返回
10004。参数错误:kind: invalid;reason: invalid(不认识或不能用于这种对象);body: too_long;target_id: own(举报自己,或自己的评论、弹幕、自己投稿和共创的作品);unit_id/item_id: unknown(不是这部作品的或看不到);unit_id/item_id: invalid(kind不是work、两个一起带、这种内容指不到这一级)。默认每人每分钟 10 次(失败的也算),超过返回10006。
GET /me/reports 列出我的举报(游标分页,新的在前)→ { "items": [举报对象], "next_cursor" }。
call("POST", "/reports", {"kind": "comment", "target_id": 777, "reason": "spoiler", "body": "没有标剧透"})6.7 按名字找用户
GET /accounts/search?q=月&limit=10 → { "items": [{ "id", "name", "avatar", "deleted" }] }:显示名或用户名以 q 开头的用户(不分大小写,按显示名排序),已注销的不在内。q 去掉两头空白后至少 1 个字,否则报 q: required;limit 默认 10,最多 20。写评论前可以用它确认要 @ 的名字。
7. 通知(用途「通知」)
站内通知,令牌要有「通知」用途。哪些种类会发由你的个人设置(第 11.2 节的 notifications)决定:关掉的种类不再生成,已有的留着。没读的通知从 updated_at 起保留 180 天,读过的从读的时候起保留 30 天;每人最多留 2000 条,多的删最旧的(都是默认,以站点设置为准)。
7.1 接口
| 接口 | 说明 |
|---|---|
GET /me/notifications | 通知列表,游标分页,按 updated_at 新的在前。kind:只要这几种(逗号分开;不认识的种类不报错,只是匹配不到);unread=true:只要没读的 |
GET /me/notifications/unread | 没读的有几条 → { "count": 3, "kinds": { "comment_reply": 2, "work_update": 1 } };要轮询时建议一分钟一次 |
POST /me/notifications/read | 标成已读,三选一:{ "ids": [9001, 9002] }、{ "kind": "work_update" }、{ "all": true } → { "count": 1 }(还剩几条没读)。都没带或带了不止一样 10001(ids: invalid);不是你的 id 会被忽略 |
DELETE /me/notifications/{id} | 删一条 → { "deleted": true };不存在或不是你的 10004 |
ids = []
for n in pages("/me/notifications", {"unread": "true"}):
print(n["kind"], n["count"], n["work"]["title"] if n["work"] else "")
ids.append(n["id"])
if ids:
call("POST", "/me/notifications/read", {"ids": ids})7.2 通知的形状和种类
{ "id": 9001, "kind": "comment_reply", "read": false, "created_at": "…", "updated_at": "…",
"actor": { "id": 5, "name": "示例用户", "avatar": null, "deleted": false }, "work": { "…": "作品列表项" },
"comment": { "id": 777, "excerpt": "说得对……", "deleted": false }, "count": 1,
"data": { "parent_id": 770, "unit_id": null, "item_id": null } }actor是做这件事的人,work是相关作品(作品列表项),comment是相关评论;用不上的为null,作品看不到了(比如下架)work也为null。comment.excerpt是评论的前 50 个字;评论删了时excerpt为空串、deleted为true。合并:
work_update同一部作品的、work_comment和income同一部作品同一天、bounty_comment同一个求档同一天(按站点时区)的没读的通知合成一条,count累加、updated_at更新;读了以后再来的另起一条。你自己的评论不会通知你;同一条评论已经让你收到
comment_reply或mention的,不再发别的种类。work_update、new_work在内容公开后一两分钟内到,你创建的、共创的作品不发。遇到不认识的
kind,跳过或者按data原样处理。
kind | 什么时候 | 带什么 |
|---|---|---|
comment_reply | 有人回复了你的评论 | actor、work、comment(那条回复);data:parent_id(所在的一级评论)、unit_id、item_id(评论在哪一集、哪一章,作品下面的为 null);求档下面的评论 work 为 null,data 是 parent_id 和 bounty({ "id", "title" }) |
mention | 有人在评论里 @ 了你 | 同 comment_reply |
work_comment | 有人评论了你的作品(你创建的、共创的) | work;count 是条数,actor、comment 是最近的一条 |
bounty_comment | 有人评论了你发起的求档 | data.bounty({ "id", "title" });count 是条数,actor、comment 是最近的一条 |
review | 你的投稿、章节审核有了结果 | work;data:result(approved / rejected)、kind(new_work / new_units / chapter / work_edit / unit_edit)、submission_id(chapter 为 null)、item_id(chapter 的那一章)、reason(退回原因)、template(退回模板名);通过时后两项为 null |
co_creation | 作品共创:有人申请加入你的作品、邀请你、要把作品转给你;你的申请、邀请、转让、接管有了结果;你被移出、共创者退出、作品的创建者换了 | work、actor(站务做的为 null);data:event(requested / accepted / declined / removed / left / added / owner_changed)、kind(apply / invite / transfer / takeover,站务直接做的、退出为 null)、request_id、title、role(你现在在这部作品里是 owner / co_creator / null)、by_staff;owner_changed 另有 owner(新创建者 { "id", "name" });站务批准、拒绝接管时有 note |
work_update | 你关注的作品有更新 | work;count 是新公开了几集、几章;data.units 或 data.chapters 是最近几个 { "id", "title" },早的在前 |
new_work | 你关注的标签、创作者、系列有新作品 | work;data.reasons(同关注动态的 reasons) |
income | 你的作品被解锁,分到了积分 | work;count 是解锁次数,data.amount 是一共分到的积分 |
tip | 有人打赏了你投的作品 | actor、work;data:amount、given(你实际拿到的)、message(没写为空串) |
bounty | 求档悬赏有进展 | actor、work(应征的作品);data:event(answered / accepted / rejected / fulfilled / expired / cancelled)、bounty({ "id", "title" }),按事件另带 amount、given、refunded、reason |
edge | 你的分发节点审批有了结果 | data:event(approved / disabled / rejected)、node(节点编号);批准时有 status(active / paused)、trust(public / trusted),停用、拒绝时有 reason(admin / offline),不适用的为空串 |
reward | 奖励积分到账 | work(有的话);data:reason(upload / writing / tag / signup / adjust)、amount(adjust 可能是负数) |
correction | 你提的纠错处理了 | work;data:result(accepted / rejected)、note(站务的回复,没写为 null) |
announcement | 系统公告 | data:title、body(Markdown 原文)、url、level(info / warning / important);在你下次取通知列表或未读数时补进来,公告撤回后消失 |
takedown | 你的作品(你创建的、共创的),或其中的一集、一话、一章被下架 | work 一般为 null;data:work_id、title、submission_id、reason(原因代码)、reason_name、text(站务的说明,可能是空串);单集下架另有 unit_id、unit_title,单章下架另有 item_id、chapter_title(不然为 null) |
work_deleted | 你的作品(你创建的、共创的)被站务删除 | data:work_id、submission_id、title、reason |
sanction | 你被处罚、处罚解除 | data:action(applied / lifted)、kind(mute / upload_ban)、until(null 是永久)、reason |
report | 你的举报处理了 | work(被举报的作品,或评论、弹幕所在的作品)、comment(举报的是评论时);data:result(resolved / dismissed)、kind(work / comment / user / danmaku)、target_id、target_account(举报的是用户时有) |
8. 积分(用途「积分」)
本节的接口要令牌带「积分」用途。会花积分的接口有:解锁 POST /unlocks、打赏 POST /works/{id}/tips,以及第 9 节的发起求档 POST /bounties、加码 POST /bounties/{id}/stakes(用途「求档」)。令牌带了这两种用途,程序就能花你的积分,请只给确实需要的程序。
这四个花积分的接口:
都必须带请求头
Idempotency-Key(1 ~ 64 个字符,只能是字母、数字和-_.:),每次新的付款生成一个新的;没带或格式不对10001。同一个键再请求,返回第一次的结果,不会重复扣分,所以网络出错时用同一个键重试是安全的(解锁连失败的结果也原样返回:积分不够时充值以后要换新的键)。同一个键、请求内容不一样76007。积分不够
76003,data有balance(你的余额)和price(要扣的分);账号封禁中不能付款76006;积分暂时用不了76005(没扣分,稍后再试)。
8.1 余额和变动
GET /me/points 返回:
balance:余额。member:会员权益,不是会员为null:role(code、name)、percent(解锁减百分之几,100 是免费)、daily_free(每天前几次解锁免费)、daily_free_left(今天还剩几次)。downloads:今天的下载次数:limit、used、reset_at(重置的时间)。checkin:签到:amount(签一次加几分,0 是签到没开)、done_today(今天签过没有)。
GET /me/points/entries:积分变动,新的在前,游标分页。每条有 id、kind、amount(加分为正、扣分为负)、title、work(相关的作品,没有或看不到为 null)、bounty_id(bounty、bounty_refund、bounty_income 三种是求档 id,其他为 null)、created_at。title 是作品名(动画带集名)、标签名或求档标题;signup、checkin 是空字符串。
kind | 说明 |
|---|---|
unlock | 解锁扣的 |
refund | 解锁退回的 |
income | 别人解锁你投的作品,你分到的 |
income_clawback | 退款时追回的分成 |
reward | 奖励:投稿通过、原创连载、标签被采纳 |
signup | 注册送分(验证邮箱以后发一次) |
checkin | 签到 |
adjust | 站务加减,title 是原因 |
tip、tip_income | 你打赏出去的;别人打赏你的作品,你拿到的 |
bounty、bounty_refund、bounty_income | 发起求档、加码扣的;到期、取消退回的;应征被采纳拿到的(work 是应征的作品) |
edge | 分发节点的奖励,title 是节点号 |
p = call("GET", "/me/points")
print("余额", p["balance"])
for e in pages("/me/points/entries"):
print(e["created_at"], e["kind"], e["amount"], e["title"])8.2 签到
POST /me/checkin(不用请求体)→ { "granted": 5, "already": false, "balance": 1239 }
每天一次,按站点时区的自然日。签一次加
GET /me/points里的checkin.amount(默认 5,以站点设置为准);签到和其他奖励一样有每天的上限,到了上限granted会比它少,可能是 0。今天已经签过也返回成功:
already为true、granted为 0。所以每天定时调一次即可,多调没有副作用。签到没开
76015。
r = call("POST", "/me/checkin")
print("今天签过了" if r["already"] else f"签到 +{r['granted']}", "余额", r["balance"])8.3 解锁和下载
阅读、在线看、在线听(站点转码后的版本)都免费。要花积分解锁的是下载类的东西:哪种内容能下载什么、要不要解锁,看 GET /meta 的 content_types[].downloads(kind、scopes、options、paid)。默认是:漫画 zip(整本或一话);动画 source(每个视频)、zip(一集)、stream(每个视频);音声 stream、source(每条音轨)和 zip(整包);小说 txt、epub(整本)。paid 为 true 的要先解锁,为 false 的登录就能下(照样算次数)。解锁的单位看 content_types[].pricing.scope:漫画、音声、小说按作品解锁(work,以后新加的话、章也包括在内),动画按集解锁(unit)。解锁永久有效。你自己投的作品不用解锁、不花分、不算次数。
价格由站点算,用 GET /works/{id}/price 取。
解锁
POST /unlocks(必带 Idempotency-Key),请求体:
| 字段 | 说明 |
|---|---|
work_id | 必填,作品 id |
expected_price | 必填,你确认过的价,取自报价:整部作品是 price;动画一起买还没解锁的集是 remaining.price,只买一集是那一集的 units[].price;再解锁一次是 renew_price(动画是 units[].renew_price) |
unit_ids | 只用于动画:要解锁哪几集(公开的、没解锁过的);不传或 null 是还没解锁的公开的集一起买(有多集折扣)。其他类型只能是 null |
renew | 可选,默认 false。true 是再解锁一次:下载次数用完以后用,解锁过的也照样买,每个文件的次数从这次起重新算 |
现在的价和 expected_price 不一样时(活动结束、会员到期、作品改价)不扣分,返回 76002,data.quote 是最新的报价,确认以后用新的价和新的键再请求。价是 0 的(会员免费、限时免费)也调这个接口,不扣分;会员的每日免费次数同时用几次也只免费剩下的那几次,多出来的同样返回 76002。
返回这次解锁:
id:字符串,查结果用。status:completed解锁成功;pending暂时不确定扣没扣分(很少见),站点会自动核对,请每隔几秒用GET /purchases/{id}查,一般几秒到几分钟内变成completed或failed;failed没扣分、没解锁。work、unit_ids(动画这次解锁的集,其他类型为空数组)、list_price(打折前)、amount(实际扣的分)、discounts[](用上的折扣:kind是event活动 /member会员 /bundle动画多集一起买,另有name、percent、ends_at)。source:purchase花分买的 /member会员免费 /daily_free会员的每日免费次数 /event限时免费 /staff站务补发。created_at、completed_at、refund(退款:at、reason、returned;没退为null)。
| 错误码 | 什么时候 |
|---|---|
76001 | 没有要解锁的,data.reason:unlocked(都解锁过了,又没带 renew)/ owner(自己创建的、共创的作品,本来就能免费下)/ not_for_sale(这种内容不卖) |
76002 | 价格变了,data.quote 是最新报价 |
76008 | 超过单笔上限(data.max),动画请少选几集分开买 |
10004 | 作品不存在或看不到;unit_ids 里有不属于这部作品或没公开的集(data.unit_ids) |
GET /purchases/{id}:查一次解锁的结果,只能查自己的,返回同上。
GET /me/unlocks:我的解锁,新的在前,游标分页;可选 work_id 只看一部作品的。每项有 work、unit_ids 和 units(动画这次解锁的集:id、title)、source、amount、purchase_id、unlocked_at、expires_at(现在总是 null,即永久)、refunded、refund_reason、work_status(作品现在的状态,不是 published 的已下架或删除,不能下载)。
自动退款:作品被下架或删除时,之前 7 天内(以站点设置为准)花分的解锁全额退回(积分变动 refund),refunded 变成 true,refund_reason 是 work_taken_down / unit_taken_down / work_deleted(站务手动退的是站务写的原因)。动画单独下架某一集时,一次解锁里的集全部下架了才退。退了款的解锁不能再下载,作品恢复以后要重新解锁。
下载
POST /downloads,请求体:
| 字段 | 说明 |
|---|---|
work_id | 必填 |
kind | 必填,这种内容的 downloads[].kind 之一:zip 打包下载、source 源文件、stream 在线播放原档、txt、epub |
unit_id、item_id | 下哪一级(要在 downloads[].scopes 里):整部作品都不传;一个单元(漫画的一话、动画的一集)传 unit_id;一个条目(一个视频、一条音轨)传 item_id |
options | 只用于小说:{ "script": "hans" },script 是 original 原文(默认)/ hans 转简体 / hant 转繁体 |
打包下载(zip)的内容:
漫画:全部原图,按页编号(
001.jpg……),不止一话时一话一个文件夹。动画的一集:这一集各个视频的源文件;上传的字幕放在视频旁边,命名为
视频名.字幕 id.扩展名(如ep01.zh-Hans.ass,播放器能自动加载);Fonts/里是上传的字体和作品的字体。音声:音轨的源文件、插图、台本,上传的字幕放在各自的音轨旁边(命名同上)。
字幕的格式:ASS、SSA 是原文件,PGS 是
.sup,SRT、VTT、LRC 是转换后的 WebVTT(.vtt)。源文件内嵌的字幕、字体在源文件里,不另外附带。source只有上传的原文件。
返回:
status:ready给了地址;preparing小说的文件还在生成,只给retry_after(秒),过这么久用同样的请求再要(不算次数)。url:下载地址,支持断点续传;用的时候现要,不要存起来反复用。小说的是/api/v1/novel-exports/开头的相对地址,接在接口地址的协议和主机后面,下载时不用带令牌,地址过期或不对回 HTTP 403。backup_urls:url连不上(超时、5xx)时按顺序换着用,没有为空数组。expires_at:地址的有效期,默认下载 4 小时、在线播放 6 小时(以站点设置为准)。filename、size(字节,未知为null)、purpose(download下载 /stream在线播放,可以拖进度)。counted:这次算没算次数;downloads_left:今天还能下几次(自己创建的、共创的作品为null);file_downloads_left:这次解锁这个文件还能下几次(不按解锁限次数时为null)。
下载次数(GET /meta 的 download_limits:daily 每人每天几次,per_unlock 每次解锁每个文件几次(0 是不限)):
「同一个文件」指
kind、work_id、unit_id、item_id、options都相同。上次算了次数的地址还没过期时,再要同一个文件不算(counted为false,照样给新地址),所以重试、续传不会多扣。stream和自己的作品不算次数。今天的次数用完
76011,data有limit、reset_at(按站点时区的自然日重置)。这次解锁这个文件的次数用完
76014,data有limit、quote:照quote.renew_price(动画是这一集的units[].renew_price)用POST /unlocks带renew: true再解锁一次。站点可能开启地址绑定 IP:地址只在要地址的那个 IP 上能用(IPv6 是同一个 /64),换了网络会回 HTTP 403,重新要一个即可(上一个没过期就不算次数)。
| 错误码 | 什么时候 |
|---|---|
76010 | 还没解锁,data.quote 是报价 |
76012 | 这个下不了,data.reason:no_source(没有源文件)/ not_playable(浏览器不能直接播放,请下源文件)/ processing(还在处理,或者源文件还在路上,过一会儿再要)/ empty(整包里没有能下的) |
76013 | 下载服务暂时不可用,或者小说的文件刚生成失败,过几分钟再试 |
import time
from urllib.parse import urljoin
from acg_client import API, call
body = {"work_id": 123, "kind": "epub", "options": {"script": "hans"}}
d = call("POST", "/downloads", body)
while d["status"] == "preparing":
time.sleep(d["retry_after"])
d = call("POST", "/downloads", body)
print(urljoin(API, d["url"]), d["filename"]) # 小说给的是相对地址,接到接口地址的主机上8.4 打赏和收入
POST /works/{id}/tips(必带 Idempotency-Key)给作品打赏积分。请求体:amount(必填,整数,在 GET /meta 的 tips.min ~ tips.max 之间)、message(可选,给投稿人的一句话,最多 100 字,只有投稿人在通知里看得到)。
返回 tip 和 balance(打赏以后你的余额)。tip 有 id、work、amount(你打了多少)、given(投稿人实际拿到的)、message、created_at。投稿人按分成比例拿(默认 70%,向下取整;每个投稿人每天的分成有上限,到了就少给),其余作废。
76016打赏不了,data.reason:off(站点关了打赏,tips.enabled为false)/own(你是这部作品的创建者;共创者能打赏,积分归创建者)/no_uploader(作品没有投稿人)。作品不存在、看不到、没发布
10004;amount不在范围里(amount: invalid)、message太长(message: too_long)10001。每人默认每分钟最多 20 次(失败的也算),超了
10006。
GET /me/income:你投的作品各被解锁了几次、一共分到多少,最近有解锁的在前,游标分页。别人解锁你的作品时,你按同样的分成比例拿实付的积分。每项:work、unlocks(完成的解锁次数,会员免费、限时免费的也算,退了款的不算)、income(实际分到的积分,退了款的不算)、last_at(最近一次解锁的时间,后来退了款的那次也算)。下架的作品照样列出。
r = call("POST", "/works/123/tips", {"amount": 50, "message": "谢谢分享"}, idempotent=True) # 自动重试沿用同一个键
print("投稿人拿到", r["tip"]["given"], "余额", r["balance"])9. 求档(用途「求档」)
求档是挂积分求一部站里还没有的作品。规则在 GET /meta 的 bounties:enabled(关着时不能发起、加码,开着的求档照常应征、采纳、到期退回)、min ~ max(一次挂多少积分)、days(默认几天到期)、max_days(最长几天)、review_days(应征以后几天不处理就自动采纳)。
发起人用
POST /bounties发起,挂的积分当时就扣(积分变动bounty)。别人可以用POST /bounties/{id}/stakes加码,同样当时扣。有人把这部作品投稿、发布以后,用
POST /bounties/{id}/answers拿这部作品应征。发起人采纳一个应征:应征的人按分成比例(默认 70%,向下取整)拿走全部悬赏(
bounty_income),其余作废,别的待处理的应征自动拒掉。发起人也可以拒掉应征。应征以后review_days天发起人还没处理的自动采纳(几个都到了时采纳最早的)。到了
expires_at还没采纳、也没有待处理的应征时,积分原样退回给每个挂了积分的人(bounty_refund);到期时还有待处理的应征就等它们处理完(都拒了马上退回)。发起人取消时也原样退回。
自动采纳和到期退回在时间到了以后十分钟内处理。求档的 status:open 开着 / fulfilled 采纳了 / expired 到期退回了 / cancelled 取消了(发起人或站务取消,积分已退回)。应征的 status:pending 待处理 / accepted 采纳了 / rejected 拒了;撤回的应征直接删掉。
9.1 发起、加码、取消
| 接口 | 说明 |
|---|---|
POST /bounties | 发起,必带 Idempotency-Key。请求体:content_type(必填,GET /meta 的 content_types[].code)、rating(必填,ratings[].code)、title(必填,1 ~ 100 字)、description(可选,最多 2000 字,写原名、出版社、链接等)、amount(必填,整数,min ~ max)、days(可选,几天后到期,1 ~ max_days,默认 days) |
POST /bounties/{id}/stakes | 加码,必带 Idempotency-Key。请求体 { "amount": 50 },范围同上;只有 open 的能加,发起人也能加 |
POST /bounties/{id}/cancel | 发起人取消,不用请求体。只有没人加码、没有待处理的应征时能取消,积分全部退回 |
三个接口都返回求档对象,和 GET /bounties 列表里的一项相同(不带 stakes、answers),my_stake 是你挂的积分。
76017发起、加码不了,data.reason:off(站点关了求档)/too_many(你开着的求档到了上限,只限发起)。75005被禁言时不能发起(加码、应征不受影响)。75006标题、说明含站点不允许的词,data.hits是[{ "word", "count" }](同评论的74001)。10005求档不是open的不能加码、取消;有人加码或有待处理的应征时不能取消。10003不是发起人不能取消。10001的字段原因:content_type、rating不认识invalid;title空的required、太长too_long;description太长too_long;amount、days不在范围里invalid。发起和加码每人默认每小时 20 次(失败的也算),超了
10006。
9.2 应征、采纳、拒
应征对象:id、account(应征的人)、work(应征的作品,看不到了为 null)、status、given(采纳时应征的人拿到多少,没采纳为 null)、reject_reason(拒的理由:发起人写的;自动拒的是 another_accepted 别的应征被采纳 / work_hidden 作品下架或删除 / cancelled 站务取消了求档)、auto_accept_at(待处理的到这个时间自动采纳,其他为 null)、created_at、decided_at。一个求档收到的应征列在 GET /bounties/{id} 的 answers 里(撤回的不列),发起人从这里拿 answer_id。
| 接口 | 说明 |
|---|---|
POST /bounties/{id}/answers | 应征:{ "work_id": 456 }(必填),返回应征对象。作品要已经发布、是你投的、类型和求档相同、在求档发起以后才第一次发布 |
DELETE /bounties/{id}/answers/{answer_id} | 应征的人撤回待处理的应征,返回 { "deleted": true };撤回以后同一部作品可以再应征 |
POST /bounties/{id}/answers/{answer_id}/accept | 发起人采纳,不用请求体,返回求档对象(fulfilled,accepted 是这个应征) |
POST /bounties/{id}/answers/{answer_id}/reject | 发起人拒:{ "reason": "不是要找的版本" }(必填,1 ~ 200 字),返回应征对象 |
应征不了
76018,data.reason:own(你在这个求档上挂了积分)/not_yours(作品不是你投的)/content_type(类型不对)/before(作品在求档发起以前就发布了)/answered(这部作品应征过这个求档,或者你还有一个待处理的应征)。作品不存在、看不到、没发布10004。求档不是
open、应征不是pending的10005;不是发起人不能采纳、拒,不是应征的人不能撤回10003;reason空的、太长10001。应征每人默认每小时 30 次(失败的也算)。
应征的作品下架或删除后,待处理的应征自动拒掉;已经采纳的不收回。
9.3 我的求档
GET /me/bounties 是页码分页:page(从 1 开始)、page_size(默认 20,最大 100),返回 items(求档对象)和 pagination(page、page_size、total、total_capped;total 最多算到 10000,超过时 total_capped 为 true)。参数 role:posted 我发起的(默认)/ staked 我挂了积分的(含我发起的)/ answered 我应征过的;status:open、fulfilled、expired、cancelled,可以用逗号分隔几个,all 是全部(默认 all)。
10. 分发节点(用途「分发节点」)
你可以在自己的机器上运行站点的节点程序 xm-edge,替站点分发文件,按每天的工作量拿积分。
申请条件:站点开放了这项功能、验证过邮箱(默认要求)、注册满一定天数(默认 7 天)、你的节点(含待审批的)不到 max_nodes 台。GET /me/edge 的 eligible 是现在能不能申请,不能时 reasons 列出原因:off(没开放)/ email(没验证邮箱)/ too_new(注册天数不够)/ too_many(节点到了上限)。
用
GET /me/edge确认eligible为true,阅读条款:条款是站点页面,terms_page是它的slug(GET /pages/{slug})。同意条款以后调
POST /me/edge/enrollments要申请码。安装:在 Linux(systemd)机器上用 root 运行返回的
command(一条现成的命令,带着申请码)。安装脚本会一项项问对外端口、对外地址(自动检测)、代理、缓存、限速、流量、测速、自动更新,再登记、建服务、启动;装好以后用sudo xm-edge-ctl status(状态)、update(更新)、config(改配置)、logs(日志)、auto-update on|off、uninstall管理,默认每天自动更新。不用脚本的照install手动装:按平台下载节点程序并核对sha256,把config存成edge.yaml,改掉注释里标着「要改」的几项(对外端口、缓存目录、对外地址等),然后运行xm-edge enroll -config edge.yaml -code <申请码> -agree <consent_version>登记。登记的节点出现在
GET /me/edge的nodes里,status是pending,等站点审核;批准以后变成active,开始分发。审核结果会发站内通知。不用了的节点:先在那台机器上停掉节点程序,离线以后用
DELETE /me/edge/nodes/{uid}删掉(见 10.1),不再占名额。
10.1 我的节点和申请码
GET /me/edge 返回:
enabled(站点开没开这项功能)、eligible、reasons、terms_page、max_nodes。reward:奖励规则per_hour、per_gb、daily_cap(见 10.2)。rewards:yesterday、last_30_days,昨天和最近 30 天拿到的分。install:安装信息,没有为null:main_url(节点连接的地址,已经填进配置模板)、downloads[](各平台的节点程序:os、arch、version、size、sha256、url)、config(配置模板,YAML 文本)、script_url(安装脚本的地址)、script_command(不带申请码的安装命令,装好以后重装、看管理用法时用;后两项没有为null)。nodes:你的节点,按登记先后;暂时取不到时是空数组,另带nodes_unavailable: true。每台节点:uid节点号;status:pending待审批 /active正常 /paused暂停 /disabled停用;online、last_seen_at、online_since(这一段连续在线从什么时候开始)。assignable:此刻能不能被派活,为false时原因在health_reasons;health_score(0 ~ 100,低了不派活)、quality(质量分 0 ~ 10000)。health_reasons:扣分、少派或不派活的原因码,例如offline掉线、cert证书没用上或过期、probe_down抽查不合格停派、too_slow、download_slow测速的上传、下载不够、limit_low自己设的限速低于要求、outdated节点程序太旧、month_full这一期流量用到了自己设的上限的 95%、slow_serving/serving_errors实际分发时慢的、出错的太多(只扣分);不认识的码请原样显示。continents:这台服务的大洲(AS、EU、NA、SA、OC、AF、AN),按最近一次成功测速定位的国家算,不用、也不能自己设;空数组是不分地区。speedtest_pending:请了重新测速、还没出结果(见下面的POST /me/edge/nodes/{uid}/speedtest)。另有
trust、speedtest(最近一次测速;server_id、server_sponsor、server_city、server_country是测的那个测速点)、cdn_cap_mbps、cdn_cap_source、limits(节点自己设的限额和这一期的用量)、probe(抽查)、proxy、version、base_url、created_at、consented_at。
POST /me/edge/enrollments:请求体 { "agree": true }(同意条款,必须是 true,否则 10001)。返回 code(一次性的申请码,xee_ 开头,expires_at 以前有效,只能用一次)、expires_at、consent_version(现在的条款版本,登记时用)、install(同上)、command(带着申请码和条款版本的安装命令,复制到机器上运行;没有为 null)。每人默认每小时 5 次(失败的也算)。
不能申请
76019,data.reason同reasons(几个都不满足时给第一个)。分发节点暂未开放
76020(data.reason是closed),暂时连不上也是76020(unavailable)。
DELETE /me/edge/nodes/{uid}:删掉自己的一台节点,返回 { "deleted": true }。离线的(online 为 false)、待审批的能删;在线、已经批准的返回 76021:先在那台机器上停掉节点程序(sudo xm-edge-ctl uninstall,或者 sudo systemctl stop xm-edge),约 2 分钟后离线了再删。删掉以后节点的密钥作废、不再派活、不再占 max_nodes 的名额;以前的工作量照常结算。不是你的、没有这台 10004。
POST /me/edge/nodes/{uid}/speedtest:请自己的一台节点重新测速,返回这台节点(speedtest_pending 为 true)。请求体可以不带(节点自己挑测速点),或者 { "server_id": "56935" } 只测那一个测速点(speedtest.net 的测速点 ID,1 ~ 10 位数字,不对 10001)。30 秒内开始测,一两分钟后出结果:隔一会儿再取 GET /me/edge,speedtest_pending 变回 false、speedtest.at 换成新的时间就是测完了。现在测不了返回 76022,data.reason:recent(10 分钟内刚测过)/ outdated(节点程序太旧,请更新)/ server_unsupported(指定测速点要更新节点程序,不指定照样能测)/ disabled(节点停用了)。不是你的、没有这台 10004。每人默认每小时 10 次(失败的也算)。
me = call("GET", "/me/edge")
if me["eligible"]:
e = call("POST", "/me/edge/enrollments", {"agree": True})
print(e["code"], e["expires_at"], e["consent_version"])
print("在你的机器上运行:", e["command"])
else:
print("现在不能申请:", me["reasons"])10.2 奖励和工作量
每台节点每天的奖励 = 向下取整(
healthy_seconds÷ 3600 ×per_hour+bytes÷ 10⁹ ×per_gb),healthy_seconds是能派活的秒数,bytes是计入奖励的分发字节数。一个人一天几台加起来最多
daily_cap分(0 是不限),超了少给,那天的记录capped为true。一天过完以后才结算(最多晚一天半),每台节点每天只结算一次,记进积分变动(
edge,title是节点号);不受其他奖励的每日上限限制。
GET /me/edge/rewards:结算过的奖励,新的在前,页码分页(同 GET /me/bounties)。每项:day、node(节点号)、healthy_seconds、bytes、points、capped。
GET /me/edge/usage:最近几天每台节点每天的工作量,包括还没结算的(今天的还在涨)。参数 days:1 ~ 30,默认 7,含今天。返回 items,不分页,新的天在前、同一天按节点号。每项:day、node、online_seconds(在线秒数)、healthy_seconds、bytes、points 和 capped(还没结算时 points 为 null)。暂时连不上 76020(unavailable)。
11. 资料和设置(用途「资料和设置」)
11.1 当前账号
GET /me 返回当前账号。任何用途的令牌都能调,可以用来确认令牌有效、属于哪个账号。
| 字段 | 说明 |
|---|---|
id、username、name | 用户 id;用户名(注册时定,不能改);显示名(没设过时同用户名) |
avatar | 头像地址(https),没有为 null |
email、email_verified | 邮箱、是否验证过;没填过为 null、false |
has_password | 设过密码没有 |
forum_linked | 是不是用论坛账号快捷注册的(两边资料不同步) |
roles | 角色:user(人人都有)、uploader 投稿者、trusted_uploader 可信投稿者(免审)、vip 会员;reviewer、staff、owner 是站务的角色 |
is_staff | 有没有站务的角色 |
permissions | 权限点,展开后的完整列表,判断是否包含即可。和普通用户有关的是 work.upload(投稿)和 work.upload.skip_review(投稿免审,提交后直接发布)。用令牌调用接口时,只有「投稿」用途保留这两个权限点(账号本身要有),别的用途不带权限点;站务账号建的令牌也不按站务算 |
status | active 正常;banned 被封禁(封禁时令牌会全部吊销,所以用令牌调用一般见不到) |
restrictions | 生效中的处罚:[{ "kind", "until", "reason" }]。kind 为 mute 禁言(发评论、弹幕等返回 75005)或 upload_ban 禁止投稿;until 为 null 是永久,到期自动失效;种类的名字见 GET /meta 的 restriction_kinds |
can_upload、upload_block_reason | 现在能不能投稿;不能时的原因:not_eligible(没有投稿权限)、upload_banned(被禁止投稿)、uploads_disabled(站点暂停投稿),能投稿时为 null。不能投稿时建投稿返回 72001,data.reason 同这里 |
age_confirmed | 确认过年龄没有 |
settings | 个人设置,全部项都有值(没设过的是默认值),见下一小节 |
session_expires_at | 用令牌调用时是令牌的到期时间 |
11.2 个人设置
PATCH /me/settings 改个人设置,返回改后的完整 settings。请求体是 JSON Merge Patch(RFC 7396),Content-Type 用 application/merge-patch+json 或 application/json 都可以:
只改带了的字段,嵌套的对象可以只带要改的那一项;值为
null表示恢复默认。数组整个替换:要往
content_filter.hidden_tags这类列表里加一项,先从GET /me取出原来的数组,加上以后整个传回去。不认识的字段(哪一层都算,
ui里面的除外)报unknown,类型不对、取值不合法报invalid,都是10001;data.errors的键是 JSON 路径,如player.audio.loop、title_preference[1]。出错时整个请求一项都不改。
| 字段 | 取值 | 默认 |
|---|---|---|
language | 界面语言:zh-CN / en-US,通知等文字也按它 | 注册时的语言(不是英文的都按 zh-CN) |
title_preference | 标题优先显示哪种,按顺序取第一个有的:语言代码(zh、en、ja 等)或 original 原名 | 中文是 ["zh", "original", "en"],英文是 ["en", "ja-Latn", "original"] |
theme | light / dark / system(跟随系统);没设过为 null,各端用自己的默认 | null(网站上按深色显示) |
list_mode | 列表显示:thumb / list / compact | thumb |
content_filter | 默认过滤,各数组最多 500 项:hidden_ratings 分级代码、hidden_tags 标签 id、hidden_languages 语言代码、blocked_uploaders 用户 id;shown_ratings、shown_tags 是站点默认隐藏的分级、默认屏蔽的标签(GET /meta 的 ratings[].hidden_by_default、default_hidden_tags)里解除了的 | 都是 [] |
reader.manga | 漫画阅读器。mode:single 单页 / double 双页 / scroll 下拉;direction:rtl 从右往左 / ltr 从左往右;fit:width / height / original;preload:0 ~ 10;quality 画质:auto 按屏幕自动 / data_saver 省流 / hd 高清 / original 原图 | scroll、ltr、width、3、auto |
reader.novel | 小说阅读器。font:serif 宋体 / sans 黑体 / kai 楷体;font_size:12 ~ 32(像素);line_height:1.2 ~ 3.0;paragraph_spacing:0 ~ 2(行高的倍数);width:480 ~ 1200(像素);indent 首行缩进;theme:auto(跟随网站主题)/ light 白 / sepia 米黄 / green 护眼绿 / dark 夜间;paging:scroll 上下滚动 / page 左右翻页;vertical 竖排;script:original / hans 简体 / hant 繁体;auto_next 读到章末接下一章 | serif、18、1.8、0.6、720、true、auto、scroll、false、original、true |
player | 视频播放器。autoplay_next 自动播下一集;speed:0.25 ~ 4;subtitle_language(见表后);auto_skip 自动跳过片头、片尾、前情回顾 | true、1.0、"auto"、false |
player.audio | 音声播放器。loop:off / one 单曲 / all 列表;speed:0.25 ~ 4;quality 音质:standard 标准 / lossless 能在线听无损原档的作品(解锁过的、你自己投的)默认放无损 | off、1.0、standard |
history.paused | true 时暂停记录历史(进度、「看过」都不保存) | false |
notifications | 各种站内通知开不开:键是通知种类(GET /meta 的 notification_kinds),值是 true / false,关了的不再生成;不认识的种类报 notifications.<种类>: unknown | 全部 true |
ui | 网站界面的偏好(如弹幕显示设置 ui.danmaku),原样保存,不超过 8 KB | {} |
subtitle_language:"auto" 跟随作品(开作品标明的默认字幕);"" 不开字幕;其他值是偏好的语言(BCP 47,如 zh-Hans、ja),作品有这种语言的字幕就开它,没有再开默认字幕。PATCH 里的 null 是恢复成 "auto",不是关字幕。
call("PATCH", "/me/settings", {"theme": "light", "player": {"speed": 1.5}, "notifications": {"work_update": False}})11.3 显示名、头像、年龄确认
| 接口 | 说明 |
|---|---|
PATCH /me/profile | 改显示名:{ "name": "新名字" },1 ~ 20 个字符,不能有控制字符;null 或空串恢复成用户名。返回改后的账号(同 GET /me)。新名字含站点不允许的词返回 75006,data 是 { "hits": [{ "word", "count" }], "field": "name" } |
PUT /me/avatar | 换头像(见下),返回改后的账号,新地址在 avatar |
DELETE /me/avatar | 去掉头像,返回改后的账号(avatar 为 null) |
POST /me/age-confirmation | 确认年龄(网站打开 R18 内容前要求确认),不用请求体 → { "age_confirmed": true };记在账号上,换设备也有效 |
换头像不用先另外上传图片:直接用 multipart/form-data 把图片放在字段 file 里发给 PUT /me/avatar。支持 JPEG、PNG、WebP、GIF,默认不超过 2 MB(以站点设置为准);图片会被裁成正方形,旧头像随之删除。
img = open("avatar.png", "rb").read()
me = call("PUT", "/me/avatar", files={"file": ("avatar.png", img, "image/png")})
print(me["avatar"])12. 只能在网站上做的事、其他公开接口
12.1 只能在网站上做的
下面这些和账号安全有关,只认网站上登录的会话:令牌调用它们返回 10003(data.reason 是 session_only),令牌也不会因此获得权限。请在网站上操作。
注册、登录、退出、找回密码、验证邮箱(
/auth/…下的接口)。修改密码
POST /me/password、修改邮箱POST /me/email、重发验证邮件POST /me/email/resend。管理接口令牌:
GET /me/api-tokens、POST /me/api-tokens、DELETE /me/api-tokens/{id}。订阅链接的凭证:
GET /me/feed-token、POST /me/feed-token、DELETE /me/feed-token(拿到订阅链接以后怎么用,见第 5.3 节)。
12.2 版权投诉
POST /copyright-claims:权利人或代理人投诉站内作品侵权。不需要令牌(带了也照常受理,并记下是谁提交的)。
| 字段 | 说明 |
|---|---|
name | 权利人或代理人的名字,1 ~ 100 字,必填 |
email | 联系邮箱,必填;站务用它联系你 |
phone、organization | 可选:电话(最多 50 字)、单位(最多 100 字) |
claimant_type | 可选:owner 权利人 / agent 代理人 |
works | 侵权的作品:作品页的链接(如 /manga/12)或作品 id,1 ~ 50 项 |
description | 说明,1 ~ 5000 字,必填 |
proof | 权利证明(文字或链接),1 ~ 5000 字,必填 |
agree | 必须是 true:确认你是权利人或代理人、内容属实 |
返回 { "id": 5, "created_at": "…" },id 是受理编号。处理结果由站务通过邮件或电话联系,接口不提供查询。按 IP 限制频率(默认每小时 5 次,失败的也计数)。
12.3 App 用的分流结果
GET /app/dispatch?platform=android:不需要令牌。这是给本站 App 用的:返回一份签过名的分流结果,里面有接口的几个可用地址(按你所在的国家排好)、网站地址和 App 的最新版本,App 照它挑地址连接。脚本一般用不到;要用的话,payload 是一段 JSON 的 base64url 编码(不带 =),解开以后:
| 字段 | 说明 |
|---|---|
lines | 接口的根地址(到 /api/v1 为止),好的在前;每项有 url、name、priority |
web_url | 网站的地址 |
release | 这个平台的 App 的版本:latest 最新版本、min 最低版本(更低的必须更新)、url 下载地址、notes 更新说明(Markdown)、size_bytes 安装包大小、released_at 发布时间、packages 按处理器架构另给的安装包([{ "abi", "url", "size_bytes" }],例如 armeabi-v7a 是 32 位的;设备的架构在里面就下那一个,不在就下 url);没有这个平台的为 null |
revision、issued_at、expires_at | 版本号、签发时间、过期时间 |
站点还没配好时返回 78001。按 IP 限制频率(默认每分钟 60 次)。
12.4 App 的各版本
GET /app/releases:不需要令牌,给本站 App 的更新日志和网站的 App 下载页用。
?platform=android 只要一个平台,不带给全部 → { "icon", "items": [ { "platform", "latest", "min", "versions": [ { "version", "url", "size_bytes", "notes", "released_at", "required", "packages" } ] } ], "screenshots": [ { "image", "device", "caption" } ] }。icon 是 App 的图标(图片,不分平台;没设为 null)。versions 新的在前,notes 是 Markdown 原文(按请求语言),required 为 true 的版本以下必须更新,packages 同上一节(没有为空数组)。screenshots 是 App 的截图(device 是 phone / tablet / tv,caption 是按请求语言的一句说明、没有为 null)。platform 写法不对返回 10001。