跳到正文

接口文档

更新于 2026年10月31日

目录99 节

用接口令牌,你可以在自己的程序里做网站上能做的大部分事:浏览和搜索作品、整理书架、发评论、看通知、签到、投稿……本页是全部接口的说明:第 1、2 节是准备工作和一个 Python 客户端,第 3 节起按令牌的用途分节。投稿的完整流程(传文件、排结构、提交审核、更新作品)另见「投稿接口」。

1. 准备

1.1 建一个接口令牌

在网站上登录,打开「接口令牌」页(个人中心里),点「新建令牌」:

  1. 起个名字(比如「同步书架」),勾选要用的用途(见下表,可以多选),选到期时间(最长多久页面上写着,默认 90 天;到期以后再建一个)。

  2. 输入当前密码确认。还没设过密码的(用论坛账号快捷注册的)先在账号设置里加一个密码;邮箱要先验证过。

  3. 页面上显示的 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 个),不用的吊销掉。

试一下:

bash
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:

json
{ "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(第一页不带),响应是:

json
{ "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 图片、视频、音频的地址

接口里每个文件都是同一个形状:

json
{ "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
"""站点接口的最小客户端: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,自动重试时沿用同一个键(解锁、打赏这类必须带它的接口用)。用法:

python
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 时用内容类型和分类代码(名字随语言变):

python
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_statusongoing 连载中 / 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(系列里的顺序)。近期热度算的是一段时间里的浏览、新收藏、评论、新评分和新点赞
filterdefault(默认):套用你在个人设置里的内容过滤(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" }
statsviews 浏览数、favorites 收藏人数、score 平均分(1 到 10,评分人数不够时为 null)和 score_count 评分人数、comments 评论数、likes 点赞数(各集、小说各章的点赞加起来)、plays 和 completions(动画、音声全部集数加起来的播放、看完听完次数,含 PV、花絮,换掉、删掉的文件也算;别的类型为 null)
uploader投稿人(作品的创建者){ "id", "name", "avatar", "deleted" };站务导入的为 null
serial_statusongoing / completed;不适用为 null
word_count、duration小说的字数;动画、音声的总时长(秒,文件处理好以后最多晚 10 秒算上);别的类型为 null
latest最新公开的一话或一章:unit_id、item_id、title、published_at;还没有为 null
published_at、updated_at发布时间;内容最后更新的时间
previewtrue 是还没公开(只有创建者、共创者和站务看得到)
python
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 这次不套用)。

python
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。

json
{ "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 星期一在前,weekday 1 到 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 不用)。

和这部作品相似的已发布作品,最像的在前 → { "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。

python
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:眼 时只在这个命名空间里找
scopeall(默认)/ tags / creators / works,可以多个。作品的候选由站内搜索给;搜索暂时不可用时按标题、别名、作品号的开头找,q 至少 2 个字符
namespace只找这个命名空间的标签
role只找有这个角色的创作者
limit默认 10、最大 20(不分页)
json
{ "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 跳过。

一段时间里(默认 7 天)搜过同一个词的人数,多的在前;同一个人同一个词只算一次,人数太少的不列。所有人看到的一样,几分钟更新一次。

参数说明
limit给几条:默认、最多都是站点设置的条数(默认 10)
json
{ "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 是空的。用接口令牌搜的不算进热搜。

python
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。

python
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只排这种内容类型;不带是全部类型混着排
limit1 到 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,重新要地址时用(图片也有)
statusprocessing 处理中(还没有播放地址)/ 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 并且目录没变时回 HTTP 304,没有响应体。

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。

python
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 次(翻页也算,以站点设置为准)。

python
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)
deletedtrue:删了、但下面还有回复的一级评论留的位置(body 是空串、account 为 null)
viewer你的状态:liked、can_edit、can_delete;按访客算时都是 false

GET /works/{id}/comments?unit_id=&item_id=&paragraph=&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(是不是你发的)。

python
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 += 1

3.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 是主人写的备注。只列你看得到的作品,也套用你的内容过滤。

python
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标题、说明(原文)
statusopen 开着 / 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 节。

python
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}/episodesBangumi 条目的各集(本篇、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:日期
kindnew_work 新作品 / new_units 关注的作品有更新
work作品列表项
units、chaptersnew_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,可以缓存五分钟。

bash
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 自己翻。

python
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 只对视频和音频有用,别的条目、单元开头带了不报错也不记。

python
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
spoilertrue 表示有剧透(网站上先折叠),默认 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 要是这一章公开的正文里有字的段落(分隔线、空行、插图、超出范围的返回 10001 anchor: 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。

python
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 里(令牌要有「浏览」用途)。

python
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: requiredtag_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。

python
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
modescroll 滚动(默认)/ 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。站务删掉一个视频或删除作品时,上面的弹幕一起删掉。

python
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" }。

python
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
python
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 通知的形状和种类

json
{ "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 是节点号
python
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。

python
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下载服务暂时不可用,或者小说的文件刚生成失败,过几分钟再试
python
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(最近一次解锁的时间,后来退了款的那次也算)。下架的作品照样列出。

python
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(应征以后几天不处理就自动采纳)。

  1. 发起人用 POST /bounties 发起,挂的积分当时就扣(积分变动 bounty)。别人可以用 POST /bounties/{id}/stakes 加码,同样当时扣。

  2. 有人把这部作品投稿、发布以后,用 POST /bounties/{id}/answers 拿这部作品应征。

  3. 发起人采纳一个应征:应征的人按分成比例(默认 70%,向下取整)拿走全部悬赏(bounty_income),其余作废,别的待处理的应征自动拒掉。发起人也可以拒掉应征。应征以后 review_days 天发起人还没处理的自动采纳(几个都到了时采纳最早的)。

  4. 到了 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(节点到了上限)。

  1. 用 GET /me/edge 确认 eligible 为 true,阅读条款:条款是站点页面,terms_page 是它的 slug(GET /pages/{slug})。

  2. 同意条款以后调 POST /me/edge/enrollments 要申请码。

  3. 安装:在 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> 登记。

  4. 登记的节点出现在 GET /me/edge 的 nodes 里,status 是 pending,等站点审核;批准以后变成 active,开始分发。审核结果会发站内通知。

  5. 不用了的节点:先在那台机器上停掉节点程序,离线以后用 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 次(失败的也算)。

python
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(投稿免审,提交后直接发布)。用令牌调用接口时,只有「投稿」用途保留这两个权限点(账号本身要有),别的用途不带权限点;站务账号建的令牌也不按站务算
statusactive 正常;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"]
themelight / dark / system(跟随系统);没设过为 null,各端用自己的默认null(网站上按深色显示)
list_mode列表显示:thumb / list / compactthumb
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.pausedtrue 时暂停记录历史(进度、「看过」都不保存)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",不是关字幕。

python
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(以站点设置为准);图片会被裁成正方形,旧头像随之删除。

python
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。