Git Product home page Git Product logo

bilibili-api's Introduction

Bilibili API JVM 调用库

该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.

使用一台虚拟的 Pixel 2 设备来截取数据包, 一些固定参数可能与真实设备不一致.

使用

compile group: 'com.hiczp', name: 'bilibili-api', version: '0.2.1'

对于 Android 项目, 如需要解析弹幕, 添加

compile group: 'javax.xml.stream', name: 'stax-api' version: last_version
compile group: 'org.codehaus.woodstox', name: 'woodstox-core-asl' version: last_version

技术说明

BilibiliClient 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.

所有调用从这个类开始, 包括登陆以及访问其他各种 API.

使用协程来实现异步, 由于 kotlin coroutines 为编译器实现, 因此并非所有 JVM 语言都能正确调用 suspend 方法.

本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.

BilibiliClient 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 BilibiliClient 实例, 而应该保存其引用.

一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:

runBlocking {
    val bilibiliClient = BilibiliClient().apply {
        login(username, password)
    }
    val myInfo = bilibiliClient.appAPI.myInfo().await()
    val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
}

如果一个请求的返回内容中的 code(code 是 BODY 的内容, 并非 HttpStatus) 不为 0, 将抛出异常 BilibiliApiException, 通过以下代码来获取服务器原始返回的 code:

val code = bilibiliApiException.commonResponse.code

一个错误返回的原始 JSON 如下所示:

{
    "code": -629,
    "message": "用户名与密码不匹配",
    "ts": 1550730464
}

每种不同的 API 在错误时返回的 code 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.

登录和登出

(Bilibili oauth2 v3)

登陆和登出均为异步方法, 需要在协程上下文中执行(接下去不会特地强调这一点).

runBlocking {
    BilibiliClient().run {
        login(username, password)
        logout()
    }
}

login 方法返回一个 LoginResponse 实例, 下次可以直接赋值到没有登陆的 BilibiliClient 实例中来恢复登陆状态.

BilibiliClient().apply {
    this.loginResponse = loginResponse
}

LoginResponse 继承 Serializable, 可被序列化(JVM 序列化).

可能的错误返回有两种:

-629 用户名与密码不匹配
-105 验证码错误

如果仅使用用户名与密码进行登陆并且得到了 -105 的结果, 那么说明需要验证码(通常是由于多次错误的登陆尝试导致的).

原始返回如下所示

{"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}

自行访问 commonResponse.data.obj.url.string 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:

login(username, password, challenge, secCode, validate)

challenge 为本次极验的唯一标识(在一开始给出的 url 中)

validate 为极验返回值

secCode"$validate|jordan"

(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).

注意, BilibiliClient 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误(想要这么做的人一定脑子瓦特了).

登陆后, 可以访问全部 API(注意, 有一些明显不需要登录的 API 也有可能需要登录).

注意, 即使返回code为0也不一定登录成功, 例如

{
  "ts": 1584206212,
  "code": 0,
  "data": {
    "status": 1,
    "url": "https://passport.bilibili.com/mobile/verifytel_h5.html?mid\u003d517548681\u0026tel\u003d156****0364\u0026source\u003d2\u0026keepTime\u003d0\u0026appId\u003d878\u0026subId\u003d0\u0026ticket\u003d1"
  }
}

需要手动判断data.url是否为null, 尽管这种情况不多见

由于各种需要登陆的 API 在未登录时返回的 code 并不统一, 因此没有办法做自动 token 刷新, 自己看着办.

在真实的客户端上, 每次一打开 APP 就会访问个人信息 API来确定 token 是否仍然可用, 这就是 B站 自己的解决方案.

访问 API

不要问文档, 用自动补全(心)来感受. 以下给出几个示例

获取个人信息

(首先要登陆)

val myInfo = bilibiliClient.appAPI.myInfo().await()

返回用户 ID, vip 信息等.

搜索

当我们想看某些内容时, 我们会首先使用搜索功能, 例如

val searchResult = bilibiliClient.appAPI.search(keyword = "刀剑神域").await()

实际上这对应客户端上的 搜索 -> 综合.

如果要搜索番剧则使用 bilibiliClient.appAPI.searchBangumi.

同理, 搜索直播, 用户, 影视, 专栏分别使用 searchLive, searchUser, searchMovie, searchArticle.

所有的搜索都使用 pageNumber 参数来控制翻页(从 1 开始).

获取视频播放地址

获取视频实际播放地址的 API 比较特殊, 被单独分了出来, 示例如下

val videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 72913641).await()

