{"id":37023292,"url":"https://github.com/czp3009/bilibili-api","last_synced_at":"2026-01-14T02:47:40.886Z","repository":{"id":57724977,"uuid":"109809642","full_name":"czp3009/bilibili-api","owner":"czp3009","description":"Bilibili API library for Kotlin","archived":false,"fork":false,"pushed_at":"2020-10-04T06:37:03.000Z","size":943,"stargazers_count":505,"open_issues_count":3,"forks_count":50,"subscribers_count":18,"default_branch":"master","last_synced_at":"2023-11-07T15:24:40.404Z","etag":null,"topics":["bilibili-api","java","kotlin"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/czp3009.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-11-07T08:49:28.000Z","updated_at":"2023-11-02T14:05:08.000Z","dependencies_parsed_at":"2022-09-10T23:40:05.739Z","dependency_job_id":null,"html_url":"https://github.com/czp3009/bilibili-api","commit_stats":null,"previous_names":[],"tags_count":0,"template":null,"template_full_name":null,"purl":"pkg:github/czp3009/bilibili-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czp3009%2Fbilibili-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czp3009%2Fbilibili-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czp3009%2Fbilibili-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czp3009%2Fbilibili-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/czp3009","download_url":"https://codeload.github.com/czp3009/bilibili-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czp3009%2Fbilibili-api/sbom","scorecard":{"id":315090,"data":{"date":"2025-08-11","repo":{"name":"github.com/czp3009/bilibili-api","commit":"56f26e7106d03a9224cf12b8cfd056380ac6759e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":1,"reason":"Found 4/21 approved changesets -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":8,"reason":"binaries present in source code","details":["Warn: binary detected: gradle/wrapper/gradle-wrapper.jar:1","Warn: binary detected: record/视频弹幕/list.so:1"],"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: GNU General Public License v3.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 13 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-18T00:00:39.994Z","repository_id":57724977,"created_at":"2025-08-18T00:00:39.994Z","updated_at":"2025-08-18T00:00:39.994Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408737,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bilibili-api","java","kotlin"],"created_at":"2026-01-14T02:47:40.314Z","updated_at":"2026-01-14T02:47:40.872Z","avatar_url":"https://github.com/czp3009.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bilibili API JVM 调用库\n该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.\n\n使用一台虚拟的 `Pixel 2` 设备来截取数据包, 一些固定参数可能与真实设备不一致.\n\n# 使用\n```groovy\ncompile group: 'com.hiczp', name: 'bilibili-api', version: '0.2.1'\n```\n\n对于 Android 项目, 如需要解析弹幕, 添加\n```groovy\ncompile group: 'javax.xml.stream', name: 'stax-api' version: last_version\ncompile group: 'org.codehaus.woodstox', name: 'woodstox-core-asl' version: last_version\n```\n\n# 技术说明\n`BilibiliClient` 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.\n\n所有调用从这个类开始, 包括登陆以及访问其他各种 API.\n\n使用协程来实现异步, 由于 [kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 为编译器实现, 因此并非所有 JVM 语言都能正确调用 `suspend` 方法.\n\n本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.\n\n`BilibiliClient` 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 `BilibiliClient` 实例, 而应该保存其引用.\n\n一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:\n\n```kotlin\nrunBlocking {\n    val bilibiliClient = BilibiliClient().apply {\n        login(username, password)\n    }\n    val myInfo = bilibiliClient.appAPI.myInfo().await()\n    val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()\n}\n```\n\n如果一个请求的返回内容中的 `code`(code 是 BODY 的内容, 并非 HttpStatus) 不为 0, 将抛出异常 `BilibiliApiException`, 通过以下代码来获取服务器原始返回的 `code`:\n\n```kotlin\nval code = bilibiliApiException.commonResponse.code\n```\n\n一个错误返回的原始 `JSON` 如下所示:\n\n```json\n{\n    \"code\": -629,\n    \"message\": \"用户名与密码不匹配\",\n    \"ts\": 1550730464\n}\n```\n\n每种不同的 API 在错误时返回的 `code` 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.\n\n# 登录和登出\n(Bilibili oauth2 v3)\n\n登陆和登出均为异步方法, 需要在协程上下文中执行(接下去不会特地强调这一点).\n\n```kotlin\nrunBlocking {\n    BilibiliClient().run {\n        login(username, password)\n        logout()\n    }\n}\n```\n\n`login` 方法返回一个 `LoginResponse` 实例, 下次可以直接赋值到没有登陆的 `BilibiliClient` 实例中来恢复登陆状态.\n\n```kotlin\nBilibiliClient().apply {\n    this.loginResponse = loginResponse\n}\n```\n\n`LoginResponse` 继承 `Serializable`, 可被序列化(JVM 序列化).\n\n可能的错误返回有两种:\n\n    -629 用户名与密码不匹配\n    -105 验证码错误\n\n如果仅使用用户名与密码进行登陆并且得到了 `-105` 的结果, 那么说明需要验证码(通常是由于多次错误的登陆尝试导致的).\n\n原始返回如下所示\n\n    {\"ts\":1550569982,\"code\":-105,\"data\":{\"url\":\"https://passport.bilibili.com/register/verification.html?success=1\u0026gt=b6e5b7fad7ecd37f465838689732e788\u0026challenge=9a67afa4d42ede71a93aeaaa54a4b6fe\u0026ct=1\u0026hash=105af2e7cc6ea829c4a95205f2371dc5\"},\"message\":\"验证码错误!\"}\n\n自行访问 `commonResponse.data.obj.url.string` 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:\n\n```kotlin\nlogin(username, password, challenge, secCode, validate)\n```\n\n`challenge` 为本次极验的唯一标识(在一开始给出的 url 中)\n\n`validate` 为极验返回值\n\n`secCode` 为 `\"$validate|jordan\"`\n\n(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).\n\n注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误(想要这么做的人一定脑子瓦特了).\n\n登陆后, 可以访问全部 API(注意, 有一些明显不需要登录的 API 也有可能需要登录).\n\n注意, 即使返回`code`为0也不一定登录成功, 例如\n\n```json\n{\n  \"ts\": 1584206212,\n  \"code\": 0,\n  \"data\": {\n    \"status\": 1,\n    \"url\": \"https://passport.bilibili.com/mobile/verifytel_h5.html?mid\\u003d517548681\\u0026tel\\u003d156****0364\\u0026source\\u003d2\\u0026keepTime\\u003d0\\u0026appId\\u003d878\\u0026subId\\u003d0\\u0026ticket\\u003d1\"\n  }\n}\n```\n\n需要手动判断`data.url`是否为`null`, 尽管这种情况不多见\n\n由于各种需要登陆的 API 在未登录时返回的 `code` 并不统一, 因此没有办法做自动 `token` 刷新, 自己看着办.\n\n在真实的客户端上, 每次一打开 APP 就会访问[个人信息 API](#获取个人信息)来确定 `token` 是否仍然可用, 这就是 B站 自己的解决方案.\n\n# 访问 API\n不要问文档, 用自动补全(心)来感受. 以下给出几个示例\n\n## 获取个人信息\n(首先要登陆)\n\n```kotlin\nval myInfo = bilibiliClient.appAPI.myInfo().await()\n```\n\n返回用户 ID, vip 信息等.\n\n## 搜索\n当我们想看某些内容时, 我们会首先使用搜索功能, 例如\n\n```kotlin\nval searchResult = bilibiliClient.appAPI.search(keyword = \"刀剑神域\").await()\n```\n\n实际上这对应客户端上的 搜索 -\u003e 综合.\n\n如果要搜索番剧则使用 `bilibiliClient.appAPI.searchBangumi`.\n\n同理, 搜索直播, 用户, 影视, 专栏分别使用 `searchLive`, `searchUser`, `searchMovie`, `searchArticle`.\n\n所有的搜索都使用 `pageNumber` 参数来控制翻页(从 1 开始).\n\n## 获取视频播放地址\n获取视频实际播放地址的 API 比较特殊, 被单独分了出来, 示例如下\n\n```kotlin\nval videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 72913641).await()\n```\n\n`aid` 即 av 号, 只能表示视频播放的那个页面, 如果一个视频有多个 `p`, 那么每个 `p` 都有单独的 `cid`.\n\n在 Web 端, URL 通常是这样的\n\n    https://www.bilibili.com/video/av44541340/?p=2\n\n实际上就是选择了该 `aid` 下的第二个 `cid`(注意, 参数里使用的 `cid` 不是这个 p 的序号, 它也是一个很长的数字).\n\n简单的来说, `aid` 和 `cid` 加在一起才能表示一个视频流(为什么 `cid` 不能直接表示一个视频我也不知道).\n\n因此无论是获取视频播放地址, 还是获取弹幕列表, 都要同时传入 `aid` 与 `cid`.\n\n而 `cid` 在哪里获得呢, 如下所示\n\n```kotlin\nval view = bilibiliClient.appAPI.view(aid = 41517911).await()\n```\n\n该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.\n\n其中 `data.cid` 为默认 `p` 的 `cid`. `data.pages[n].cid` 为每个 `p` 的 `cid`. 如果只有一个 `p` 那么说明视频没有分 `p`.\n\n请求视频地址将访问如下结构的内容\n\n```json\n{\n    \"code\": 0,\n    \"data\": {\n        \"accept_description\": [\n            \"高清 1080P+\",\n            \"高清 1080P\",\n            \"高清 720P\",\n            \"清晰 480P\",\n            \"流畅 360P\"\n        ],\n        \"accept_format\": \"hdflv2,flv,flv720,flv480,flv360\",\n        \"accept_quality\": [\n            112,\n            80,\n            64,\n            32,\n            16\n        ],\n        \"dash\": {\n            \"audio\": [\n                {\n                    \"bandwidth\": 319173,\n                    \"base_url\": \"http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_\u0026deadline=1551113319\u0026gen=playurl\u0026nbs=1\u0026oi=3670888782\u0026os=ks3u\u0026platform=android\u0026trid=925269b941bf4883ac9ec92c6ab5af4e\u0026uipk=5\u0026upsig=33273eaf403739d9f51304509f55589e\",\n                    \"codecid\": 0,\n                    \"id\": 30280\n                },\n                {\n                    \"bandwidth\": 67326,\n                    \"base_url\": \"http://upos-hz-mirrorkodou.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30216.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_\u0026deadline=1551113319\u0026gen=playurl\u0026nbs=1\u0026oi=3670888782\u0026os=kodou\u0026platform=android\u0026trid=925269b941bf4883ac9ec92c6ab5af4e\u0026uipk=5\u0026upsig=3d1f9b836430bb8033b2f318faf42f9b\",\n                    \"codecid\": 0,\n                    \"id\": 30216\n                }\n            ],\n            \"video\": [\n                {\n                    \"bandwidth\": 376693,\n                    \"base_url\": \"http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30015.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_\u0026deadline=1551113319\u0026gen=playurl\u0026nbs=1\u0026oi=3670888782\u0026os=ks3u\u0026platform=android\u0026trid=925269b941bf4883ac9ec92c6ab5af4e\u0026uipk=5\u0026upsig=82bc845bce9f22b731b062bf83fa000f\",\n                    \"codecid\": 7,\n                    \"id\": 16\n                },\n                ...\n                {\n                    \"bandwidth\": 2615324,\n                    \"base_url\": \"http://upos-hz-mirrorcosu.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30080.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_\u0026deadline=1551113319\u0026dynamic=1\u0026gen=playurl\u0026oi=3670888782\u0026os=cosu\u0026platform=android\u0026rate=0\u0026trid=925269b941bf4883ac9ec92c6ab5af4e\u0026uipk=5\u0026uipv=5\u0026um_deadline=1551113319\u0026um_sign=22fef3c0efa0d23388429f6926fad298\u0026upsig=c4768c036beb667ba4648369770f8de8\",\n                    \"codecid\": 7,\n                    \"id\": 80\n                }\n            ]\n        },\n        \"fnval\": 16,\n        \"fnver\": 0,\n        \"format\": \"flv480\",\n        \"from\": \"local\",\n        \"quality\": 32,\n        \"result\": \"suee\",\n        \"seek_param\": \"start\",\n        \"seek_type\": \"offset\",\n        \"timelength\": 175332,\n        \"video_codecid\": 7,\n        \"video_project\": true\n    },\n    \"message\": \"0\",\n    \"ttl\": 1\n}\n```\n\n(由于内容太长, 去除了一部分内容)\n\n注意, 视频下载地址有好几个(以上返回内容中被折叠成了两个), 但是实际上他们都是一样的内容, 只是清晰度不同. `data.dash.video.id` 实际上代表 `data.accept_quality`.\n\n视频和音频是分开的, 视频和音频都返回 `m4s` 文件, 将其合并即可得到完整的 `mp4` 文件.\n\n`data.quality` 指默认选择的清晰度, 通常情况下移动网络会自动选择 `32`, 即 \"清晰 480P\"(在 `data.accept_description` 中对应).\n\n对于番剧来说, 也使用 `aid` 与 `cid` 来获得播放地址\n\n```kotlin\nval bangumiPlayUrl = bilibiliClient.playerAPI.bangumiPlayUrl(aid = 42714241, cid = 74921228).await()\n```\n\n返回内容差不多是一个原理, 这里就不赘述了.\n\n如何获得番剧的 `aid` 与 `cid` 呢. 我们都知道, 实际上番剧那个页面的唯一标识是 \"季\", 同一个番的不同 \"季\" 其实是不同的东西.\n\n我们在番剧搜索页面可以得到番剧的 `season`, 这代表了一个番剧的某一季的页面.\n\n然后我们用 `season` 来打开番剧页面.\n\n```kotlin\nval season = bilibiliClient.mainAPI.season(seasonId = 25617).await()\n```\n\n返回值中的 `result.seasons[n].season_id` 为该番所有季的 id(包含用来作为查询条件的 `seasonId`).\n\n该 API 还可以用 `episodeId` 作为查询条件, 即以集为条件打开一个番剧页面(会跳转到对应的季).\n\n返回值中的 `result.episodes` 包含了当前所选择的季的全部集的 `aid` 与 `cid`.\n\n## 查看视频下面的评论\n看完了视频当然要看一下傻吊网友都在说些什么. 使用以下 API 获取一个视频的评论.\n\n```kotlin\nval reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()\n```\n\n这里的 `oid` 指 `aid`(其他一些 API 中 `oid` 也可能指 `cid` 详见方法上面的注释).\n\n评论是不分 `p` 的, 所有评论都是在一起的.\n\n可以额外使用一个 `next` 参数来指定返回的起始楼层(即翻页).\n\n楼层是越翻越小的, 所以 `next` 也要越来越小.\n\n看到了傻吊网友们的评论是不够的, 我们还想看到杠精与其隔着屏幕对喷的场景, 因此我们要获取评论的子评论, 即评论的评论\n\n```kotlin\nval childReply = bilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()\n```\n\n其中的 `root` 表示根评论的 id.\n\n每个评论都有自己的 `replyId`, `parentId` 以及 `rootId`.\n\n假如一个人在一个评论的子评论里发布了一个评论并且 at 了其他人发的评论, 那么其 `parentId` 是他所 at 的评论, 其 `rootId` 为所在的根评论.\n\n如果不满足对应的层级逻辑关系(例如本身为根评论), `parentId` 或 `rootId` 可能为 0.\n\n用额外的 `minId` 参数来指定返回的起始子楼层.\n\n注意, 子楼层是越翻越大的.\n\n如果一个根评论下面有很多个喷子在互喷, 会导致看不清, 客户端上有一个按钮 \"查看对话\" 就是解决这个问题的.\n\n```kotlin\nval chatList = bilibiliClient.mainAPI.chatList(oid = 34175504, root = 1136310360, dialog = 1136351035).await()\n```\n\n`root` 为根评论 ID, `dialog` 为父评论 ID.\n\n用 `minFloor` 控制分页, 原理同上.\n\n番剧下面的评论用一样的方式获取.\n\n## 获得一个视频的弹幕\n看评论自然不够刺激, 我们想看到弹幕!\n\n获取弹幕非常简单\n\n```kotlin\nval danmakuFile = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await()\n```\n\n弹幕是一个文件, 可能非常大, 里面是二进制内容.\n\n为了解析弹幕, 我们要用到另一个类\n\n```kotlin\nval (flagMap, danmakuList) = DanmakuParser.parser(danmakuFile.byteStream())\n```\n\n`flagMap` 类型为 `Map\u003cLong, Int\u003e` 键和值分别表示 弹幕ID 与 弹幕等级.\n\n弹幕等级在区间 \\[1, 10\\] 内, 低于客户端设置的 \"弹幕云屏蔽等级\" 的弹幕将不会显示出来.\n\n`danmakuList` 类型为 `List\u003cDanmaku\u003e`, 内含所有解析得到的弹幕.\n\n使用以下代码来输出全部弹幕的内容\n\n```kotlin\ndanmakuList.forEach {\n    println(it.content)\n}\n```\n\n注意, 弹幕的解析是惰性的, `danmakuList` 是一个 `Sequence`. 如果同时持有很多未用完的 `danmakuList` 的引用可能会造成大量内存浪费.\n\n客户端的弹幕屏蔽设置是对弹幕中的 `user` 属性做的. 而实际上 `danmaku.user` 是一个字符串.\n\n这个字符串是 用户ID 的 `CRC32` 的校验和.\n\n众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.\n\n在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法完全确定一条弹幕到底是哪个用户发送的.\n\n如果想获得发送这条弹幕的所有可能的用户的 ID, 可以通过以下方法:\n\n```kotlin\nval possibleUserIds = danmaku.calculatePossibleUserIds()\n```\n\n返回一个 `List\u003cInt\u003e`, 内容为所有可能的用户 ID(至少有一个).\n\n注意, 第一次使用 `CRC反查` 功能将花费大约 `300ms` 来生成彩虹表, 如果想手动初始化请使用以下代码\n\n```kotlin\nCrc32Cracker\n```\n\n(`Crc32Cracker` 是一个惰性初始化的单例)\n\n通常情况下, 一次 `CRC反查` 耗时大约 `1ms`.\n\n由于这是一个比较耗时的操作, 请不要每条弹幕都如此操作(相比较 6000 条弹幕的解析只需要 `150ms`).\n\n番剧的弹幕同理.\n\n## 发送视频弹幕\n光看不发憋着慌, 我们来发送一条视频弹幕:\n\n```kotlin\nbilibiliClient.mainAPI.sendDanmaku(aid = 40675923, cid = 71438168, progress = 2297, message = \"2333\").await()\n```\n\n其中 `progress` 是播放器时间, 其他观众将看到你的弹幕在视频的此处出现, 单位为毫秒.\n\n`message` 应该是有长度限制的, 但是没有测过.\n\n如果不确定视频的长度, 需要从[视频播放地址的 API](#获取视频播放地址) 中的 `data.timelength` 来获得, 单位也是毫秒.\n\n## 获取直播弹幕\n刚进入直播间时, 立即看到的十条弹幕实际上是最近的历史弹幕, 通过以下方式来获取\n\n```kotlin\nbilibiliClient.liveAPI.roomMessage(roomId).await()\n```\n\n接下来的弹幕都是实时弹幕, 直播间实时弹幕通过 `Websocket` 来推送.\n\n```kotlin\nval job = bilibiliClient.liveClient(roomId = 3) {\n    onConnect = {\n        println(\"Connected\")\n    }\n\n    onPopularityPacket = { _, popularity -\u003e\n        println(\"Current popularity: $popularity\")\n    }\n\n    onCommandPacket = { _, jsonObject -\u003e\n        println(jsonObject)\n    }\n\n    onClose = { _, closeReason -\u003e\n        println(closeReason)\n    }\n}.launch()\n```\n\n服务器推送的 `Message` 有两种, 一种是 `人气值` 数据, 另一种是 `Command` 数据.\n\n`Command` 数据包用于控制客户端渲染何种内容. 弹幕, 送礼, 系统公告等全部都是由 `Command` 数据包控制的, 其本体为一个 `JsonObject`.\n\n例如一个弹幕数据是这样的(`cmd` 字段的值为 `DANMU_MSG`):\n\n```json\n{\"cmd\":\"DANMU_MSG\",\"info\":[[0,1,25,16777215,1553417856,1553414245,0,\"9e539d78\",0,0,0],\"记得存档！\",[3432444,\"喵的叫一声\",0,0,0,10000,1,\"\"],[6,\"日常\",\"奶粉の日常\",35399,5805790,\"\"],[22,0,5805790,\"\u003e50000\"],[\"\",\"\"],0,0,null,{\"ts\":1553417856,\"ct\":\"87255D9C\"}]}\n```\n\n`Welcome` 的数据是这样的\n```json\n{\"cmd\":\"WELCOME\",\"data\":{\"uid\":110208099,\"uname\":\"霸刀宋壹i\",\"is_admin\":false,\"svip\":1}}\n```\n\n各种 `Command` 数据包的结构经常改变, 因此不提供实体类.\n\n由于 `DANMU_MSG` 的数据结构太过意识流, 因此提供了额外的辅助工具来方便地解析它.\n\n`DanmakuMessage` 是一个 `inline class` 请不要对其进行太过复杂的操作.\n\n```kotlin\nonCommandPacket = { _, jsonObject -\u003e\n    val cmd by jsonObject.byString\n    println(\n        if (cmd == \"DANMU_MSG\") {\n            with(DanmakuMessage(jsonObject)) {\n                \"${if (fansMedalInfo.isNotEmpty()) \"[$fansMedalName $fansMedalLevel] \" else \"\"}[UL$userLevel] $nickname: $message\"\n            }\n        } else {\n            jsonObject.toString()\n        }\n    )\n}\n```\n\n输出:\n\n```\n[甜甜天 7] [UL25] czp3009: 233\n```\n\n更多 `Command` 数据包的数据结构详见本项目的 [/record/直播弹幕](record/直播弹幕) 文件夹.\n\n注意, `onPopularityPacket`, `onCommandPacket` 这些回调不能进行耗时操作.\n\n关闭连接\n\n```kotlin\njob.cancel()\n```\n\n## 发送直播弹幕\n在直播间里发送弹幕也非常简单(必须先登陆)\n\n```kotlin\nliveClient.sendMessage(\"我上我也行\").await()\n```\n\n注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回(`code` 为 0).\n\n完全正常返回时(弹幕正确的被发送了), 返回内容中的 `message` 为一个空字符串.\n\n如果不为空字符串, 则表示不完全正常\n\n例如返回内容的 `message` 为 \"msg repeat\" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 确实是 0.\n\n其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.\n\n正常返回时, 就算不完全正常, 客户端也会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).\n\n需要额外判断返回的 `message` 是否为空字符串来确认这条弹幕有没有被正确发送.\n\n# License\nGPL V3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczp3009%2Fbilibili-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fczp3009%2Fbilibili-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczp3009%2Fbilibili-api/lists"}