# 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): ```http 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-8` for every POST listed here (the site's own client sends form-encoded, **not** JSON, for `/rest/pc-direct/*`). - **No `sign` / `__` / `md5` parameter is needed** on any read endpoint verified here. The site's own comment widget (`@ac/pc-comment.*.js`) posts plain form data with `subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP` and no signature. (Auth token is a separate thing, §7.) ### 0.4 Common response envelope ```json {"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: ```js 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): ```powershell $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: ```js 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 `` 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 `` highlights | `emTitle` | `【原神】…` | | 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`), *not* `videoId`. - `sourceType` = **3** for videos (the embedded comment objects themselves carry `"sourceType": 3`, which is the authoritative value). Lenient server quirk: `sourceType=1` also returned the same 21 comments; `sourceType=2` returned 0. Use **3**. - `supportZtEmot=true` enables Kuaishou emoticon rendering in `content`. - Optional `pivotCommentId` / `newPivotCommentId` / `t` (cache-buster ms) accepted. **Evidence (HTTP 200):** ```json {"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):** ```json {"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):** ```json {"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=&t=&us=`. **Expiring** (the `t` value is a future timestamp ≈ +? minutes; treat the URL as short-lived, re-call playInfo when it 403s). - Streams are HLS (`#EXTM3U` with `.ts` slices; `m3u8Slice` also 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):** ```json {"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) ≠ `totalCount` here (5): some entries are filtered/special; also `enableAdvanced=true`/`sortType=1` change the set. Use `totalCount` as authoritative for the API. - Pagination: `pcursor` from the response (`"no_more"` when done), `count` page size, `asc` order. - Posting danmaku (write, not executed): `POST /rest/pc-direct/new-danmaku/add` with `body,color,id(=ac number),mode,position,size,subChannelId,subChannelName,type=douga,videoId` — **[UNVERIFIED]**, requires login, `Referer` set 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):** ```json {"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 ``` ```json {"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`), **not** `sourceId`/`sourceType` (despite the list endpoint using those). This is what `@ac/pc-comment`'s `Ke()` sends. - `replyToCommentId = 0` for a top-level comment, or the parent `cid` to reply. - `content` is UBB text; emoticons are `[emot=acfun,{emotionId}/]`. `ztExtParams` is optional (image payload for the image-comment mode). - Success: `{"result":0,"commentId":…}` (the widget then reloads the list). - A captcha/verification branch exists for `result` 400001–410999: the site redirects to `error_url` and 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`): ```json {"result":0,"ssecurity":"***","userId":625724, "acfun.midground.api_st":"***","acfun.midground.api.at":"***"} ``` - `acfun.midground.api_st` is then sent as a **form field** (not header) to `kuaishouzt.com/rest/zt/interact/*` and to `/rest/pc-direct/favorite/*`, with `userId`. - The PC page itself calls this token endpoint through `common/static/js/common.*.js` (`rest/web/token/get` appears 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.js `fetch` is 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 the `Cookie` header. - The kuaishouzt interact host answers with the same auth envelope; no ACAO needed from Node. ### 8.2 Required headers / redirects — [VERIFIED] - No `Referer` and no custom UA needed for JSON reads (both tested → 200). - `www.acfun.cn` serves everything over HTTPS directly; no redirect chains observed (`curl -L` not needed). `m.acfun.cn` is 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 before `JSON.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 `curl` transport 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 `429` seen. ### 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-Type` for POSTs: `application/x-www-form-urlencoded; charset=UTF-8`. json bodies are **not** used by `/rest/pc-direct/*`. - `playDuration` in search results is a `"mm:ss"` **string**, not seconds; `durationMillis` elsewhere 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) 1. End-to-end success shapes for like/favorite/follow/banana/comment-add (only 401 proofs here). 2. The exact minimum cookie set for authenticated calls (`acPasstoken`+`auth_key` per community docs; PC additionally sends `_userid`, `acPlatform=2`, `csrfToken`, `_did`). 3. Whether `kuaishouzt.com` vs `api.kuaishouzt.com` matters for `/rest/zt/interact/*` in 2026 (page uses `window.likeDomain` = `https://kuaishouzt.com`; the older `api.` host returned 502 on a probe of a different path). 4. Populated `feed/webPush` item shape (empty without login).