aid 即 av 号, 只能表示视频播放的那个页面, 如果一个视频有多个 p, 那么每个 p 都有单独的 cid.

在 Web 端, URL 通常是这样的

https://www.bilibili.com/video/av44541340/?p=2

实际上就是选择了该 aid 下的第二个 cid(注意, 参数里使用的 cid 不是这个 p 的序号, 它也是一个很长的数字).

简单的来说, aidcid 加在一起才能表示一个视频流(为什么 cid 不能直接表示一个视频我也不知道).

因此无论是获取视频播放地址, 还是获取弹幕列表, 都要同时传入 aidcid.

cid 在哪里获得呢, 如下所示

val view = bilibiliClient.appAPI.view(aid = 41517911).await()

该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.

其中 data.cid 为默认 pcid. data.pages[n].cid 为每个 pcid. 如果只有一个 p 那么说明视频没有分 p.

请求视频地址将访问如下结构的内容

{
    "code": 0,
    "data": {
        "accept_description": [
            "高清 1080P+",
            "高清 1080P",
            "高清 720P",
            "清晰 480P",
            "流畅 360P"
        ],
        "accept_format": "hdflv2,flv,flv720,flv480,flv360",
        "accept_quality": [
            112,
            80,
            64,
            32,
            16
        ],
        "dash": {
            "audio": [
                {
                    "bandwidth": 319173,
                    "base_url": "http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=ks3u&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=33273eaf403739d9f51304509f55589e",
                    "codecid": 0,
                    "id": 30280
                },
                {
                    "bandwidth": 67326,
                    "base_url": "http://upos-hz-mirrorkodou.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30216.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=kodou&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=3d1f9b836430bb8033b2f318faf42f9b",
                    "codecid": 0,
                    "id": 30216
                }
            ],
            "video": [
                {
                    "bandwidth": 376693,
                    "base_url": "http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30015.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=ks3u&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=82bc845bce9f22b731b062bf83fa000f",
                    "codecid": 7,
                    "id": 16
                },
                ...
                {
                    "bandwidth": 2615324,
                    "base_url": "http://upos-hz-mirrorcosu.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30080.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&dynamic=1&gen=playurl&oi=3670888782&os=cosu&platform=android&rate=0&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&uipv=5&um_deadline=1551113319&um_sign=22fef3c0efa0d23388429f6926fad298&upsig=c4768c036beb667ba4648369770f8de8",
                    "codecid": 7,
                    "id": 80
                }
            ]
        },
        "fnval": 16,
        "fnver": 0,
        "format": "flv480",
        "from": "local",
        "quality": 32,
        "result": "suee",
        "seek_param": "start",
        "seek_type": "offset",
        "timelength": 175332,
        "video_codecid": 7,
        "video_project": true
    },
    "message": "0",
    "ttl": 1
}

(由于内容太长, 去除了一部分内容)

注意, 视频下载地址有好几个(以上返回内容中被折叠成了两个), 但是实际上他们都是一样的内容, 只是清晰度不同. data.dash.video.id 实际上代表 data.accept_quality.

视频和音频是分开的, 视频和音频都返回 m4s 文件, 将其合并即可得到完整的 mp4 文件.

data.quality 指默认选择的清晰度, 通常情况下移动网络会自动选择 32, 即 "清晰 480P"(在 data.accept_description 中对应).

对于番剧来说, 也使用 aidcid 来获得播放地址

val bangumiPlayUrl = bilibiliClient.playerAPI.bangumiPlayUrl(aid = 42714241, cid = 74921228).await()

返回内容差不多是一个原理, 这里就不赘述了.

如何获得番剧的 aidcid 呢. 我们都知道, 实际上番剧那个页面的唯一标识是 "季", 同一个番的不同 "季" 其实是不同的东西.

我们在番剧搜索页面可以得到番剧的 season, 这代表了一个番剧的某一季的页面.

然后我们用 season 来打开番剧页面.

val season = bilibiliClient.mainAPI.season(seasonId = 25617).await()

返回值中的 result.seasons[n].season_id 为该番所有季的 id(包含用来作为查询条件的 seasonId).

该 API 还可以用 episodeId 作为查询条件, 即以集为条件打开一个番剧页面(会跳转到对应的季).

返回值中的 result.episodes 包含了当前所选择的季的全部集的 aidcid.

查看视频下面的评论

看完了视频当然要看一下傻吊网友都在说些什么. 使用以下 API 获取一个视频的评论.

val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()

