Eight tools (search, video, comments, danmaku, rank, playurl, interact, post_comment) over acfun.cn's public web API, a model-facing skill, a bundle patch with a skill-filesystem row, and offline/live checks. Reads are anonymous; writes require allowWriteActions plus a login cookie.
38 KiB
AcFun (acfun.cn / "A站") Web API — reverse-engineered for a Node.js fetch plugin
All probes were executed live on 2026-10-27 (Asia/Shanghai) against https://www.acfun.cn
using desktop Chrome UA and Referer: https://www.acfun.cn/. Every endpoint below is marked
[VERIFIED] (called live, real status/JSON pasted) or [UNVERIFIED] (contract only — usually
because it needs a logged-in session I do not have; writes were never executed).
0. Conventions, host map, and shared plumbing
0.1 Base hosts
| Purpose | Host |
|---|---|
| PC web JSON API | https://www.acfun.cn (paths under /rest/pc-direct/...) |
| Login / token service | https://id.app.acfun.cn (/rest/web/token/get) |
| Like ("顶") service | https://kuaishouzt.com (/rest/zt/interact/add), also api.kuaishouzt.com |
| App API (legacy, mostly dead) | https://api-ipv6.acfunchina.com (/rest/app/...) |
| Video CDN (m3u8/ts) | https://tx-video.acfun.cn, https://tx-safety-video.acfun.cn, https://ali-safety-video.acfun.cn |
0.2 Two different "video ids" — this is the #1 gotcha
The PC web uses two numeric ids per video:
| Name in payload | Meaning | Example | Used by |
|---|---|---|---|
dougaId / contentId / id |
The "ac number" — what appears in ac12345678 and in /v/acNNNN URLs |
42588725 |
page URL, comments (sourceId), favorites (resourceId), banana (resourceId), interact (objectId) |
videoId / currentVideoId / currentVideoInfo.id |
Per-part id (a video can have several parts) | 32224197 |
playInfo (videoId), danmaku (resourceId) |
[VERIFIED] /v/ac42588725 → HTTP 200, full SSR payload.
[VERIFIED] /v/ac32224197 (using the part id in the URL) → HTTP 404 (the app shell).
So a plugin's getVideo(acId) must treat the number the user typed as dougaId; the videoId
needed by playInfo/danmaku comes out of the detail payload (currentVideoId, videoList[].id).
0.3 Required request headers
[VERIFIED] JSON reads work without Referer and even with curl's default UA:
no-referer search HTTP 200
no-UA search HTTP 200 -> {"result":0,"totalNum":547,...}
Recommended (matches the browser, avoids surprises):
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Referer: https://www.acfun.cn/
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8for every POST listed here (the site's own client sends form-encoded, not JSON, for/rest/pc-direct/*).- No
sign/__/md5parameter is needed on any read endpoint verified here. The site's own comment widget (@ac/pc-comment.*.js) posts plain form data withsubBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APPand no signature. (Auth token is a separate thing, §7.)
0.4 Common response envelope
{"result": 0, "host-name": "hb2-acfun-kce-node20.aliyun"}
Observed result codes:
result |
HTTP | Meaning |
|---|---|---|
0 |
200 | success |
1 |
200 | generic "ok" for some interact endpoints (/rest/zt/interact/add) |
11 |
200 | "网络错误,请稍后再试" / "请检查下网络连接是否正常" — handler ran but request context/params rejected (e.g. POST to a route that wants different params) |
21 |
200 | "参数格式错误,请仔细阅读API文档" — missing/malformed parameter |
-401 |
401 | "user not login" — route exists, cookie session missing/invalid |
-401 |
401 | "token value error" — route exists, requires the acfun.midground.api_st token (§7) |
105001 |
200 | app API (api-ipv6.acfunchina.com) — auth token invalid/missing |
0.5 Auth cookies (names only — never logged in here)
[VERIFIED] an anonymous visit to https://www.acfun.cn/ sets only:
.acfun.cn _did web_83374403393B0029
www.acfun.cn csrfToken Pkyh8iuCGfRggSGwYox04LCz
[UNVERIFIED] logged-in session (from community docs for id.app.acfun.cn/rest/app/login/signin
plus the PC web's own getCookie("auth_key") usage):
acPasstoken <- login response field acPassToken (the main session cookie)
auth_key <- login response field auth_key (numeric userId)
_userid <- numeric userId
acPlatform = 2 (PC web)
_did, csrfToken (set for anonymous too)
Probes with forged values (acPasstoken=deadbeef; auth_key=123456; _userid=123456; acPlatform=2)
still return {"result":-401,"error_msg":"user not login"} — proving the server validates the
session rather than merely checking cookie presence. Minimum cookie set remains UNVERIFIED.
1. Video detail — [VERIFIED] via SSR page (there is no working pc-direct JSON endpoint)
1.1 Endpoint
GET https://www.acfun.cn/v/ac{dougaId} # dougaId == contentId == the "ac" number
- HTTP 200,
text/html, ~139 KB. - The page embeds the entire structured video object as a JS assignment:
window.pageInfo = window.videoInfo = { ... }; // then: window.videoResource = {}; ...
Parse with a regex from window.pageInfo = window.videoInfo = up to window.videoResource, trim
the trailing ;, then JSON.parse. In PowerShell (what I actually ran):
$c = Get-Content page.html -Raw
$s = $c.IndexOf('window.pageInfo = window.videoInfo = ') + 38
$e = $c.IndexOf('window.videoResource', $s)
$o = ($c.Substring($s, $e-$s).Trim().TrimEnd(';') | ConvertFrom-Json)
Node equivalent for the plugin:
const html = await (await fetch(`https://www.acfun.cn/v/ac${acId}`, {headers})).text();
const start = html.indexOf('window.pageInfo = window.videoInfo = ');
const end = html.indexOf('window.videoResource', start);
const info = JSON.parse(html.slice(start + 38, end).trim().replace(/;\s*$/, ''));
Evidence: HTTP 200, payload length 15,567 chars, 63 top-level keys.
HTTP 200 https://www.acfun.cn/v/ac42588725
LEN 138828 (html) pageInfo JSON LEN 15567
TOP KEYS: currentVideoId, pctr, commentCountRealValue, groupId, bananaCountShow,
stowCountShow, giftPeachCountShow, stowCount, channel, likeCount, hasHotComment,
shareCountShow, isDislike, shareCount, picShareUrl, danmakuCount, currentVideoInfo,
viewCountShow, dougaId, commentCountTenThousandShow, coverImgInfo, commentCount,
coverUrl, disableEdit, createTimeMillis, superUbb, shareUrl, status, isFavorite,
isLike, description, title, belongToSpecifyArubamu, result, videoList, isThrowBanana,
viewCount, bananaCount, likeCountShow, coverCdnUrls, giftPeachCount, isRewardSupportted,
commentCountShow, durationMillis, host-name, tagList, danmakuCountShow, createTime,
user, mkey, priority
1.2 Field mapping (exactly what a plugin needs)
| Plugin field | Path | Sample (ac42588725) |
|---|---|---|
| ac number | dougaId |
42588725 |
| part/video id (for playInfo/danmaku) | currentVideoId |
32224197 |
| title | title |
【原神】三周年超治愈的歌曲《你呀你呀》 |
| description | description |
"" (many videos have empty) |
| author name | user.name |
天阶夜色凉如水de陈秀凤 |
| author id | user.id |
74428749 |
| author avatar | user.headUrl (also user.headCdnUrls[].url) |
…/defaultAvatar.jpg?imageslim |
| author signature | user.signature |
— |
| author fans | user.fanCountValue (number) / user.fanCount (display) |
— |
| is following author | user.isFollowing |
false |
| views | viewCount (viewCountShow display) |
5682 |
| danmaku count | danmakuCount (danmakuCountShow) |
8 |
| comments | commentCount (also commentCountRealValue) |
21 |
| likes/"顶" | likeCount (likeCountShow) |
217 |
| favorites/collect | stowCount (stowCountShow) |
324 |
| bananas/投蕉 | bananaCount (bananaCountShow string "119") |
119 |
| shares | shareCount (shareCountShow) |
34 |
| publish time | createTimeMillis (ms epoch) / createTime ("2023-10-8") |
1696772749620 |
| duration | durationMillis (ms) |
196976 (= 3:16) |
| cover | coverUrl (coverImgInfo.thumbnailImage.cdnUrls[].url variants) |
https://tx-free-imgs.acfun.cn/newUpload/…png?imageslim |
| channel | channel.id, channel.name, channel.parentId, channel.parentName |
106 / 动画综合 / 1 / 动画 |
| tags | tagList[] → {id, name} |
[{id:"609267",name:"原神空"},{id:"371390",name:"#原神"},{id:"9546",name:"原神"}] |
| parts | videoList[] → {id, title, durationMillis} |
[{id:"32224197",title:"noTitle",durationMillis:196976}] |
| playback (embedded!) | currentVideoInfo.ksPlayJson / ksPlayJsonHevc |
see §4 |
| viewer state | isLike, isFavorite, isThrowBanana, isFollowing |
all false anonymously |
| video status | status |
2 |
1.3 Endpoint status (dead paths)
[VERIFIED 404] — all return the SPA 404 HTML (~102.5 KB <!DOCTYPE html> shell):
/rest/pc-direct/video/index?videoId=32224197
/rest/pc-direct/video/detail?videoId=32224197
/rest/pc-direct/video/getVideoInfo?videoId=32224197
/rest/pc-direct/video/info?videoId=32224197
Caveat: the SSR payload is the only complete detail source. The search API (§2) can supply a partial substitute (title/author/avatar/counts/cover/duration/channelId/ctime) but has no description and no tag names, and it only works if the video is searchable.
2. Video search — [VERIFIED]
2.1 Endpoint
GET https://www.acfun.cn/rest/pc-direct/search/video?keyword={urlencoded}&page=1&pageSize=20[&sortType=N]
Also accepts POST with the same form fields (identical JSON result). No referer/auth needed.
Evidence (baseline, the example that already worked):
HTTP 200 len 23026
{"result":0,"totalNum":600,"videoList":[{...}]}
2.2 Parameters — and the pagination trap
| Param | Status | Notes |
|---|---|---|
keyword |
[VERIFIED] required, URL-encoded UTF-8 | keyword=原神 → 600 hits |
page |
[VERIFIED] ignored | page=1/2/3 returns the identical 30 rows (first id always 32224197) |
pageSize |
[VERIFIED] ignored | pageSize=5/10/20/50 all return exactly 30 rows |
pageNum, pageNo, pn, offset, cursor, pcursor, currentPage |
[VERIFIED] ignored | all identical results (tried each) |
sortType |
[VERIFIED] honoured | 0/1/omitted = relevance (default); 2 = most-viewed (ordering by viewCount desc: 29725, 29452, 8253, 7443, 5682); 3/4 behaved like 2 |
Evidence for ignored pagination:
pageSize=5 -> HTTP 200 count=30 total=600
pageSize=20 -> HTTP 200 count=30 total=600
page=2 -> HTTP 200 count=30 first=32224197 (same as page 1)
GET 龙 page1 sort2 -> 200 total=600 count=30 first=17777112
GET 龙 page2 sort2 -> 200 total=600 count=30 first=17777112
POST (page=2) -> 200 len=22040 (same shape; page still not applied)
The response echoes bogus pagination metadata (pageNum=20, pageSize=30 regardless of input),
which is a server-side bug. Conclusion: only the first 30 results are obtainable. Deep search
paging must go through the SSR search page (/search?keyword=...&page=N, server-rendered) instead.
totalNum sanity check: obscure keyword qzxwvn → totalNum=0, videoList=[]; test → 547;
common terms (原神, 编程, 龙) → 600 (appears to be a display cap at 600).
2.3 Response field map (videoList[])
top-level keys: result, totalNum, videoList, requestId, pageSize, host-name, pageNum
videoList[0] keys: groupId, emTitle, viewCountInfo, danmakuCountInfo, displayInfo,
belongToSpecifyArubamu, channelId, viewCount, commentCount, coverUrl, userImg, videoId,
contentId, danmuCount, playDuration, userName, userId, id, itemType, ctime, title
| Plugin field | Path | Sample |
|---|---|---|
| ac number | contentId (== id) |
42588725 |
| part id (for playInfo/danmaku) | videoId |
32224197 |
| title | title |
【原神】三周年超治愈的歌曲《你呀你呀》 |
title with <em> highlights |
emTitle |
【<em>原神</em>】… |
| author name | userName |
天阶夜色凉如水de陈秀凤 |
| author id | userId |
74428749 |
| author avatar | userImg |
https://tx-free-imgs.acfun.cn/…?imageslim |
| views | viewCount / viewCountInfo |
5682 / "5682次播放" |
| comments | commentCount |
21 |
| danmaku | danmuCount / danmakuCountInfo |
8 |
| duration | playDuration ("mm:ss" string!) |
"03:16" |
| published | ctime (ms epoch) |
1696772749620 |
| channel | channelId (id only — no name!) |
106 |
| cover | coverUrl |
https://tx-free-imgs.acfun.cn/newUpload/…png?imageslim |
| type | itemType |
2 (video) |
| display string | displayInfo |
"5682次播放 2023-10-08" |
| short description | decr (some items only) |
— |
2.4 Sibling search endpoints
[VERIFIED 200] GET /rest/pc-direct/search/suggest?keyword=... — used by the header search box
(found in common/widget/searchBox/index.*.js).
[VERIFIED 200, documented] GET /rest/pc-direct/search/user?keyword=... (returns userList).
3. Comments (评论 / 回复) — [VERIFIED]
3.1 Main comment list (new "JIMU" comment system — works anonymously)
GET https://www.acfun.cn/rest/pc-direct/comment/listByFloor
?sourceId={dougaId}&sourceType=3&page=1&supportZtEmot=true
sourceId= the ac number (dougaId), notvideoId.sourceType= 3 for videos (the embedded comment objects themselves carry"sourceType": 3, which is the authoritative value). Lenient server quirk:sourceType=1also returned the same 21 comments;sourceType=2returned 0. Use 3.supportZtEmot=trueenables Kuaishou emoticon rendering incontent.- Optional
pivotCommentId/newPivotCommentId/t(cache-buster ms) accepted.
Evidence (HTTP 200):
{"result":0,"curPage":1,"isUp":false,"sourceType":3,"totalPage":1,
"pivotCommentId":0,"commentsMap":{...},"contentUbbVersion":"1.0",
"pageSize":50,"totalCount":21,"commentIds":["c806796787", ...]}
totalCount=21 matches commentCount/commentCountRealValue in pageInfo. pageSize is 50,
totalPage 1. commentsMap is an object keyed by "c" + cid ("c806796787"), and
commentIds[] lists those keys in display order (newest first).
Comment object field map (from commentsMap["c…"]):
userHeadImgInfo, avatarFrameImgInfo, imageInfo, cid, userId, isUpDelete, floor, postDate,
isUp, userName, sourceType, verified, sourceId, isDelete, headUrl, nameRed, avatarFrame,
quoteId, nameColor, verifiedTypes, deviceModel, isSignedUpCollege, avatarImage, imageUrl,
content, timestamp
| Plugin field | Path | Sample |
|---|---|---|
| comment id | cid |
806796787 |
| author id / name | userId / userName |
77984979 / 不知何处吊湘君de段倩玉 |
| avatar | headUrl[] → {cdn,url}; also userHeadImgInfo.thumbnailImage.cdnUrls[].url |
https://tx-free-imgs.acfun.cn/… |
content (UBB, may contain [emot=acfun,ID/]) |
content |
[emot=acfun,2789/][emot=acfun,2789/]… |
| floor | floor |
10 |
| posted | timestamp (ms) / postDate ("07-28 21:32" local, no year) |
1785245573037 |
| is uploader | isUp |
false |
| verified badge | verified / verifiedTypes[] |
0 |
| name color | nameColor, nameRed |
0 |
| deleted markers | isDelete, isUpDelete |
false |
| device | deviceModel |
HUAWEI ART-AL00x |
| quote | quoteId |
0 |
| source | sourceId, sourceType |
42588725, 3 |
Note: listByFloor comment objects have no likeCount / subCommentCount fields; reply
counts come from the sublist call (§3.2). Total count comes from totalCount on the list response.
3.2 Reply / sub-comments — [VERIFIED]
GET https://www.acfun.cn/rest/pc-direct/comment/sublist
?sourceId={dougaId}&sourceType=3&rootCommentId={cid}&page=1&supportZtEmot=true
Evidence (HTTP 200):
{"result":0,"subCommentCount":0,"curPage":1,"subComments":[],"sourceType":3,
"totalPage":0,"contentUbbVersion":"1.0","pcursor":"no_more","pageSize":10,
"contentTitle":"【原神】三周年超治愈的歌曲《你呀你呀》",
"rootComment":{...}}
subComments[] items follow the same shape as §3.1 (they additionally carry subCommentCount /
reply-level fields). pageSize=10, pcursor ∈ {number, "no_more"}.
3.3 Dead / auth-gated comment routes
[VERIFIED] POST /rest/pc-direct/comment/list (objectId+pcursor+pageSize,
the "SECONDARY"/legacy widget path) — does not work anonymously:
POST /rest/pc-direct/comment/list (objectId=42588725&pcursor=&pageSize=20&subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP)
-> HTTP 200 {"result":11,"error_msg":"网络错误,请稍后再试"}
GET /rest/pc-direct/comment/list?objectId=…&objectType=…&page=…&pageSize=…
-> HTTP 200 {"result":21,"error_msg":"参数格式错误,请仔细阅读API文档。"}
/comment/subCommentList, /comment/hotList, /comment/replyList → [VERIFIED 404] (SPA shell).
The legacy objectType parameter does not exist in the new widget at all (0 occurrences in
@ac/pc-comment.*.js); classification is by sourceId+sourceType only.
4. Playback (playInfo / m3u8 / quality list) — [VERIFIED]
4.1 playInfo endpoint
GET https://www.acfun.cn/rest/pc-direct/play/playInfo/ksPlayJson?videoId={videoId}
(POST with the same form body also works. videoId is the part id from pageInfo
currentVideoId, not the ac number. Extra resourceId/resourceType params are ignored.)
Evidence (HTTP 200, 7,494 bytes):
{"result":0,"playInfo":{
"streams":[],
"ksPlayJsonHevc":"{\"version\":\"1.0.0\",\"businessType\":1,...}",
"domainInfos":…, "duration":…, "sizeType":…, "firstFrameUrls":…,
"userPlayedSeconds":0, "danmakuGuidePosition":…, "durationMillis":196976,
"ksPlayJson":"{\"version\":\"1.0.0\",\"businessType\":1,\"mediaType\":1,
\"videoId\":\"a7856caac8f5d4cc\",\"hideAuto\":false,\"adaptationSet\":[{
\"id\":0,\"duration\":196976,\"representation\":[
{\"id\":1,\"url\":\"https://tx-video.acfun.cn/mediacloud/acfun/acfun_video/a7856caac8f5d4cc-…-hls_1080p_h264_6m_1.m3u8?sign=6971e530669cf53e31dbf8a424d4acc6&t=1793091872&us=2128358904\",
\"backupUrl\":[\"https://ali-safety-video.acfun.cn/…\"],\"maxBitrate\":6000,\"avgBitrate\":1483,
\"width\":1920,\"heigh\":1080,\"qualityType\":\"1080p+\",\"qualityLabel\":\"1080P+\",…}]}]}",
"isKsManifest":…, "transcodeInfos":[…]},
"host-name":"hb2-acfun-kce-node20.aliyun"}
Parse: JSON.parse(playInfo.ksPlayJson) → adaptationSet[0].representation[].
Quality list actually returned for this video (HTTP 200):
| id | qualityType | qualityLabel | width×height | maxBitrate | avgBitrate |
|---|---|---|---|---|---|
| 1 | 1080p+ |
1080P+ | 1920×1080* | 6000 | 1483 |
| 2 | 1080p |
1080P | 1920×1080* | 3000 | 1143 |
| 3 | 720p |
720P | 1280×… | 2000 | 779 |
| 4 | 540p |
540P | 960×… | 900 | 431 |
| 5 | 360p |
360P | 640×… | 500 | 288 |
* the JSON field is misspelled heigh (not height) — read rep.heigh.
Page-level quality catalogue is also embedded in the HTML as
window.qualityConfig (all 16 tiers the player can negotiate):
2160p120HDR, 2160p120, 2160p60HDR, 2160p60, 2160pHDR, 2160p, 1080p60HDR, 1080p60, 1080p+, 1080pHDR, 1080p, 720p60, 720p (defaultSelect), 540p, 480p, 360p with {qualityType, qualityLabel, definition, disableAutoSwitch?, limitType?, defaultSelect?}. Individual videos expose only the
tiers they were transcoded to (the 5 above), so treat qualityConfig as the label dictionary and
representation[] as the actual streams.
4.2 m3u8 reachability — [VERIFIED]
m3u8 fetch HTTP 200 ; first line: #EXTM3U
- Signed query:
?sign=<md5>&t=<unix seconds>&us=<int>. Expiring (thetvalue is a future timestamp ≈ +? minutes; treat the URL as short-lived, re-call playInfo when it 403s). - Streams are HLS (
#EXTM3Uwith.tsslices;m3u8Slicealso inlines the playlist in pageInfo). - CDN hosts seen:
tx-video.acfun.cn,tx-safety-video.acfun.cn,ali-safety-video.acfun.cn. - Anonymous access is fine — no cookies needed for either playInfo or the CDN.
4.3 Danmaku (弹幕) read — [VERIFIED]
POST https://www.acfun.cn/rest/pc-direct/new-danmaku/list
Content-Type: application/x-www-form-urlencoded
resourceId={videoId}&resourceType=9&enableAdvanced=true&pcursor=1&count=200&sortType=1&asc=false
resourceId=videoId(part id).resourceType= 9 (video). [VERIFIED]resourceId=42588725(ac number) →totalCount=0;resourceId=32224197(videoId) →totalCount=5, 5 items.- Old-style XML danmaku endpoints are dead; this is the JSON API the current PC player uses.
Evidence (HTTP 200):
{"result":0,"danmakus":[
{"likeCount":0,"color":16777215,"mode":1,"roleId":0,"danmakuId":287561494,
"danmakuType":0,"danmakuStyle":…,"danmakuAvatarUrl":…,"isLike":false,"rank":5,
"createTime":1778494836010,"userId":77697264,"body":"啊啊啊好萌的和声",
"danmakuImgUrl":…,"position":11880,"size":25}, …],
"styleDanmakuCount":0,"requestId":"…","pcursor":"no_more",
"host-name":"hb2-acfun-kce-node24.aliyun","totalCount":5}
| Plugin field | Path | Sample |
|---|---|---|
| danmaku id | danmakuId |
287561494 |
| text | body |
啊啊啊好萌的和声 |
| time offset in video | position (milliseconds) |
11880 |
| color | color (int RGB) |
16777215 (white) |
| mode (scroll/top/bottom) | mode |
1 (1 scroll, 5 top, 4 bottom per player conventions) |
| font size | size |
25 |
| created | createTime (ms epoch) |
1778494836010 |
| author | userId |
77697264 |
| advanced/style | danmakuType, danmakuStyle, danmakuImgUrl, danmakuAvatarUrl |
— |
| likes | likeCount, isLike |
0, false |
Notes:
- Page
danmakuCount(8) ≠totalCounthere (5): some entries are filtered/special; alsoenableAdvanced=true/sortType=1change the set. UsetotalCountas authoritative for the API. - Pagination:
pcursorfrom the response ("no_more"when done),countpage size,ascorder. - Posting danmaku (write, not executed):
POST /rest/pc-direct/new-danmaku/addwithbody,color,id(=ac number),mode,position,size,subChannelId,subChannelName,type=douga,videoId— [UNVERIFIED], requires login,Refererset to the video page.
5. Hot / ranking / feeds — mixed
5.1 Banana / channel ranking — [VERIFIED] (the only public "hot" list that works)
POST https://www.acfun.cn/rest/pc-direct/rank/channel
Content-Type: application/x-www-form-urlencoded
channelId=0&rankPeriod=day&rankLimit=10 # rankPeriod: day | week ; GET also works
Evidence (HTTP 200):
{"result":0,"host-name":"hb2-acfun-kce-node24.aliyun","rankList":[
{"groupId":"Ml8wXzE3OTMwNzAzMDY4ODhfMzg1MA_1&-124&&rlg1","userId":17735808,
"dougaId":"48940670","contentId":48940670,"title":"XBOX联名,一个比一个抽象",
"userName":"Shi咪","channelName":"生活日常","channelId":86,
"viewCount":1615,"danmuCount":47,"commentCount":23,"bananaCount":1072,"likeCount":253,
"durationMillis":358099,"videoCover":"https://tx-free-imgs.acfun.cn/newUpload/…",
"createTimeMillis":1793014710758,"isFollowing":false,"isLike":false,"isFavorite":false,
"isThrowBanana":false,"tagList":[…],"videoList":[…],"user":{…},"channel":{…}}]}
rankList[] is a superset of the search item shape — it carries title, description,
channelName, tagList, durationMillis and full user object, so it is a decent
"browse without search" feed. Field names: dougaId/contentId (ac number), contentTitle
(alias of title on some nodes), videoCover/coverUrl, createTimeMillis, viewCount,
danmuCount, commentCount, bananaCount, likeCount, durationMillis.
POST /rest/pc-direct/rank/banana (channelId=0&rankPeriod=DAY) → [VERIFIED] HTTP 200,
same rankList shape (每日香蕉榜).
5.2 Following feed — [VERIFIED] route, empty without login
GET https://www.acfun.cn/rest/pc-direct/feed/webPush?count=20&pcursor=0
{"result":0,"feedList":[],"followUpers":[],"pcursor":"no_more",
"host-name":"hb2-acfun-kce-node24.aliyun","requestId":"…"}
Route exists and returns a valid envelope, but feedList is empty anonymously → login required
for real content [UNVERIFIED for populated shape; item fields match the rank item shape per
community docs: videoId, caption, coverUrl, playDuration, channel, viewCount,
commentCount, user, tag, likeCount, createTime].
5.3 Dead endpoints — [VERIFIED 404] (all returned the 102.5 KB SPA shell)
GET/POST /rest/pc-direct/rank/video (tried rankId=1, channelId+rankPeriod variants)
GET/POST /rest/pc-direct/rank/video/day
/rest/pc-direct/rank/douga
/rest/pc-direct/rank/list
/rest/pc-direct/rank/hot
/rest/pc-direct/feed/recommend
/rest/pc-direct/feed/hot
/rest/pc-direct/feed/list
/rest/pc-direct/recommend/list
/rest/pc-direct/recommend
/rest/pc-direct/video/recommend
/rest/pc-direct/hotVideo
/rest/pc-direct/index/recommend
/rest/pc-direct/index/hot
There is no working anonymous "recommended for you" JSON endpoint as of 2026-10. The homepage
(https://www.acfun.cn/) is fully SSR (pagelets pagelet_douga, pagelet_dance, …,
pagelet_monkey_recommend); a plugin that wants a home feed must either scrape those pagelets from
the HTML or use rank/channel.
6. Interactions (cookie-authenticated) — routes [VERIFIED], semantics [UNVERIFIED]
I never executed a successful write (no AcFun account, and the task forbids logging in). Everything below is proven to exist by unauthenticated probing: each endpoint answers HTTP 401 with JSON instead of the 404 app shell. Parameters come from the site's own JS bundles (
@ac/pc-comment.*.js,newVideo/widget/bread/bread.*.js) — that is the exact contract the browser uses.
6.1 Like / "顶" (点赞 + 取消) — [VERIFIED route, 401]
The PC web does not use a pc-direct thumb endpoint (all /rest/pc-direct/thumb/* → 404).
It calls the Kuaishou-side interact service:
POST https://kuaishouzt.com/rest/zt/interact/add # 点赞
POST https://kuaishouzt.com/rest/zt/interact/delete # 取消点赞
Content-Type: application/x-www-form-urlencoded; withCredentials: true
Form fields (from bread.es6, likeParams + token merge):
| Param | Value |
|---|---|
objectId |
ac number (contentId / dougaId) |
objectType |
2 |
interactType |
1 |
subBiz |
mainApp |
kpn |
ACFUN_APP |
kpf |
PC_WEB |
acfun.midground.api_st |
token from §7, param name is `${sid}_st` with sid = "acfun.midground.api" |
userId |
userId returned by the token call |
extParams |
object: {isPlaying, showCount, otherBtnClickedCount, playBtnClickedCount} (jQuery serializes as extParams[...]) |
Success response: {"result":1,"hostName":"…"} (1, not 0). Button state then flips locally.
Evidence (unauthenticated):
POST https://kuaishouzt.com/rest/zt/interact/add
-> HTTP 401 {"result":-401,"error_msg":"token value error","error_id":""}
Note: pageInfo contains window.likeDomain = "https://kuaishouzt.com" — use that value rather than
hardcoding; it is how the page resolves the host.
6.2 Coin / 投蕉 — [VERIFIED route, 401]
POST https://www.acfun.cn/rest/pc-direct/banana/throwBanana
Content-Type: application/x-www-form-urlencoded
resourceId={ac number}&count={1..5}&resourceType=2
Evidence (unauthenticated):
POST /rest/pc-direct/banana/throwBanana (resourceId=42588725&count=2&resourceType=2)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
App/legacy duplicate (same error class): POST https://api-ipv6.acfunchina.com/rest/app/banana/throwBanana
with resourceId,count,resourceType. Success shape per community docs:
{"result":0,"extData":{"bananaRealCount":5,"criticalHitInfo":null}}. Rules: cannot banana your own
video (page checks auth_key !== videoInfo.user.id), 1–5 per call.
6.3 Favorite / 收藏 — [VERIFIED routes, 401 "token value error"]
Videos ("v" category, resourceType = 9) use a multi-folder system:
POST /rest/pc-direct/favorite/folder/list {resourceId}
POST /rest/pc-direct/favorite/folder/add {name} # 1-40 chars, CJK/alnum/_
POST /rest/pc-direct/favorite/resource/add {resourceId, resourceType, addFolderIds} # comma list
POST /rest/pc-direct/favorite/resource/remove {resourceId, resourceType, delFolderIds}
POST /rest/pc-direct/favorite/resource/updateFolder {resourceId, resourceType, addFolderIds, delFolderIds}
resourceId = ac number, resourceType = 9 for videos. Max 10 folders per operation; a
folder can be full (resourceCount >= favoriteCountLimit) or type:1 (default folder, no limit).
Bangumi (番剧) uses a different pair: POST /rest/pc-direct/favorite {resourceId,resourceType}
and POST /rest/pc-direct/unFavorite {resourceIds,resourceType}.
Evidence (unauthenticated):
POST /rest/pc-direct/favorite/resource/add -> HTTP 401 {"result":-401,"error_msg":"token value error"}
POST /rest/pc-direct/favorite/folder/list -> HTTP 401 {"result":-401,"error_msg":"token value error"}
Important: favorites require the midground token (§7), not just the session cookie — the error
message differs from the plain user not login routes. Success is result:0 plus a dataList for
folder/list ([{folderId, name, type, resourceCount, favoriteCountLimit, inFolder}]).
6.4 Follow / 关注 (and unfollow) — [VERIFIED route, 401]
POST https://www.acfun.cn/rest/pc-direct/relation/follow
Content-Type: application/x-www-form-urlencoded
toUserId={userId}&action=1&groupId=-1 # action: 1 = follow, 2 = unfollow ; groupId -1 = default
Group management (optional): POST /rest/pc-direct/relation/getGroups, getFollows
(action=7 follows / 8 fans, page, count, groupId).
Evidence (unauthenticated):
POST /rest/pc-direct/relation/follow (toUserId=52326&action=1&groupId=-1)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
6.5 Post a comment (and delete/like) — [VERIFIED routes, 401]
POST https://www.acfun.cn/rest/pc-direct/comment/add
Content-Type: application/x-www-form-urlencoded
objectId={ac number}&content={UBB text}&replyToCommentId=0&subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP
objectId= ac number (dougaId), notsourceId/sourceType(despite the list endpoint using those). This is what@ac/pc-comment'sKe()sends.replyToCommentId = 0for a top-level comment, or the parentcidto reply.contentis UBB text; emoticons are[emot=acfun,{emotionId}/].ztExtParamsis optional (image payload for the image-comment mode).- Success:
{"result":0,"commentId":…}(the widget then reloads the list). - A captcha/verification branch exists for
result400001–410999: the site redirects toerror_urland obtains a{type, token}before retrying — a plugin should surface that failure rather than retry blindly.
Related:
POST /rest/pc-direct/comment/delete {sourceId, sourceType, commentId}
POST /rest/pc-direct/comment/like {commentId} # or {sourceId, sourceType, commentId}
POST /rest/pc-direct/comment/unlike {commentId}
Evidence (unauthenticated):
POST /rest/pc-direct/comment/add (objectId=42588725&content=test&replyToCommentId=0)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
POST /rest/pc-direct/comment/like -> HTTP 401 {"result":-401,"error_msg":"user not login"}
6.6 Miscellaneous authenticated read routes (useful for "am I logged in?")
| Route | Method | Anon result |
|---|---|---|
/rest/pc-direct/user/personalInfo |
POST | 401 {"result":-401,"error_msg":"user not login"} [VERIFIED] |
/rest/pc-direct/user/userInfo?userId= |
POST/GET | public profile [UNVERIFIED live, route from bundle] |
/rest/pc-direct/relation/getGroups |
POST | 401 |
/rest/pc-direct/browse/history/list |
POST | 401 |
/rest/pc-direct/user/signIn |
POST | 401 |
personalInfo doubles as the cheapest session check: result:0 + info.userId means the
acPasstoken/auth_key cookie pair is valid.
7. Token flow for like / favorite (midground token) — [VERIFIED endpoints, auth required]
The site's common.es6 exposes getTokenGen("acfun.midground.api"):
POST https://id.app.acfun.cn/rest/web/token/get
Content-Type: application/x-www-form-urlencoded
sid=acfun.midground.api
Success (community docs, needs a logged-in cookie jar on id.app.acfun.cn):
{"result":0,"ssecurity":"***","userId":625724,
"acfun.midground.api_st":"***","acfun.midground.api.at":"***"}
acfun.midground.api_stis then sent as a form field (not header) tokuaishouzt.com/rest/zt/interact/*and to/rest/pc-direct/favorite/*, withuserId.- The PC page itself calls this token endpoint through
common/static/js/common.*.js(rest/web/token/getappears there). - [VERIFIED] unauthenticated:
POST https://id.app.acfun.cn/rest/web/token/get→HTTP 401 {"result":-401,"error_msg":"token value error"}— route exists, session required. - Token is short-lived; re-fetch per action batch rather than caching for hours. Treat the whole like/favorite flow as [UNVERIFIED] end-to-end because it cannot be tested without credentials.
8. CORS, redirects, rate-limit, encoding quirks
8.1 CORS — [VERIFIED]
Response headers on /rest/pc-direct/search/video:
HTTP/1.1 200 OK
Server: Tengine
Content-Type: application/json;charset=UTF-8
Access-Control-Allow-Origin: https://www.acfun.cn
Access-Control-Allow-Credentials: true
Strict-Transport-Security: max-age=172800
X-Cache: MISS TCP_MISS
- ACAO is pinned to
https://www.acfun.cn— a browser page on another origin cannot read these responses. Node.jsfetchis unaffected (no CORS enforcement), which is why this plugin approach works server-side. - Cookie-authenticated calls need the cookie jar and
credentials: 'include'only if you ever run in a browser-like context; in Node just set theCookieheader. - The kuaishouzt interact host answers with the same auth envelope; no ACAO needed from Node.
8.2 Required headers / redirects — [VERIFIED]
- No
Refererand no custom UA needed for JSON reads (both tested → 200). www.acfun.cnserves everything over HTTPS directly; no redirect chains observed (curl -Lnot needed).m.acfun.cnis a different (mobile) frontend — do not mix.- 404s are HTTP 404 with the full SPA HTML shell, ~102.5 KB. A plugin MUST check
content-type/ body prefix beforeJSON.parse; otherwise a dead path throws a confusing JSON syntax error.
8.3 Rate limiting — [VERIFIED: none hit]
- 6 rapid consecutive search calls:
200 200 200 200 200 200. - During a 20-call batch at ~0 ms spacing I saw a few transient
curltransport failures (no response / connection reset), which recovered immediately on retry. That is the only throttling-like behavior observed. - Practical advice: 200–500 ms between calls, retry once on network error; no
429seen.
8.4 Encoding
- All JSON is UTF-8 (
Content-Type: application/json;charset=UTF-8). Decode as UTF-8 — Chinese text is not escaped, so a wrong code page garbles it (this bit me in the Windows console only). Content-Typefor POSTs:application/x-www-form-urlencoded; charset=UTF-8. json bodies are not used by/rest/pc-direct/*.playDurationin search results is a"mm:ss"string, not seconds;durationMilliselsewhere is milliseconds.
9. Recommended plugin mapping (what to call for each capability)
| Capability | Call |
|---|---|
resolve ac123 → detail |
GET /v/ac{acId} → parse window.pageInfo (§1) |
| get part id for playback | pageInfo.currentVideoId (or search videoId) |
| search videos | GET /rest/pc-direct/search/video?keyword=&sortType=0|2 (first 30 only) |
| comments | GET /rest/pc-direct/comment/listByFloor?sourceId={acId}&sourceType=3&page=N |
| replies | GET /rest/pc-direct/comment/sublist?sourceId={acId}&sourceType=3&rootCommentId={cid}&page=N |
| play URL / qualities | GET /rest/pc-direct/play/playInfo/ksPlayJson?videoId={videoId} → JSON.parse(playInfo.ksPlayJson) |
| danmaku | POST /rest/pc-direct/new-danmaku/list (resourceId={videoId}&resourceType=9&…) |
| hot/ranking | POST /rest/pc-direct/rank/channel (channelId=0&rankPeriod=day|week&rankLimit=N) |
| follow feed | GET /rest/pc-direct/feed/webPush (login) |
| like / favorite / follow / banana / comment | §6 + token §7 (login required) |
Dead / do-not-use as of 2026-10-27
/rest/pc-direct/video/{index,detail,getVideoInfo,info}, /rest/pc-direct/rank/video*,
/rest/pc-direct/feed/{recommend,hot,list}, /rest/pc-direct/recommend*, /rest/pc-direct/hot*,
/rest/pc-direct/thumb/*, /rest/pc-direct/coin/*, /rest/pc-direct/stow|collect/*,
/rest/pc-direct/comment/{list (POST anon), subCommentList, hotList, replyList},
/rest/pc-direct/play/{playInfo,m3u8}. All answered 404 + SPA shell (or result:11/21 for
routes that exist but reject anonymous/legacy usage). The current replacements are the SSR page
(§1), play/playInfo/ksPlayJson (§4) and comment/listByFloor (§3).
Open items (explicitly UNVERIFIED — no login used)
- End-to-end success shapes for like/favorite/follow/banana/comment-add (only 401 proofs here).
- The exact minimum cookie set for authenticated calls (
acPasstoken+auth_keyper community docs; PC additionally sends_userid,acPlatform=2,csrfToken,_did). - Whether
kuaishouzt.comvsapi.kuaishouzt.commatters for/rest/zt/interact/*in 2026 (page useswindow.likeDomain=https://kuaishouzt.com; the olderapi.host returned 502 on a probe of a different path). - Populated
feed/webPushitem shape (empty without login).