这里的 oidaid(其他一些 API 中 oid 也可能指 cid 详见方法上面的注释).

评论是不分 p 的, 所有评论都是在一起的.

可以额外使用一个 next 参数来指定返回的起始楼层(即翻页).

楼层是越翻越小的, 所以 next 也要越来越小.

看到了傻吊网友们的评论是不够的, 我们还想看到杠精与其隔着屏幕对喷的场景, 因此我们要获取评论的子评论, 即评论的评论

val childReply = bilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()

其中的 root 表示根评论的 id.

每个评论都有自己的 replyId, parentId 以及 rootId.

假如一个人在一个评论的子评论里发布了一个评论并且 at 了其他人发的评论, 那么其 parentId 是他所 at 的评论, 其 rootId 为所在的根评论.

如果不满足对应的层级逻辑关系(例如本身为根评论), parentIdrootId 可能为 0.

用额外的 minId 参数来指定返回的起始子楼层.

注意, 子楼层是越翻越大的.

如果一个根评论下面有很多个喷子在互喷, 会导致看不清, 客户端上有一个按钮 "查看对话" 就是解决这个问题的.

val chatList = bilibiliClient.mainAPI.chatList(oid = 34175504, root = 1136310360, dialog = 1136351035).await()

root 为根评论 ID, dialog 为父评论 ID.

minFloor 控制分页, 原理同上.

番剧下面的评论用一样的方式获取.

获得一个视频的弹幕

看评论自然不够刺激, 我们想看到弹幕!

获取弹幕非常简单

val danmakuFile = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await()

弹幕是一个文件, 可能非常大, 里面是二进制内容.

为了解析弹幕, 我们要用到另一个类

val (flagMap, danmakuList) = DanmakuParser.parser(danmakuFile.byteStream())

flagMap 类型为 Map<Long, Int> 键和值分别表示 弹幕ID 与 弹幕等级.

弹幕等级在区间 [1, 10] 内, 低于客户端设置的 "弹幕云屏蔽等级" 的弹幕将不会显示出来.

danmakuList 类型为 List<Danmaku>, 内含所有解析得到的弹幕.

使用以下代码来输出全部弹幕的内容

danmakuList.forEach {
    println(it.content)
}

注意, 弹幕的解析是惰性的, danmakuList 是一个 Sequence. 如果同时持有很多未用完的 danmakuList 的引用可能会造成大量内存浪费.

客户端的弹幕屏蔽设置是对弹幕中的 user 属性做的. 而实际上 danmaku.user 是一个字符串.

这个字符串是 用户ID 的 CRC32 的校验和.

众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.

在另一方面, 通过这个 CRC32 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法完全确定一条弹幕到底是哪个用户发送的.

如果想获得发送这条弹幕的所有可能的用户的 ID, 可以通过以下方法:

val possibleUserIds = danmaku.calculatePossibleUserIds()

返回一个 List<Int>, 内容为所有可能的用户 ID(至少有一个).

注意, 第一次使用 CRC反查 功能将花费大约 300ms 来生成彩虹表, 如果想手动初始化请使用以下代码

Crc32Cracker

(Crc32Cracker 是一个惰性初始化的单例)

通常情况下, 一次 CRC反查 耗时大约 1ms.

由于这是一个比较耗时的操作, 请不要每条弹幕都如此操作(相比较 6000 条弹幕的解析只需要 150ms).

番剧的弹幕同理.

发送视频弹幕

光看不发憋着慌, 我们来发送一条视频弹幕:

bilibiliClient.mainAPI.sendDanmaku(aid = 40675923, cid = 71438168, progress = 2297, message = "2333").await()

其中 progress 是播放器时间, 其他观众将看到你的弹幕在视频的此处出现, 单位为毫秒.

message 应该是有长度限制的, 但是没有测过.

如果不确定视频的长度, 需要从视频播放地址的 API 中的 data.timelength 来获得, 单位也是毫秒.

获取直播弹幕

刚进入直播间时, 立即看到的十条弹幕实际上是最近的历史弹幕, 通过以下方式来获取

bilibiliClient.liveAPI.roomMessage(roomId).await()

接下来的弹幕都是实时弹幕, 直播间实时弹幕通过 Websocket 来推送.

val job = bilibiliClient.liveClient(roomId = 3) {
    onConnect = {
        println("Connected")
    }

    onPopularityPacket = { _, popularity ->
        println("Current popularity: $popularity")
    }

    onCommandPacket = { _, jsonObject ->
        println(jsonObject)
    }

    onClose = { _, closeReason ->
        println(closeReason)
    }
}.launch()

服务器推送的 Message 有两种, 一种是 人气值 数据, 另一种是 Command 数据.

Command 数据包用于控制客户端渲染何种内容. 弹幕, 送礼, 系统公告等全部都是由 Command 数据包控制的, 其本体为一个 JsonObject.

例如一个弹幕数据是这样的(cmd 字段的值为 DANMU_MSG):

{"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,">50000"],["",""],0,0,null,{"ts":1553417856,"ct":"87255D9C"}]}

Welcome 的数据是这样的

{"cmd":"WELCOME","data":{"uid":110208099,"uname":"霸刀宋壹i","is_admin":false,"svip":1}}

各种 Command 数据包的结构经常改变, 因此不提供实体类.

由于 DANMU_MSG 的数据结构太过意识流, 因此提供了额外的辅助工具来方便地解析它.

DanmakuMessage 是一个 inline class 请不要对其进行太过复杂的操作.

onCommandPacket = { _, jsonObject ->
    val cmd by jsonObject.byString
    println(
        if (cmd == "DANMU_MSG") {
            with(DanmakuMessage(jsonObject)) {
                "${if (fansMedalInfo.isNotEmpty()) "[$fansMedalName $fansMedalLevel] " else ""}[UL$userLevel] $nickname: $message"
            }
        } else {
            jsonObject.toString()
        }
    )
}

输出:

[甜甜天 7] [UL25] czp3009: 233

更多 Command 数据包的数据结构详见本项目的 /record/直播弹幕 文件夹.

注意, onPopularityPacket, onCommandPacket 这些回调不能进行耗时操作.

关闭连接

job.cancel()

发送直播弹幕

在直播间里发送弹幕也非常简单(必须先登陆)

liveClient.sendMessage("我上我也行").await()

注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回(code 为 0).

完全正常返回时(弹幕正确的被发送了), 返回内容中的 message 为一个空字符串.

如果不为空字符串, 则表示不完全正常

例如返回内容的 message 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 code 确实是 0.

其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.

正常返回时, 就算不完全正常, 客户端也会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).

需要额外判断返回的 message 是否为空字符串来确认这条弹幕有没有被正确发送.

License

GPL V3

bilibili-api's People

Contributors

czp3009 avatar duzhaokun123 avatar nekosunflower avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bilibili-api's Issues

侧拉抽屉 -> 直播中心 -> 获奖记录 的数据收集

获奖记录只保留三个月, 而有获奖记录的人又很少, 所以这个API 在获奖记录不为空时到底会返回什么内容尚不明确.

如果您有获奖记录(在 APP 里打开 获奖记录, 里面非空), 可以通过以下方式来帮助本项目的开发.

运行本项目的测试用例, 并提供其中的 GetAwardsTest 的控制台输出.

首先克隆本项目的源代码, 然后设定用户名和密码并进行测试(见 README 中的 测试 一节), 就可以看到输出了.

运行本项目需要 Java8+, Gradle4.5+

请教关于接口的使用

/live/LiveService.java里面有以下这一条
/**
* 获取直播间信息
* 登录后访问该 API 将在服务器新增一条直播间观看历史
*
* 2018-05-11 现在用假的房间 ID 也能获得正确的信息
*
* @param roomId 房间号
*/
@get("AppRoom/index")
Call getRoomInfo(@query("room_id") long roomId);

请问下这个的url是多少?最近在写一个B站的脚本,用不了你的类,只能自己写

带验证码登录返回-2001

测试代码见下,第二个login总是会抛出-2001的错误。是不是我的secCode构造有误?

fun main(args: Array<String>) = runBlocking {
    val bilibiliClient = BilibiliClient()
    try {
        val login_res = bilibiliClient.login("XXX", "YYY")
    } catch (exp: BilibiliApiException) {
        println(format("Login failed with code %d", exp.commonResponse.code))
        if (exp.commonResponse.code == -105) {
            println(format("Needs verification: %s", exp.commonResponse.data!!["url"].asString))
            val re = Regex("challenge=([0-9a-zA-Z]+)")
            val challenge = re.find(exp.commonResponse.data!!["url"].asString)!!.groupValues[1]
            print("Please paste the validate result: ")
            val validate = readLine()!!
            try {
                val login_res = bilibiliClient.login("XXX", "YYY", challenge, "$validate|jordan", validate)
            } catch (exp: BilibiliApiException) {
                println(format("2nd login failed with code %d", exp.commonResponse.code))
             }
        }
    }
}

请求提供说明文档

github上还在更新的基本上只有大佬的这个项目了,B站官网也没有API文档了,请求大佬做个说明文档,谢谢!

开发 Bilibili Web API

已经实现通过 SSO API 获取可用于 Web API 调用的 cookies.

使用得到的 cookies 就可以使用全部的 B站 Web API.

一些内容和功能在 Android APP 上不存在, 例如奖励领取(双端观看直播奖励等), 所以 Web API 可能是必要的.

于是新一波的 API 分析又开始了(躺).

项目文档也不知道该怎么写(扶额).

README 更新了一些编码规范, 诚邀合作开发者.

没看懂send message的api

这里的post的内容,sign加密,url都没找到。。。
实在是看不懂了
求助
可否直接把代码解析出来,我只需要实现这一个功能

提交一点api 2333

GET https://api.live.bilibili.com/appUser/myTitleList
access_key = 用户accessToken

获取用户拥有的所有头衔
返回

@SerializedName("code")
public int code;
@SerializedName("message")
public String message;
@SerializedName("data")
public MyTitlesEntity data;

public static class MyTitlesEntity {
	@SerializedName("list")
	public List<TitleEntity> list;

	public static class TitleEntity {
		@SerializedName("uid")
		public int uid;
		@SerializedName("had")
		public boolean hasTitle;
		@SerializedName("title")
		public String titleID;
		@SerializedName("status")
		public int status;
		@SerializedName("activity")
		public String activity;
		@SerializedName("score")
		public int scoreLevelup;
		@SerializedName("title_pic")
		public TitlePicEntity pic;

		public static class TitlePicEntity {
			@SerializedName("id")
			public String id;
			@SerializedName("img")
			public String imageUrl;
			@SerializedName("width")
			public int width;
			@SerializedName("height")
			public int height;
		}
	}
}

POST https://api.live.bilibili.com/AppExchange/silver2coin
access_key = 用户accessToken

银瓜子换硬币
返回code=403则当日已经换过(每天可换一个硬币)
返回数据仅ts. code和message

GET https://api.live.bilibili.com/AppUser/wearTitle
title = 头衔id
access_key = 用户accessToken

佩戴头衔
头衔id为获取用户所有头衔返回的data - list - title
头衔不存在返回code=-400
返回数据仅ts. code和message

(自动带可升级头衔签到,签完换回来美滋滋2333)

小电视抽奖, 丰收庆典, 新春抽奖 等的执行过程

在弹幕推送 socket 中, 小电视抽奖通知对应 SYS_MSG, 数据包差不多如下

{
  "cmd": "SYS_MSG",
  "msg": "\u3010\u5e7d\u5c0f\u591c\u5929\u5c0f\u52ab\u3011:?\u5728\u76f4\u64ad\u95f4:?\u3010392\u3011:?\u8d60\u9001 \u5c0f\u7535\u89c6\u4e00\u4e2a\uff0c\u8bf7\u524d\u5f80\u62bd\u5956",
  "msg_text": "\u3010\u5e7d\u5c0f\u591c\u5929\u5c0f\u52ab\u3011:?\u5728\u76f4\u64ad\u95f4:?\u3010392\u3011:?\u8d60\u9001 \u5c0f\u7535\u89c6\u4e00\u4e2a\uff0c\u8bf7\u524d\u5f80\u62bd\u5956",
  "rep": 1,
  "styleType": 2,
  "url": "http:\/\/live.bilibili.com\/392",
  "roomid": 392,
  "real_roomid": 71084,
  "rnd": 44332151,
  "tv_id": "29349"
}

丰收庆典以及新春抽奖等普通抽奖对应 SYS_GIFT

{
  "cmd": "SYS_GIFT",
  "msg": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01",
  "msg_text": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01",
  "tips": "sakamakiryoryo\u5728\u76f4\u64ad\u95f471084\u5f00\u542f\u4e86\u4e30\u6536\u796d\u5178\uff0c\u4e00\u8d77\u6765\u5206\u4eab\u6536\u83b7\u7684\u798f\u5229\u5427\uff01",
  "url": "http:\/\/live.bilibili.com\/71084",
  "roomid": 71084,
  "real_roomid": 71084,
  "giftId": 102,
  "msgTips": 0
}

ACTIVITY_EVENT 带有某种活动的进度信息

{
  "cmd": "ACTIVITY_EVENT",
  "data": {
    "keyword": "newspring_2018",
    "type": "cracker",
    "limit": 300000,
    "progress": 158912
  }
}

小电视和普通抽奖的通知都会带有一个 URL, 在 web 上进入这个 URL, 点击悬浮窗即可抽奖.

尚不明确 Android 上看直播时有没有 SYS_MSG 和 SYS_GIFT 的提示(印象中好像没见到过).

尚不明确 Android 上参与抽奖的 API(是一个 Restful API, 不是发弹幕, 发弹幕只是副作用)(不太清楚 Android 上能不能抽奖).

抽奖之后开奖有两种, 一种是 socket 内直接推送的大奖获得者信息(例如 TV_END)(不明确除了小电视抽奖, 其他普通抽奖会不会有类似的大奖推送).

另一种是通过 Restful API 拉取的奖励(参与抽奖至少有辣条, 所以人人都有), 这在 Android 上的 API 是怎么样的也不明确(如果 Android 不能抽奖的话那这个 API 也肯定是没有的).

以上问题尚未解决, 先留存案.

player-loader-1.8.2.min.js 有写live sub 消息格式

* @param shortTag 一种 tag, 如果是非 command 数据包则为 1, 否则为 0, short 类型

e[e.WS_OP_HEARTBEAT = 2] = "WS_OP_HEARTBEAT",
e[e.WS_OP_HEARTBEAT_REPLY = 3] = "WS_OP_HEARTBEAT_REPLY",
e[e.WS_OP_MESSAGE = 5] = "WS_OP_MESSAGE",
e[e.WS_OP_USER_AUTHENTICATION = 7] = "WS_OP_USER_AUTHENTICATION",
e[e.WS_OP_CONNECT_SUCCESS = 8] = "WS_OP_CONNECT_SUCCESS",

e[e.WS_PACKAGE_HEADER_TOTAL_LENGTH = 16] = "WS_PACKAGE_HEADER_TOTAL_LENGTH",
e[e.WS_PACKAGE_OFFSET = 0] = "WS_PACKAGE_OFFSET",
e[e.WS_HEADER_OFFSET = 4] = "WS_HEADER_OFFSET",
e[e.WS_VERSION_OFFSET = 6] = "WS_VERSION_OFFSET",
e[e.WS_OPERATION_OFFSET = 8] = "WS_OPERATION_OFFSET",
e[e.WS_SEQUENCE_OFFSET = 12] = "WS_SEQUENCE_OFFSET",
e[e.WS_BODY_PROTOCOL_VERSION = 1] = "WS_BODY_PROTOCOL_VERSION",
e[e.WS_HEADER_DEFAULT_VERSION = 1] = "WS_HEADER_DEFAULT_VERSION",
e[e.WS_HEADER_DEFAULT_OPERATION = 1] = "WS_HEADER_DEFAULT_OPERATION",
e[e.WS_HEADER_DEFAULT_SEQUENCE = 1] = "WS_HEADER_DEFAULT_SEQUENCE",

WS_BODY_PROTOCOL_VERSION_NORMAL: 0,
WS_BODY_PROTOCOL_VERSION_DEFLATE: 2,

protover: parseInt(n.protover, 10) || i.a.WS_BODY_PROTOCOL_VERSION_NORMAL
  • 当前从电脑网页端整理的
发送方 总长, 4 head长, 2 version, 2 operation, 4 sequence, 4 body
客户端 16 1 7 1 USER_AUTHENTICATION, 进入房间
有个protover(Protocol Version)参数猜测是(指定服务端优先以什么格式给我发消息,0=NORMAL,2=DEFLATE,1=不知道,但貌似已失效)
服务端 16 1 8 1 CONNECT_SUCCESS, 进房成功
客户端 16 1 2 1 HEARTBEAT, 心跳, body不填都行
服务端 16 1 3 1 HEARTBEAT_REPLY, 心跳响应, body=人气(int)
服务端 16 2 5 0 MESSAGE, 消息, body=deflate压缩
└─ 16 0 5 0 解压后同下
服务端 16 0 5 0 MESSAGE, 消息, body=字符串(utf8)
  • 如果把head+body叫做一个Package,我记得之前收到一次消息可能包含多个Package的
    • 当前看,deflate解压后,也可能包含多个Package的

在android上运行返回-400 Bad request

在使用您的api开发的过程中碰到一点问题,因此请教一下

首先,为了能在android上运行,我修改了部分代码(将lambda改为匿名类,以及自行实现部分java8新增的api),我可以确认这部分代码不会引起问题

我遇到的问题是
在我的电脑(ubuntu 14.04.5)上,经测试一切正常
但在安卓上同样的代码登录时返回了code:-400, message:"请求出错,请重试"

经过抓包对比,我并没能找到问题....看起来两个post请求没什么不对

请问这种情况有可能是什么造成的?

添加依赖后出错。。

添加后出现了这个。。。请教下大佬还有其他方式使用B站api吗。。andorid新人想练练手 android.support.multidex.MultiDexApplication: java.lang.ClassNotFoundException

DanmakuParser 在 Android 9 上出现错误(ERROR)

android 版本: LineageOS 16 (android 9)
bilibili-api 版本: 0.2.0

E/AndroidRuntime: FATAL EXCEPTION: Thread-15
    Process: com.duzhaokun123.bilibilihd, PID: 24430
    java.lang.NoClassDefFoundError: Failed resolution of: Ljavax/xml/stream/XMLInputFactory;
        at com.hiczp.bilibili.api.danmaku.DanmakuParser.parse(DanmakuParser.kt:75)
        at com.duzhaokun123.bilibilihd.ui.play.PlayActivity$3.run(PlayActivity.java:363)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "javax.xml.stream.XMLInputFactory" on path: DexPathList[[zip file "/data/app/com.duzhaokun123.bilibilihd-EWRpYYgLTKXGbRV_tVoFSQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.duzhaokun123.bilibilihd-EWRpYYgLTKXGbRV_tVoFSQ==/lib/arm64, /data/app/com.duzhaokun123.bilibilihd-EWRpYYgLTKXGbRV_tVoFSQ==/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at com.hiczp.bilibili.api.danmaku.DanmakuParser.parse(DanmakuParser.kt:75) 
        at com.duzhaokun123.bilibilihd.ui.play.PlayActivity$3.run(PlayActivity.java:363) 

您好,我这里在你的model里加了根据roomId去“打开直播”和“关闭直播”,能否提到你的分支上?

具体代码如下
` /**
* 获取直播分区列表,多层
*
* @return 成功时code为0
*/
@get("room/v1/Area/getList?show_pinyin=1")
Call getLiveAreaList();

/**
 * 获取直播间信息(主要是拿roomId)
 *
 * @return LiveInfoEntity
 */
@GET("i/api/liveinfo")
Call<LiveInfoEntity> getLiveInfo();

/**
 * 根据roomId 获取推流的地址
 *
 * @param roomId
 * @return LiveRoomStreamInfoEntity
 */
@GET("live_stream/v1/StreamList/get_stream_by_roomId")
Call<LiveRoomStreamInfoEntity> getStreamByRoomId(@Query("room_id") String roomId);

/**
 * 开始直播
 *
 * @param roomId
 * @param platform 固定PC
 * @param areaId 子分区id 譬如学习的id为27
 * @param csrfToken (csrfToken是cookie里的bili_jct cookie这个的值)
 * @return StartLiveEntity
 */
@POST("room/v1/Room/startLive")
Call<StartLiveEntity> startLive(@Query("room_id") String roomId, @Query("platform") String platform,
                                @Query("area_v2") String areaId, @Query("csrf_token") String csrfToken);

/**
 * 停止直播
 *
 * @param roomId
 * @param platform
 * @param areaId
 * @param csrfToken
 * @return
 */
@POST("room/v1/Room/stopLive")
Call<StopLiveEntity> stopLive(@Query("room_id") String roomId, @Query("platform") String platform,
                              @Query("area_v2") String areaId, @Query("csrf_token") String csrfToken);`

获取满级用户的主页时抛出异常

E/AndroidRuntime: FATAL EXCEPTION: Thread-4
    Process: com.duzhaokun123.bilibilihd, PID: 18272
    java.util.concurrent.ExecutionException: com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "--"
        at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:359)
        at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1921)
        at com.duzhaokun123.bilibilihd.pBilibiliApi.app.PAppAPI.space(PAppAPI.kt:38)
        at com.duzhaokun123.bilibilihd.ui.UserSpaceActivity$2.run(UserSpaceActivity.java:73)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "--"
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:227)
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:217)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:39)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27)
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:223)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:121)
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:174)
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764) 
     Caused by: java.lang.NumberFormatException: For input string: "--"
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
        at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.lang.Double.parseDouble(Double.java:538)
        at com.google.gson.stream.JsonReader.nextInt(JsonReader.java:1201)
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:225)
        at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:217) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129) 
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220) 
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:39) 
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27) 
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:223) 
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:121) 
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:174) 
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) 
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
        at java.lang.Thread.run(Thread.java:764)

bug 登录时未抛出异常

版本 0.1.2

java.lang.NullPointerException: Attempt to invoke virtual method 'long com.hiczp.bilibili.api.passport.model.LoginResponse$Data$TokenInfo.getMid()' on a null object reference
	at com.hiczp.bilibili.api.passport.model.LoginResponse.getUserId(LoginResponse.kt:57)
	at com.duzhaokun123.bilibilihd.ui.LoginActivity$1$1.run(LoginActivity.java:70)

测试账号 15671920364 密码 bb123456 这个账号是在B站公开的 https://www.bilibili.com/video/av95946645/?redirectFrom=h5
但是被封禁

查看小电视

 * 查看可用的小电视抽奖
 *
 * @param roomId 房间号
 * @return 当目标房间没有可用的小电视抽奖时返回 -400
 */
@GET("AppSmallTV/index")
Call<AppSmallTVEntity> getAppSmallTV(@Query("roomid") long roomId);

我使用python访问这个api获取的信息是这样的,不知道怎么看具体的小电视编号
{'code': 0, 'msg': 'OK', 'message': 'OK', 'data': {'lastid': 0, 'join': [], 'unjoin': []}}

关于VcAPI中“意义不明”的dynamic_num接口的意义

我为什么要闲的蛋疼去抓动态接口
综述:/dynamic_svr/v1/dynamic_svr/dynamic_num接口是关于动态的一系列接口的整合体。
结合web版,目前抓到了这几个:

获取整个动态页

此时的query:
uid:用户uid
type:动态的类型,如下图所示
image
各动态栏目有各自固定的type

栏目 type参数的值
全部 268435455
追番 512
专栏 64
小视频 16
投稿视频 8
图片 2

返回的是一个如下的描述动态页面内容的json。

{
    "code": 0,
    "msg": "",
    "message": "",
    "data": {
        "new_num": 0,
        "exist_gap": 1,
        "open_rcmd": 1,
        "archive_up_num": 2,
        "up_num": {
            "archive_up_num": 2,
            "bangumi_up_num": 1
        },
        "extra_flag": {
            "great_dynamic": 1
        },
        "cards": [{
            "desc": {
                "uid": __userid,
                "type": 2,
                "rid": __some_rid,
                "acl": 0,
                "view": 1255,
                "repost": 0,
                "like": 27,
                "is_liked": 0,
                "dynamic_id": __some_dynamic_id,
                "timestamp": __ts,
                "pre_dy_id": 0,
                "orig_dy_id": 0,
                "orig_type": 0,
                "user_profile": {
                    "info": {
                        "uid": uid,
                        "uname": "username",
                        "face": "some_face_uri"
                    },
                    "card": {
                        "official_verify": {
                            "type": 0,
                            "desc": "some_description"
                        }
                    },
                    "vip": {
                        "vipType": 2,
                        "vipDueDate": 1584892800000,
                        "dueRemark": "",
                        "accessStatus": 0,
                        "vipStatus": 1,
                        "vipStatusWarn": "",
                        "themeType": 0
                    },
                    "pendant": {
                        "pid": 0,
                        "name": "",
                        "image": "",
                        "expire": 0
                    },
                    "rank": "10000",
                    "sign": "some_sign",
                    "level_info": {
                        "current_level": 6,
                        "current_min": 0,
                        "current_exp": 0,
                        "next_exp": "0"
                    }
                },
                "stype": 0,
                "r_type": 1,
                "inner_id": 0,
                "status": 1,
                "dynamic_id_str": "str"
            },
            "card": "some card json",
            "extend_json": "some extend json"
        }...],
        "attentions": {
            "uids": [2...],
            "bangumis": [{
                "season_id": 687,
                "type": 1
            }...]
        },
        "_gt_": 0
    }
}

获取增量

此时的query:
uid:用户uid
type:动态类型,跟随栏目的下列选择有固定的值
rsp_type:1 意义应该是指定response的类型为只返回动态更新增量
[可空] current_dynamic_id:当前的最新动态id
[可空] update_num_dy_id:获取这个动态id后的动态。

后两个参数一般置为一样同时传递。
返回参考:

{
    "code": 0,
    "msg": "",
    "message": "",
    "data": {
        "new_num": 0,
        "update_num": 1,
        "_gt_": 0
    }
}

相对于前一个response类型,data中少了cards和attentions。

关于图一中的“热门”动态

调用的是同目录下的recommend接口,参数接受uid和page,返回response结构略有不同。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.