实现视频弹幕的获取和解析

This commit is contained in:
czp3009 2019-02-26 14:58:14 +08:00
parent 174fd92be8
commit 7c6297f6d2
70 changed files with 560 additions and 862 deletions

View File

@ -138,7 +138,7 @@ val view = bilibiliClient.appAPI.view(aid = 41517911).await()
该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.
其中 `data.cid` 为默认 `p``cid`. `data.pages[n].cid` 为每个 `p``cid`. 如果没有分 `p` 则使用外层那个 `cid` 来请求视频地址.
其中 `data.cid` 为默认 `p``cid`. `data.pages[n].cid` 为每个 `p``cid`. 如果只有一个 `p` 那么说明视频没有分 `p`.
请求视频地址将访问如下结构的内容
@ -254,6 +254,10 @@ val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
评论是不分 `p` 的, 所有评论都是在一起的.
可以额外使用一个 `next` 参数来指定返回的起始楼层(即翻页).
楼层是越翻越小的, 所以 `next` 也要越来越小.
看到了傻吊网友们的评论是不够的, 我们还想看到杠精与其隔着屏幕对喷的场景, 因此我们要获取评论的子评论, 即评论的评论
```kotlin
@ -266,12 +270,52 @@ val childReply = bilibiliClient.mainAPI.childReply(oid = 16622855, root = 140560
(子评论原理上可以再有子评论, 但是 B站 在逻辑上只有一层子评论)
番剧下面的评论用一样的方式获取, 下同.
用额外的 `minId` 参数来指定返回的起始子楼层.
注意, 子楼层是越翻越大的.
番剧下面的评论用一样的方式获取.
## 获得一个视频的弹幕
看评论自然不够刺激, 我们想看到弹幕!
//TODO
获取弹幕非常简单
```kotlin
val danmakuFile = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await()
```
弹幕是一个文件, 可能非常大, 里面是二进制内容.
为了解析弹幕, 我们要用到另一个类
```kotlin
val (flagMap, danmakuList) = DanmakuParser.parser(danmakuFile.byteStream())
```
`flagMap` 类型为 `Map<Long, Int>` 键和值分别表示 弹幕ID 与 弹幕等级.
弹幕等级在区间 \[1, 10\] 内, 低于客户端设置的 "弹幕云屏蔽等级" 的弹幕将不会显示出来.
`danmakuList` 类型为 `List<Danmaku>`, 内含所有解析得到的弹幕.
使用以下代码来输出全部弹幕的内容
```kotlin
danmakuList.forEach {
println(it.content)
}
```
客户端的弹幕屏蔽设置是对弹幕中的 `user` 属性做的. 而实际上 `danmaku.user` 是一个字符串.
这个字符串是 用户ID 的 `CRC32` 的校验和.
众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.
在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法确定一条弹幕到底是哪个用户发送的.
番剧的弹幕同理.
# License
GPL V3

View File

@ -1,7 +1,7 @@
buildscript {
ext {
kotlin_version = '1.3.21'
kotlin_coroutines_version= '1.1.1'
kotlin_coroutines_version = '1.1.1'
jvm_target = JavaVersion.VERSION_1_8
}
@ -65,6 +65,12 @@ dependencies {
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.13.1'
}
//utils
dependencies {
// https://mvnrepository.com/artifact/commons-io/commons-io
compile group: 'commons-io', name: 'commons-io', version: '2.6'
}
//unit test
dependencies {
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter

View File

@ -1,9 +0,0 @@
{
"cmd": "ACTIVITY_EVENT",
"data": {
"keyword": "newspring_2018",
"type": "cracker",
"limit": 300000,
"progress": 158912
}
}

View File

@ -1,4 +0,0 @@
{
"cmd": "CHANGE_ROOM_INFO",
"background": "http://static.hdslb.com/live-static/images/bg/4.jpg"
}

View File

@ -1,13 +0,0 @@
{
"cmd": "COMBO_END",
"data": {
"uname": "打死安迷修-雷狮",
"r_uname": "Jinko_神子",
"combo_num": 1,
"price": 200,
"gift_name": "flag",
"gift_id": 20002,
"start_time": 1527929335,
"end_time": 1527929335
}
}

View File

@ -1,11 +0,0 @@
{
"cmd": "COMBO_SEND",
"data": {
"uid": 33012231,
"uname": "我就是讨厌你这样",
"combo_num": 3,
"gift_name": "凉了",
"gift_id": 20010,
"action": "赠送"
}
}

View File

@ -1,5 +0,0 @@
{
"cmd": "CUT_OFF",
"msg": "禁播游戏",
"roomid": 8446134
}

View File

@ -1,72 +0,0 @@
{
"info": [
//
[
0,
//pool
1,
//mode
25,
//fontSize
16777215,
//color
1510498713,
//
"1510498712",
//(Android , )
0,
"8a0f75dc",
0
],
"网易云音乐库在当前直播间已停留0天0时39分41秒",
//
[
39042255,
// ID
"夏沫丶琉璃浅梦",
//
0,
//
1,
// VIP
0,
// svip
10000,
1,
""
],
//(, JsonArray )
[
13,
//
"夏沫",
//
"乄夏沫丶",
//
"1547306",
//
16746162,
""
],
//
[
41,
//
0,
16746162,
6603
//
],
//(, , )
[
"title-131-1",
"title-131-1"
],
0,
0,
{
"uname_color": ""
}
],
"cmd": "DANMU_MSG"
}

View File

@ -1,47 +0,0 @@
{
"info": [
[
0,
1,
25,
16777215,
1510498713,
"1510498712",
0,
"8a0f75dc",
0
],
"网易云音乐库在当前直播间已停留0天0时39分41秒",
[
39042255,
"夏沫丶琉璃浅梦",
0,
1,
0,
10000,
1,
""
],
[
13,
"夏沫",
"乄夏沫丶",
"1547306",
16746162,
""
],
[
41,
0,
16746162,
6603
],
[],
0,
0,
{
"uname_color": ""
}
],
"cmd": "DANMU_MSG"
}

View File

@ -1,16 +0,0 @@
{
"cmd": "ENTRY_EFFECT",
"data": {
"id": 3,
"uid": 9359447,
"target_id": 275592903,
"show_avatar": 1,
"copy_writing": "欢迎 \u003c%藏拙当成玉%\u003e 进入房间",
"highlight_color": "#FFF100",
"basemap_url": "http://i0.hdslb.com/bfs/live/d208b9654b93a70b4177e1aa7e2f0343f8a5ff1a.png",
"effective_time": 1,
"priority": 50,
"privilege_type": 0,
"face": "http://i1.hdslb.com/bfs/face/12cb1ea6eea79667e3fb722bbd8995bb96f4cd6f.jpg"
}
}

View File

@ -1,8 +0,0 @@
{
"roomid": 234024,
"cmd": "EVENT_CMD",
"data": {
"event_type": "flower_rain-16915",
"event_img": "http://s1.hdslb.com/bfs/static/blive/live-assets/mobile/activity/lover_2018/raffle.png"
}
}

View File

@ -1,10 +0,0 @@
{
"cmd": "GUARD_BUY",
"data": {
"uid": 4561799,
"username": "微笑The迪妮莎",
"guard_level": 1,
"num": 1
},
"roomid": "5279"
}

View File

@ -1,25 +0,0 @@
{
"cmd": "GUARD_LOTTERY_START",
"data": {
"id": 396410,
"roomid": 56998,
"message": "ちゆき蝙蝠公主 在【56998】购买了舰长请前往抽奖",
"type": "guard",
"privilege_type": 3,
"link": "https://live.bilibili.com/56998",
"lottery": {
"id": 396410,
"sender": {
"uid": 11206312,
"uname": "ちゆき蝙蝠公主",
"face": "http://i0.hdslb.com/bfs/face/06d0d58131100acf13d75d3c092b1a58d41b0129.jpg"
},
"keyword": "guard",
"time": 1200,
"status": 1,
"mobile_display_mode": 2,
"mobile_static_asset": "",
"mobile_animation_asset": ""
}
}
}

View File

@ -1,4 +0,0 @@
{
"cmd": "GUARD_MSG",
"msg": "乘客 :?想不想joice:? 成功购买1313366房间总督船票1张欢迎登船"
}

View File

@ -1,4 +0,0 @@
{
"cmd": "LIVE",
"roomid": "1110317"
}

View File

@ -1,29 +0,0 @@
{
"cmd": "NOTICE_MSG",
"full": {
"head_icon": "",
"is_anim": 1,
"tail_icon": "",
"background": "#33ffffff",
"color": "#33ffffff",
"highlight": "#33ffffff",
"border": "#33ffffff",
"time": 10
},
"half": {
"head_icon": "",
"is_anim": 0,
"tail_icon": "",
"background": "#33ffffff",
"color": "#33ffffff",
"highlight": "#33ffffff",
"border": "#33ffffff",
"time": 8
},
"roomid": "11415406",
"real_roomid": "0",
"msg_common": "恭喜\u003c%汤圆老师%\u003e获得大奖\u003c%23333x银瓜子%\u003e, 感谢\u003c%林发发爱林小兔%\u003e的赠送",
"msg_self": "恭喜\u003c%汤圆老师%\u003e获得大奖\u003c%23333x银瓜子%\u003e, 感谢\u003c%林发发爱林小兔%\u003e的赠送",
"link_url": "http://live.bilibili.com/0",
"msg_type": 4
}

View File

@ -1,17 +0,0 @@
{
"cmd": "PK_AGAIN",
"pk_id": 60672,
"pk_status": 400,
"data": {
"new_pk_id": 60678,
"init_id": 10817769,
"match_id": 1489926,
"escape_all_time": 10,
"escape_time": 10,
"is_portrait": false,
"uname": "穆阿是给你的mua",
"face": "http://i0.hdslb.com/bfs/face/07fa1057b60afe74cdd477f123c6ccf460ee8f2c.jpg",
"uid": 38105366
},
"roomid": 1489926
}

View File

@ -1,6 +0,0 @@
{
"pk_status": 400,
"pk_id": 60672,
"cmd": "PK_CLICK_AGAIN",
"roomid": 1489926
}

View File

@ -1,10 +0,0 @@
{
"cmd": "PK_END",
"pk_id": 8797,
"pk_status": 400,
"data": {
"init_id": 8049573,
"match_id": 1409458,
"punish_topic": "惩罚:模仿面筋哥"
}
}

View File

@ -1,4 +0,0 @@
{
"cmd": "PK_INVITE_FAIL",
"pk_invite_status": 1100
}

View File

@ -1,11 +0,0 @@
{
"cmd": "PK_INVITE_INIT",
"pk_invite_status": 200,
"invite_id": 408,
"face": "http://i0.hdslb.com/bfs/face/e1ad4df39e95180e990fdd565b216662bdb2503c.jpg",
"uname": "Tocci椭奇",
"area_name": "视频聊天",
"user_level": 24,
"master_level": 31,
"roomid": 883802
}

View File

@ -1,4 +0,0 @@
{
"cmd": "PK_INVITE_SWITCH_CLOSE",
"roomid": 1938890
}

View File

@ -1,4 +0,0 @@
{
"cmd": "PK_INVITE_SWITCH_OPEN",
"roomid": 9615419
}

View File

@ -1,15 +0,0 @@
{
"cmd": "PK_MATCH",
"pk_status": 100,
"pk_id": 3596,
"data": {
"init_id": 9615419,
"match_id": 10185039,
"escape_time": 5,
"is_portrait": false,
"uname": "茉莉艿",
"face": "http://i2.hdslb.com/bfs/face/3f2833a3ac598d9757ba33b79ec219cf941bdda8.jpg",
"uid": 18963076
},
"roomid": 9615419
}

View File

@ -1,8 +0,0 @@
{
"cmd": "PK_MIC_END",
"pk_id": 3596,
"pk_status": 1300,
"data": {
"type": 0
}
}

View File

@ -1,15 +0,0 @@
{
"cmd": "PK_PRE",
"pk_id": 3597,
"pk_status": 200,
"data": {
"init_id": 9615419,
"match_id": 10185039,
"count_down": 5,
"pk_topic": "模仿游戏角色让对方猜",
"pk_pre_time": 1529476609,
"pk_start_time": 1529476614,
"pk_end_time": 1529476914,
"end_time": 1529477034
}
}

View File

@ -1,12 +0,0 @@
{
"cmd": "PK_PROCESS",
"pk_id": 8798,
"pk_status": 300,
"data": {
"uid": 0,
"init_votes": 30,
"match_votes": 20,
"user_votes": 0
},
"roomid": 346075
}

View File

@ -1,69 +0,0 @@
{
"cmd": "PK_SETTLE",
"pk_id": 8806,
"pk_status": 400,
"data": {
"pk_id": 8806,
"init_info": {
"uid": 7799328,
"init_id": 10979759,
"uname": "筱宇淅淅",
"face": "http://i0.hdslb.com/bfs/face/e16515ac39329aa125bb8de5bb1fa9455f06337c.jpg",
"votes": 0,
"is_winner": false
},
"match_info": {
"uid": 18654316,
"match_id": 430063,
"uname": "卖丸子尕害羞",
"face": "http://i1.hdslb.com/bfs/face/1c579a244ec0c66bbb6e2ad6c770a2a498268735.jpg",
"votes": 129,
"is_winner": true,
"vip_type": 0,
"exp": {
"color": 5805790,
"user_level": 31,
"master_level": {
"level": 26,
"color": 10512625
}
},
"vip": {
"vip": 0,
"svip": 0
},
"face_frame": "",
"badge": {
"url": "http://i0.hdslb.com/bfs/live/74b2f9a48ce14d752dd27559c4a0df297243a3fd.png",
"desc": "bilibili直播签约主播\r\n",
"position": 3
}
},
"best_user": {
"uid": 31459309,
"uname": "七友球球",
"face": "http://i1.hdslb.com/bfs/face/09406a4fe632dda9d523da14f3e3735ee02efbab.jpg",
"vip_type": 0,
"exp": {
"color": 6406234,
"user_level": 19,
"master_level": {
"level": 1,
"color": 6406234
}
},
"vip": {
"vip": 0,
"svip": 0
},
"privilege_type": 0,
"face_frame": "",
"badge": {
"url": "",
"desc": "",
"position": 0
}
},
"punish_topic": "惩罚:模仿一款表情包"
}
}

View File

@ -1,10 +0,0 @@
{
"cmd": "PK_START",
"pk_id": 3597,
"pk_status": 300,
"data": {
"init_id": 9615419,
"match_id": 10185039,
"pk_topic": "模仿游戏角色让对方猜"
}
}

View File

@ -1,4 +0,0 @@
{
"cmd": "PREPARING",
"roomid": "1110317"
}

View File

@ -1,17 +0,0 @@
{
"cmd": "RAFFLE_END",
"roomid": 521429,
"data": {
"raffleId": 16897,
"type": "flower_rain",
"from": "鷺沢怜人",
"fromFace": "http://i1.hdslb.com/bfs/face/09eafe44f913012512014e91f25001edf6e072d0.jpg",
"win": {
"uname": "nbqgd",
"face": "http://i1.hdslb.com/bfs/face/09eafe44f913012512014e91f25001edf6e072d0.jpg",
"giftId": 115,
"giftName": "桃花",
"giftNum": 66
}
}
}

View File

@ -1,10 +0,0 @@
{
"cmd": "RAFFLE_START",
"roomid": 234024,
"data": {
"raffleId": 16915,
"type": "flower_rain",
"from": "爱吃喵姐的鱼",
"time": 60
}
}

View File

@ -1,14 +0,0 @@
{
"cmd": "ROOM_ADMINS",
"uids": [
4561799,
432672,
2179804,
7928207,
94380,
1626161,
3168349,
13182672
],
"roomid": 5279
}

View File

@ -1,6 +0,0 @@
{
"cmd": "ROOM_BLOCK_MSG",
"uid": "60244207",
"uname": "承包rose",
"roomid": 5279
}

View File

@ -1,5 +0,0 @@
{
"cmd": "ROOM_LOCK",
"expire": "2018-03-15 10:24:18",
"roomid": 6477301
}

View File

@ -1,10 +0,0 @@
{
"cmd": "ROOM_RANK",
"data": {
"roomid": 1241012,
"rank_desc": "小时榜 182",
"color": "#FB7299",
"h5_url": "https://live.bilibili.com/p/eden/rank-h5-current?anchor_uid\u003d35577726",
"timestamp": 1527148082
}
}

View File

@ -1,7 +0,0 @@
{
"cmd": "ROOM_SHIELD",
"type": 1,
"user": "",
"keyword": "",
"roomid": 234024
}

View File

@ -1,11 +0,0 @@
{
"cmd": "ROOM_SHIELD",
"type": 1,
"user": [],
"keyword": [
"暗号",
"摄像头",
"色相头"
],
"roomid": 505447
}

View File

@ -1,5 +0,0 @@
{
"cmd": "ROOM_SILENT_OFF",
"data": [],
"roomid": "29434"
}

View File

@ -1,9 +0,0 @@
{
"cmd": "ROOM_SILENT_ON",
"data": {
"type": "level",
"level": 1,
"second": 1520424615
},
"roomid": 5279
}

View File

@ -1,97 +0,0 @@
{
"cmd": "SEND_GIFT",
"data": {
"giftName": "节奏风暴",
"num": 1,
"uname": "爱上熹",
"rcost": 569788,
"uid": 230845505,
"top_list": [
{
"uid": 288348879,
"uname": "我爱我家一生",
"face": "http://i1.hdslb.com/bfs/face/dd52e4f2dfe881751816e45522f504f10458b514.jpg",
"rank": 1,
"score": 1852300,
"guard_level": 0,
"isSelf": 0
},
{
"uid": 287551243,
"uname": "熹上城的专属天使菲",
"face": "http://i1.hdslb.com/bfs/face/c3ef04ba6c267c41067cd7708b7abd60c0c5c49f.jpg",
"rank": 2,
"score": 1245200,
"guard_level": 3,
"isSelf": 0
},
{
"uid": 32416351,
"uname": "镜子。。",
"face": "http://i1.hdslb.com/bfs/face/08c54c2c97434811a99e9d070d621ccbb5d3f2c4.jpg",
"rank": 3,
"score": 332862,
"guard_level": 3,
"isSelf": 0
}
],
"timestamp": 1520992553,
"giftId": 39,
"giftType": 0,
"action": "赠送",
"super": 1,
"super_gift_num": 1,
"price": 100000,
"rnd": "1980508331",
"newMedal": 0,
"newTitle": 0,
"medal": {
"medalId": "95723",
"medalName": "布丁诶",
"level": 1
},
"title": "",
"beatId": "4",
"biz_source": "live",
"metadata": "",
"remain": 0,
"gold": 88570,
"silver": 127492,
"eventScore": 0,
"eventNum": 0,
"smalltv_msg": [],
"specialGift": {
"id": "316221038798",
"time": 90,
"hadJoin": 0,
"num": 1,
"content": "你们城里人真会玩",
"action": "start",
"storm_gif": "http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift/2/jiezou.gif?2017011901"
},
"notice_msg": [],
"capsule": {
"normal": {
"coin": 166,
"change": 10,
"progress": {
"now": 3630,
"max": 10000
}
},
"colorful": {
"coin": 2,
"change": 0,
"progress": {
"now": 0,
"max": 5000
}
},
"move": 1
},
"addFollow": 0,
"effect_block": 0,
"coin_type": "gold",
"total_coin": 100000
}
}

View File

@ -1,14 +0,0 @@
{
"cmd": "SPECIAL_GIFT",
"data": {
"39": {
"id": 214692,
"time": 90,
"hadJoin": 0,
"num": 1,
"content": "前方高能预警,注意这不是演习",
"action": "start",
"storm_gif": "http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift/2/jiezou.gif?2017011901"
}
}
}

View File

@ -1,12 +0,0 @@
{
"cmd": "SPECIAL_GIFT",
"data": {
"39": {
"id": 214692,
"time": 0,
"hadJoin": 0,
"num": 0,
"action": "end"
}
}
}

View File

@ -1,9 +0,0 @@
{
"cmd": "SYS_GIFT",
"msg": "jjhghhfgh:? 在蜜桃姐姐w的:?直播间7813816:?内赠送:?6:?共450个",
"msg_text": "jjhghhfgh在蜜桃姐姐w的直播间7813816内赠送亿圆共450个",
"roomid": 0,
"real_roomid": 0,
"giftId": 0,
"msgTips": 0
}

View File

@ -1,11 +0,0 @@
{
"cmd": "SYS_GIFT",
"msg": "【情怀家的尹蓝ovo】在直播间【147191】洒下漫天花雨快来拾撷桃花邂逅你的缘分",
"msg_text": "【情怀家的尹蓝ovo】在直播间【147191】洒下漫天花雨快来拾撷桃花邂逅你的缘分",
"tips": "【情怀家的尹蓝ovo】在直播间【147191】洒下漫天花雨快来拾撷桃花邂逅你的缘分",
"url": "http://live.bilibili.com/147191",
"roomid": 147191,
"real_roomid": 147191,
"giftId": 116,
"msgTips": 0
}

View File

@ -1,10 +0,0 @@
{
"cmd": "SYS_GIFT",
"msg": "十四不落:? 在直播间 :?590:? 使用了 20 倍节奏风暴,大家快去跟风领取奖励吧!",
"tips": "【十四不落】在直播间【590】使用了 20 倍节奏风暴,大家快去跟风领取奖励吧!",
"url": "http://live.bilibili.com/590",
"roomid": 847617,
"real_roomid": 0,
"giftId": 39,
"msgTips": 1
}

View File

@ -1,12 +0,0 @@
{
"cmd": "SYS_MSG",
"msg": "【天南地狗-】:?在直播间:?【531】:?赠送 小电视一个,请前往抽奖",
"msg_text": "【天南地狗-】:?在直播间:?【531】:?赠送 小电视一个,请前往抽奖",
"rep": 1,
"styleType": 2,
"url": "http://live.bilibili.com/531",
"roomid": 531,
"real_roomid": 22237,
"rnd": 1520992662,
"tv_id": "40478"
}

View File

@ -1,21 +0,0 @@
{
"cmd": "TV_END",
"data": {
"id": "39077",
"uname": "かこゆきこvew",
"sname": "是你的苏苏吖",
"giftName": "10W银瓜子",
"mobileTips": "恭喜 かこゆきこvew 获得10W银瓜子",
"raffleId": "39077",
"type": "small_tv",
"from": "是你的苏苏吖",
"fromFace": "http://i0.hdslb.com/bfs/face/147f137d24138d1cfec5443d98ac8b03c4332398.jpg",
"win": {
"uname": "かこゆきこvew",
"face": "http://i0.hdslb.com/bfs/face/4d63bd62322e7f3ef38723a91440bc6930626d9f.jpg",
"giftName": "银瓜子",
"giftId": "silver",
"giftNum": 100000
}
}
}

View File

@ -1,23 +0,0 @@
{
"cmd": "TV_START",
"data": {
"id": "40072",
"dtime": 180,
"msg": {
"cmd": "SYS_MSG",
"msg": "【杰宝Yvan生命倒计时】:?在直播间:?【102】:?赠送 小电视一个,请前往抽奖",
"msg_text": "【杰宝Yvan生命倒计时】:?在直播间:?【102】:?赠送 小电视一个,请前往抽奖",
"rep": 1,
"styleType": 2,
"url": "http://live.bilibili.com/102",
"roomid": 102,
"real_roomid": 5279,
"rnd": 12987955,
"tv_id": "40072"
},
"raffleId": 40072,
"type": "small_tv",
"from": "杰宝Yvan生命倒计时",
"time": 180
}
}

View File

@ -1,5 +0,0 @@
{
"cmd": "WARNING",
"msg": "违反直播分区规范,请立即更换至游戏区",
"roomid": 1365604
}

View File

@ -1,10 +0,0 @@
{
"cmd": "WELCOME",
"data": {
"uid": 18625858,
"uname": "\u662f\u767d\u8272\u70e4\u6f06",
"isadmin": 0,
"svip": 1
},
"roomid": 39189
}

View File

@ -1,9 +0,0 @@
{
"cmd": "WELCOME_ACTIVITY",
"data": {
"uid": 49427998,
"uname": "起个名真tm费事",
"type": "forever_love",
"display_mode": 1
}
}

View File

@ -1,10 +0,0 @@
{
"cmd": "WELCOME_GUARD",
"data": {
"uid": 23598108,
"username": "lovevael",
"guard_level": 3,
"water_god": 0
},
"roomid": 43001
}

View File

@ -1,23 +0,0 @@
{
"cmd": "WISH_BOTTLE",
"data": {
"action": "update",
"id": 1832,
"wish": {
"id": 1832,
"uid": 110631,
"type": 1,
"type_id": 7,
"wish_limit": 99999,
"wish_progress": 14381,
"status": 1,
"content": "女装直播",
"ctime": "2018-01-12 17:25:58",
"count_map": [
1,
3,
5
]
}
}
}

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<i>
<!--aid 44514794-->
<oid>77932184</oid>
<ps>0</ps>
<pe>196000</pe>
<pc>1</pc>
<pn>1</pn>
<state>0</state>
<real_name>0</real_name>
<!--https://github.com/bilibili/DanmakuFlameMaster/blob/master/Sample/src/main/java/com/sample/BiliDanmukuParser.java 与现在版本不一致-->
<!--0: 弹幕 id-->
<!--1: 不明确(可能是弹幕池 id)-->
<!--2: 弹幕出现时间(从视频开始播放开始计算)(ms)(不一定与弹幕发送者的播放器时间一致)-->
<!--3: 类型(1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)-->
<!--4: 字号-->
<!--5: 颜色-->
<!--6: 时间戳(弹幕的发送时间)-->
<!--7: 不明确-->
<!--8: 用户 id 的 hash(ITU I.363.5, 即 CRC32)-->
<d p="12509048833835076,0,117373,5,25,16777215,1551001292,0,d2c5fc5">硬核劈柴</d>
<d p="12509130082746372,0,10149,1,25,16777215,1551001447,0,36762f3f">2222</d>
<d p="12509469902635008,0,167322,1,25,16777215,1551002095,0,a54ebcbd">神奇的三哥,没有什么是他们顶不了的</d>
<d p="12509518345273348,0,99416,1,25,16777215,1551002187,0,fdfc821d">这水是甜的吧</d>
<d p="12509689741312004,0,143359,1,25,16777215,1551002514,0,d9141364">真 逃生</d>
<d p="12510325574729732,0,31553,1,25,16777215,1551003727,0,9be0083c">非常优秀</d>
<d p="12510425270190082,0,112103,1,25,16777215,1551003917,0,6b7e70e1">中国,赞</d>
<d p="12511511077453828,0,145335,1,25,16777215,1551005988,0,4640575b">逃离生活的窗</d>
<d p="12511622457196548,0,35862,1,25,16777215,1551006200,0,32249eeb">嘴冲?</d>
<d p="12511627372396548,0,38222,1,25,16777215,1551006210,0,32249eeb">口冲</d>
<d p="12515287960125440,0,33997,1,25,16777215,1551013192,0,31f305c2">自取其乳</d>
<d p="12515547915223040,0,168259,1,25,16777215,1551013688,0,5a09b397">666</d>
<d p="12516693915467780,0,177747,1,25,16777215,1551015874,0,eee14fa1">这脖子</d>
<d p="12516704495075332,0,177747,1,25,16777215,1551015894,0,eee14fa1">这脖子是铁打的吧</d>
<d p="12517097740435458,0,14248,1,25,16777215,1551016644,0,4acca5b2">333</d>
<d p="12517272835326020,0,54028,1,25,16777215,1551016978,0,d087ac22">666啊</d>
<d p="12517313871872002,0,84671,1,25,41194,1551017056,0,c27bae2f">四倍体草莓</d>
<d p="12521502397169668,0,152101,1,25,16777215,1551025045,0,d425ebe6">高层建筑通云梯的窗台</d>
<d p="12521739791630340,0,13170,1,25,16777215,1551025498,0,ae910367">3338</d>
<d p="12526503713046532,0,51669,1,25,16777215,1551034584,0,782f992b">哈哈</d>
<d p="12528652852396036,0,151912,1,25,16777215,1551038683,0,24780dca">你告诉我哪里有这么高的云梯</d>
<d p="12531482545356802,0,151162,4,25,15138834,1551044081,0,7ba95a84">逃离生活窗</d>
<d p="12534806507159556,0,174937,1,25,16777215,1551050421,0,23e291df">自重最少三百公斤的玩意顶在头上还能单手爬楼梯,这是人能做到的吗</d>
<d p="12535841481555968,0,28234,1,25,16777215,1551052395,0,463ae566">前功尽弃系列</d>
<d p="12536174091436036,0,187603,1,25,16777215,1551053029,0,2f9d3c2b">深圳会展中心?</d>
<d p="12536447369216000,0,25105,1,25,16777215,1551053550,0,c2dd98f1">求这女孩的的体重 马上</d>
<d p="12536668506030082,0,45319,1,25,16777215,1551053972,0,7095f6c8">蘸糖墩儿</d>
<d p="12536737092337668,0,97592,1,25,16777215,1551054103,0,24fc5da4">我们家的</d>
<d p="12537151663636480,0,52026,1,25,16777215,1551054894,0,85067ef">牛逼</d>
<d p="12537154059632644,0,56703,1,25,16777215,1551054898,0,85067ef">卧槽</d>
<d p="12537358213709826,0,147955,1,25,16777215,1551055288,0,c0f9fca3">生活重来窗</d>
<d p="12538283349770244,0,98274,1,25,16777215,1551057052,0,9089266e">糖墩儿那个,我们是老乡</d>
<d p="12540637177970692,0,53562,1,25,16777215,1551061542,0,630553e8">卧槽</d>
<d p="12541039516057604,0,172384,1,25,16777215,1551062309,0,18afe75f">上化佛他们能顶么?</d>
<d p="12541168076718084,0,87307,1,25,16777215,1551062554,0,87411221">九星虹梅</d>
<d p="12541180367601668,0,62312,1,25,16777215,1551062578,0,84bbd366">6666</d>
<d p="12541421086572548,0,124646,1,25,16777215,1551063037,0,89e08e5b">快看 是岳云鹏</d>
<d p="12541523945586692,0,58595,1,25,16777215,1551063233,0,89853200">军人nb</d>
<d p="12541681330552836,0,55322,1,25,16777215,1551063533,0,7fc58f5">这是练什么,你们成天笑印度人,敢不敢把这个给印度人看</d>
<d p="12541710838005764,0,69226,1,25,16777215,1551063589,0,919f1906">一辈子单身</d>
<d p="12541713491951620,0,85083,1,25,16777215,1551063595,0,7fc58f5">这tm成了灵芝了</d>
<d p="12541729957216256,0,81506,1,25,16777215,1551063626,0,c99a3c2d">要坚强</d>
<d p="12541757011525636,0,83999,1,25,16777215,1551063678,0,c574fa94">草莓:我控制不住我的生长</d>
<d p="12541766596034560,0,175567,1,25,16777215,1551063696,0,919f1906">铁头功</d>
<d p="12541807514615812,0,58938,1,25,16777215,1551063774,0,795d594d">站军姿,身体要前倾</d>
<d p="12541814231793668,0,6716,1,25,16777215,1551063787,0,159c9870">牛逼</d>
<d p="12542220192186372,0,52988,1,25,16777215,1551064561,0,18ccd294">这个上初中时被班主任罚站,就是在台阶上用脚尖站</d>
<d p="12542324742553604,0,57888,1,25,16777215,1551064760,0,aba65a57">小腿肚子疼</d>
<d p="12542490122911748,0,52502,1,25,16777215,1551065076,0,dc79c826">江科炸出来</d>
<d p="12542873415188484,0,4141,4,25,15138834,1551065807,0,4fca51a">一句卧槽行天下</d>
<d p="12542894059028484,0,36129,1,25,16777215,1551065846,0,e86f7dbe">禁止自娱自乐</d>
<d p="12542915894575108,0,59905,4,25,15138834,1551065888,0,4fca51a">一句卧槽行走天下</d>
<d p="12543067077738500,0,55463,1,25,16777215,1551066176,0,47c66d00">这个是真的难受!!!</d>
<d p="12543107687514116,0,18613,1,25,16777215,1551066254,0,cb4a9077">还有卧槽</d>
<d p="12543134014636032,0,148627,1,25,16777215,1551066304,0,c830b87b">逃出升天</d>
<d p="12543278968209412,0,51770,1,25,16777215,1551066580,0,6090150d">我也这么站过</d>
<d p="12543327467995140,0,76223,1,25,16777215,1551066673,0,fbb62223">除了牛逼还可以说盖帽</d>
<d p="12543367314931716,0,135928,5,25,16777215,1551066749,0,88f119f1">半挂</d>
<d p="12543439578595332,0,59536,1,25,16777215,1551066887,0,8665586e">一个下去一排倒</d>
<d p="12543474239799300,0,47028,1,25,16711680,1551066953,0,3b59d8ed">强迫症不能忍</d>
<d p="12543478613409796,0,120357,1,25,16777215,1551066961,0,8665586e">硬核劈材</d>
<d p="12543729190043652,0,58468,1,25,16777215,1551067439,0,2158075b">我们也这么站过</d>
<d p="12543770022641666,0,12159,1,25,16777215,1551067517,0,1b84016c">卧槽</d>
<d p="12543874514812932,0,54399,1,25,16777215,1551067716,0,b8e8864e">我大江科</d>
<d p="12544083492339716,0,27741,1,25,16777215,1551068115,0,42bd1786">功亏一篑</d>
<d p="12544113036492804,0,54983,1,25,16777215,1551068171,0,42bd1786">我只会说:卧槽</d>
<d p="12544135804223492,0,68683,1,25,16777215,1551068215,0,42bd1786">亲媳妇</d>
<d p="12544157188358148,0,86981,1,25,16777215,1551068256,0,42bd1786">手炉?</d>
<d p="12544171444797444,0,101377,1,25,16777215,1551068283,0,42bd1786">甜辣口的</d>
<d p="12544201995059204,0,121207,1,25,16777215,1551068341,0,42bd1786">这个是高手</d>
<d p="12544234053173252,0,151787,1,25,16777215,1551068402,0,42bd1786">逃离生命</d>
<d p="12544262875381764,0,167925,1,25,16777215,1551068457,0,42bd1786">摩托精?</d>
<d p="12544293120507908,0,35820,1,25,16777215,1551068515,0,54681ea3">公的</d>
<d p="12544313787940868,0,190580,1,25,16777215,1551068554,0,9a0c194e">想看三哥顶汽车</d>
<d p="12544316839821316,0,54579,1,25,16777215,1551068560,0,54681ea3">好了站5个小时</d>
<d p="12544374657777668,0,151059,1,25,16777215,1551068670,0,54681ea3">逃离生活的窗户</d>
<d p="12544396354387970,0,145747,5,25,16707842,1551068712,0,1c2904f3">众所周知,逃生=逃出生天=逃出,生天</d>
<d p="12544659980025860,0,163561,1,25,16777215,1551069215,0,dac77b12">金字塔是不是他们顶上去的</d>
<d p="12545464756862980,0,174150,1,25,16777215,1551070749,0,7927ad01">腰间盘突出了解一下</d>
<d p="12546357659172868,0,65518,1,25,16777215,1551072453,0,d1b711e0">俺也一样</d>
<d p="12546551898963972,0,11939,1,25,16777215,1551072823,0,37017ae0">可以说 卧槽</d>
<d p="12546668542033924,0,31087,1,25,16777215,1551073046,0,a81360fc">我了个大草</d>
<d p="12546701081444354,0,82303,1,25,16777215,1551073108,0,a81360fc">兄弟帽子不错</d>
<d p="12546720176013316,0,175796,1,25,16777215,1551073144,0,46c90882">梯子牛逼</d>
<d p="12547003792228356,0,66404,1,25,16777215,1551073685,0,359613b4">老子最讨厌女人了!滚!</d>
<d p="12547078239027200,0,117822,1,25,16777215,1551073827,0,d586fc8f">还有这种操作!?</d>
<d p="12547123676971012,0,159473,1,25,16777215,1551073914,0,e08d6d5e">被谁淹没不知所措</d>
<d p="12547149768163328,0,18573,1,25,16777215,1551073963,0,af799cfa">奈何本人无文化,一句卧槽走天下</d>
<d p="12547183239757828,0,138761,1,25,16777215,1551074027,0,8ab298ba">好心酸。</d>
<d p="12547367960051714,0,174706,1,25,16777215,1551074380,0,d66698a9">我顶不住</d>
<d p="12547463784693762,0,98486,1,25,16777215,1551074562,0,315e19b2">她有一个大胆的想法</d>
<d p="12547504946544642,0,105119,1,25,16777215,1551074641,0,2bb0a0d5">那个不是西葫芦么。。西葫芦甜的???</d>
<d p="12547560521072644,0,162119,1,25,16777215,1551074747,0,590724e4">舒服</d>
<d p="12547667420250116,0,11704,1,25,16777215,1551074951,0,2bf4452b">牛了个逼</d>
<d p="12547706826260482,0,9199,1,25,16777215,1551075026,0,ac40f732">还可以说卧槽</d>
<d p="12547714786525184,0,4020,1,25,16777215,1551075041,0,ac40f732">还可以说卧槽</d>
<d p="12547717997789188,0,48483,1,25,16777215,1551075047,0,3c0c3293">他说弯腰下去继续蘸酱么?</d>
<d p="12547798832513028,0,49654,1,25,16777215,1551075201,0,6655f47c"></d>
<d p="12547845801377794,0,168066,1,25,16777215,1551075291,0,51068b32">牛逼</d>
<d p="12548000418627588,0,114144,1,25,16777215,1551075586,0,dff175b5">水瓜</d>
<d p="12548114018205700,0,56942,1,25,16777215,1551075803,0,10f78b15">模型出了点问题</d>
<d p="12548138132832260,0,152683,1,25,16777215,1551075849,0,a914360c">逃出生天</d>
<d p="12548268299911172,0,188366,1,25,16777215,1551076097,0,50865f2c">阿三是真牛逼…</d>
<d p="12548273663377412,0,31422,1,25,16777215,1551076107,0,33e5f873">回首掏 惨不忍睹</d>
<d p="12548290776137728,0,58698,1,25,16777215,1551076140,0,124b4e53">帅啊</d>
<d p="12548323870769154,0,186805,1,25,16777215,1551076203,0,dbc23261">这孩子的弹跳力惊人</d>
<d p="12548337070768128,0,170612,1,25,16777215,1551076228,0,552b1633">这是高手</d>
<d p="12548341505720322,0,170612,1,25,16777215,1551076237,0,552b1633">这是高手</d>
<d p="12548375489544196,0,191208,1,25,16777215,1551076301,0,124b4e53">吉尼斯记录三哥就顶的车</d>
<d p="12548479186370564,0,170612,1,25,16777215,1551076499,0,552b1633">这是高手</d>
<d p="12548593167630340,0,87039,1,25,16777215,1551076717,0,b3367171">八倍体草莓</d>
</i>

BIN
record/视频弹幕/list.so Normal file

Binary file not shown.

View File

@ -1,6 +1,7 @@
package com.hiczp.bilibili.api
import com.hiczp.bilibili.api.app.AppAPI
import com.hiczp.bilibili.api.danmaku.DanmakuAPI
import com.hiczp.bilibili.api.main.MainAPI
import com.hiczp.bilibili.api.member.MemberAPI
import com.hiczp.bilibili.api.message.MessageAPI
@ -8,6 +9,7 @@ import com.hiczp.bilibili.api.passport.PassportAPI
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.player.PlayerAPI
import com.hiczp.bilibili.api.player.PlayerInterceptor
import com.hiczp.bilibili.api.retrofit.Header
import com.hiczp.bilibili.api.retrofit.Param
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
@ -72,21 +74,21 @@ class BilibiliClient(
@Suppress("SpellCheckingInspection")
private val defaultCommonHeaderInterceptor = CommonHeaderInterceptor(
"Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" },
"Buvid" to { billingClientProperties.buildVersionId },
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
Header.DISPLAY_ID to { "${billingClientProperties.buildVersionId}-$initTime" },
Header.BUILD_VERSION_ID to { billingClientProperties.buildVersionId },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent },
Header.DEVICE_ID to { billingClientProperties.hardwareId }
)
@Suppress("SpellCheckingInspection")
private val defaultCommonParamArray = arrayOf(
Param.ACCESS_KEY to { token },
Param.APP_KEY to { billingClientProperties.appKey },
"build" to { billingClientProperties.build },
"channel" to { billingClientProperties.channel },
"mobi_app" to { billingClientProperties.platform },
"platform" to { billingClientProperties.platform },
"ts" to { Instant.now().epochSecond.toString() }
Param.BUILD to { billingClientProperties.build },
Param.CHANNEL to { billingClientProperties.channel },
Param.MOBILE_APP to { billingClientProperties.platform },
Param.PLATFORM to { billingClientProperties.platform },
Param.TIMESTAMP to { Instant.now().epochSecond.toString() }
)
private val defaultCommonQueryParamInterceptor = CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray)
@ -100,11 +102,11 @@ class BilibiliClient(
defaultCommonHeaderInterceptor,
CommonParamInterceptor(ParamType.FORM_URL_ENCODED,
Param.APP_KEY to { billingClientProperties.appKey },
"build" to { billingClientProperties.build },
"channel" to { billingClientProperties.channel },
"mobi_app" to { billingClientProperties.platform },
"platform" to { billingClientProperties.platform },
"ts" to { Instant.now().epochSecond.toString() }
Param.BUILD to { billingClientProperties.build },
Param.CHANNEL to { billingClientProperties.channel },
Param.MOBILE_APP to { billingClientProperties.platform },
Param.PLATFORM to { billingClientProperties.platform },
Param.TIMESTAMP to { Instant.now().epochSecond.toString() }
)
)
}
@ -117,7 +119,7 @@ class BilibiliClient(
createAPI<MessageAPI>(BaseUrl.message,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray,
"actionKey" to { "appkey" },
Param.ACTION_KEY to { Param.APP_KEY },
"has_up" to { "1" }
)
)
@ -142,10 +144,10 @@ class BilibiliClient(
createAPI<MainAPI>(BaseUrl.main,
CommonHeaderInterceptor(
//如果未登陆则没有 Display-ID
"Display-ID" to { userId?.let { "$it-$initTime" } },
"Buvid" to { billingClientProperties.buildVersionId },
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
Header.DISPLAY_ID to { userId?.let { "$it-$initTime" } },
Header.BUILD_VERSION_ID to { billingClientProperties.buildVersionId },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent },
Header.DEVICE_ID to { billingClientProperties.hardwareId }
),
defaultCommonQueryParamInterceptor
)
@ -159,12 +161,12 @@ class BilibiliClient(
createAPI<VcAPI>(BaseUrl.vc,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray,
"_device" to { billingClientProperties.platform },
"_hwid" to { billingClientProperties.hardwareId },
"src" to { billingClientProperties.channel },
"trace_id" to { generateTraceId() },
"uid" to { userId?.toString() },
"version" to { billingClientProperties.version }
Param._DEVICE to { billingClientProperties.platform },
Param._HARDWARE_ID to { billingClientProperties.hardwareId },
Param.SOURCE to { billingClientProperties.channel },
Param.TRACE_ID to { generateTraceId() },
Param.USER_ID to { userId?.toString() },
Param.VERSION to { billingClientProperties.version }
)
)
}
@ -190,15 +192,33 @@ class BilibiliClient(
.client(OkHttpClient.Builder().apply {
addInterceptor(PlayerInterceptor(billingClientProperties) { loginResponse })
addInterceptor(FailureResponseInterceptor)
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
}
addNetworkInterceptor(httpLoggingInterceptor)
}.build())
.build()
.create(PlayerAPI::class.java)
}
/**
* 获取弹幕所用的 API
*/
val danmakuAPI: DanmakuAPI by lazy {
Retrofit.Builder()
.baseUrl(BaseUrl.main)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
addInterceptor(CommonHeaderInterceptor(
Header.ACCEPT to { "application/xhtml+xml,application/xml" },
Header.ACCEPT_ENCODING to { "gzip, deflate" },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent }
))
addInterceptor(defaultCommonQueryParamInterceptor)
addInterceptor(sortAndSignInterceptor)
addNetworkInterceptor(httpLoggingInterceptor)
}.build())
.build()
.create(DanmakuAPI::class.java)
}
/**
* 登陆
* v3 登陆接口会同时返回 cookies token
@ -269,6 +289,7 @@ class BilibiliClient(
fun logoutFuture() = GlobalScope.future { logout() }
private val sortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
private val httpLoggingInterceptor = HttpLoggingInterceptor().setLevel(logLevel)
private inline fun <reified T : Any> createAPI(
baseUrl: String,
vararg interceptors: Interceptor
@ -282,10 +303,7 @@ class BilibiliClient(
}
addInterceptor(sortAndSignInterceptor)
addInterceptor(FailureResponseInterceptor)
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
}
addNetworkInterceptor(httpLoggingInterceptor)
}.build())
.build()
.create(T::class.java)

View File

@ -5,6 +5,12 @@ package com.hiczp.bilibili.api
* 默认值对应 5.37.0(release-b220051) 版本.
*/
class BilibiliClientProperties {
/**
* 默认 UA, 用于大多数访问
*/
@Suppress("SpellCheckingInspection")
var defaultUserAgent = "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)"
/**
* Android 平台的 appKey(该默认值为普通版客户端, 非概念版)
*/

View File

@ -5,7 +5,7 @@ import java.security.MessageDigest
//MD5
private val md5Instance = MessageDigest.getInstance("MD5")
internal fun String.md5() =
fun String.md5() =
StringBuilder(32).apply {
//优化过的 md5 字符串生成算法
md5Instance.digest(toByteArray()).forEach {

View File

@ -0,0 +1,30 @@
package com.hiczp.bilibili.api
import org.apache.commons.io.input.BoundedInputStream
import org.apache.commons.io.input.BoundedReader
import java.io.InputStream
import java.nio.charset.Charset
/**
* 以大端模式从流中读取一个 int
*/
fun InputStream.readInt(): Int {
val byteArray = ByteArray(4).apply {
read(this)
}
return (byteArray[0].toInt() shl 24) or
(byteArray[1].toInt() shl 16) or
(byteArray[2].toInt() shl 8) or
(byteArray[3].toInt())
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun InputStream.readUInt() = readInt().toUInt()
fun InputStream.boundedReader(maxCharsFromTargetReader: Int, charset: Charset = Charsets.UTF_8) =
BoundedReader(reader(charset), maxCharsFromTargetReader)
fun InputStream.bounded(size: Long) = BoundedInputStream(this, size)
@Suppress("EXPERIMENTAL_API_USAGE")
fun InputStream.bounded(size: UInt) = bounded(size.toLong())

View File

@ -82,6 +82,7 @@ interface AppAPI {
* 视频页面(普通视频, 非番剧)
* 包含视频基本信息, 推荐和广告
* 从这个接口得到视频的 cid
* 如果返回内容里的 pages 有多个表明有多个 p, 每个 p 有自己的 cid(外层的 cid 为默认的那个 p cid)
*
* @param aid 视频的唯一标识
*/

View File

@ -0,0 +1,54 @@
package com.hiczp.bilibili.api.danmaku
data class Danmaku(
/**
* 弹幕 id
*/
val id: Long,
/**
* 下标 1, 不明属性
*/
val unknownAttribute1: String,
/**
* 弹幕出现时间(播放器时间)(ms)
*/
val time: Long,
/**
* 弹幕类型
* (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
*/
val type: Int,
/**
* 字号
*/
val fontSize: Int,
/**
* 颜色
*/
val color: Int,
/**
* 弹幕的发送时间(时间戳)(s)
*/
val timestamp: Long,
/**
* 下标 7, 不明属性
*/
val unknownAttribute7: String,
/**
* 弹幕发送者的 hash(用户 id CRC32 校验和)
*/
val user: String,
/**
* 弹幕的内容
*/
val content: String
)

View File

@ -0,0 +1,26 @@
package com.hiczp.bilibili.api.danmaku
import kotlinx.coroutines.Deferred
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Query
@Suppress("DeferredIsResult")
interface DanmakuAPI {
/**
* 获取弹幕(视频或者番剧)
*
* @param aid 视频的唯一标识
* @param oid 注意, 此处的 oid cid
*
* @return 返回的内容是二进制数据, 由于数据量可能很大, 此处不做解析
*/
@GET("/x/v2/dm/list.so")
fun list(
@Query("aid") aid: Long,
@Query("oid") oid: Long,
@Query("plat") plat: Int? = 2,
@Query("ps") pageSize: Int = 0,
@Query("type") type: Int = 1
): Deferred<ResponseBody>
}

View File

@ -0,0 +1,126 @@
package com.hiczp.bilibili.api.danmaku
import com.google.gson.stream.JsonReader
import com.hiczp.bilibili.api.bounded
import com.hiczp.bilibili.api.readUInt
import java.io.InputStream
import java.util.*
import java.util.zip.GZIPInputStream
import javax.xml.namespace.QName
import javax.xml.stream.XMLInputFactory
import javax.xml.stream.XMLStreamConstants
/**
* 弹幕文件解析器.
* 弹幕文件(list.so)有三个部分
* 第一个部分为一个 Int, 表示第二部分的长度
* 第二部分为一个 Json, 标识各个弹幕的等级(用于屏蔽设置)
* 第三部分为一个 gzip 压缩过的 xml
*
* Web 端的弹幕是一个明文 xml, APP 的接口是不一样的.
*
* json 部分形如 {"dmflags":[{"dmid":12551893546958848,"flag":10}],"rec_flag":1,"rec_text":"开启后,全站视频将按等级等优化弹幕","rec_switch":1}
* xml 部分形如 <d p="12509048833835076,0,117373,5,25,16777215,1551001292,0,d2c5fc5">硬核劈柴</d>
*
* @see com.hiczp.bilibili.api.danmaku.DanmakuAPI.list
*/
@Suppress("SpellCheckingInspection")
class DanmakuParser {
companion object {
/**
* 解析弹幕文件
*
* @param inputStream 输入流, 可以指向任何位置
* @param autoClose 是否在解析后自动关闭流
*
* @return 返回 flags map 弹幕列表. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间.
*/
@JvmStatic
fun parser(inputStream: InputStream, autoClose: Boolean = true): Pair<Map<Long, Int>, List<Danmaku>> {
//Json 的长度
val jsonLength = inputStream.readUInt()
//弹幕ID-Flag
val danmakuFlags = HashMap<Long, Int>()
//gson 会从 reader 中自行缓冲 1024 个字符, 这会导致额外的字符被消费. 因此要限制其读取数量
//流式解析 Json
with(JsonReader(inputStream.bounded(jsonLength).reader())) {
beginObject()
while (hasNext()) {
when (nextName()) {
"dmflags" -> {
beginArray()
while (hasNext()) {
var danmakuId = 0L
var flag = 0
beginObject()
while (hasNext()) {
when (nextName()) {
"dmid" -> danmakuId = nextLong()
"flag" -> flag = nextInt()
else -> skipValue()
}
}
endObject()
danmakuFlags[danmakuId] = flag
}
endArray()
}
else -> skipValue()
}
}
endObject()
}
//json 解析完毕后, 剩下的内容是一个 gzip 压缩过的 xml
val reader = GZIPInputStream(inputStream).reader()
val danmakus = LinkedList<Danmaku>()
//流式解析 xml
val xmlEventReader = XMLInputFactory.newInstance().createXMLEventReader(reader)
var startD = false //之前解析到的 element 是否是 d
var p: String? = null //之前解析到的 p 的值
while (xmlEventReader.hasNext()) {
val event = xmlEventReader.nextEvent()
when (event.eventType) {
XMLStreamConstants.START_ELEMENT -> {
with(event.asStartElement()) {
startD = name.localPart == "d"
if (startD) {
p = getAttributeByName(P).value
}
}
}
XMLStreamConstants.CHARACTERS -> {
//如果前一个解析到的是 d 标签, 那么此处得到的一定是 d 标签的 body
if (startD) {
val danmaku = with(StringTokenizer(p, ",")) {
Danmaku(
nextToken().toLong(),
nextToken(),
nextToken().toLong(),
nextToken().toInt(),
nextToken().toInt(),
nextToken().toInt(),
nextToken().toLong(),
nextToken(),
nextToken(),
event.asCharacters().data
)
}
danmakus.add(danmaku)
}
}
}
}
//自动关闭流
if (autoClose) inputStream.close()
return danmakuFlags to danmakus
}
//常量, 用于加快速度
@JvmStatic
private val P = QName("p")
}
}

View File

@ -10,6 +10,11 @@ import java.lang.management.ManagementFactory
/**
* 这里是播放器会访问的 API
* 返回内容中会有多个视频下载地址, 他们代表不同的视频质量(音频同理)
* 音频和视频是分开的
* 下载视频得到的是一个 m4s 文件, 但是实际上是完整的视频(例如整个番剧而非片段)
* 下载音频得到的也是一个 m4s 文件, 也是完整的
* 将视频和音频合在一起, 就可以播放了
*/
@Suppress("DeferredIsResult", "SpellCheckingInspection")
interface PlayerAPI {
@ -38,7 +43,7 @@ interface PlayerAPI {
*
* @param aid 番剧的唯一标识
* @param cid 在番剧详情页的返回值里
* @param seasonType 类型, 不明确, 似乎总为 1
* @param seasonType 类型, 不明确, 似乎总为 1
* @param session 其值为 系统已运行时间(ms)的MD5值, 此处的默认值为 JVM 已启动时间, Android 上请使用 SystemClock
* @param trackPath 不明确
*

View File

@ -26,9 +26,9 @@ class PlayerInterceptor(
//添加 header
val header = request.headers().newBuilder().apply {
add("Accept", "*/*")
add("User-Agent", "Bilibili Freedoooooom/MarkII")
add("Accept-Language", Header.ZH_CN)
add(Header.ACCEPT, "*/*")
add(Header.USER_AGENT, "Bilibili Freedoooooom/MarkII")
add(Header.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8")
}.build()
//添加 Query Params
@ -50,13 +50,13 @@ class PlayerInterceptor(
addParamEncode(Param.MID, "0")
}
//公共参数
addParamEncode("device", bilibiliClientProperties.platform)
addParamEncode("mobi_app", bilibiliClientProperties.platform)
addParamEncode("platform", bilibiliClientProperties.platform)
addParamEncode(Param.DEVICE, bilibiliClientProperties.platform)
addParamEncode(Param.MOBILE_APP, bilibiliClientProperties.platform)
addParamEncode(Param.PLATFORM, bilibiliClientProperties.platform)
addParamEncode("otype", "json")
addParamEncode("ts", Instant.now().epochSecond.toString())
addParamEncode("build", bilibiliClientProperties.build)
addParamEncode("buvid", bilibiliClientProperties.buildVersionId)
addParamEncode(Param.TIMESTAMP, Instant.now().epochSecond.toString())
addParamEncode(Param.BUILD, bilibiliClientProperties.build)
addParamEncode(Param.BUILD_VERSION_ID, bilibiliClientProperties.buildVersionId)
}.toString().let {
//排序
val sortedEncodedQuery = it.split('&').sorted().joinToString(separator = "&")

View File

@ -12,15 +12,38 @@ object Method {
}
object Header {
const val JSON = "application/json"
const val FORM_URLENCODED = "application/x-www-form-urlencoded; charset=utf-8"
const val ZH_CN = "zh-CN,zh;q=0.8"
const val DISPLAY_ID = "Display-ID"
@Suppress("SpellCheckingInspection")
const val BUILD_VERSION_ID = "Buvid"
const val DEVICE_ID = "Device-ID"
const val USER_AGENT = "User-Agent"
const val ACCEPT = "Accept"
const val ACCEPT_LANGUAGE = "Accept-Language"
const val ACCEPT_ENCODING = "Accept-Encoding"
}
object Param {
const val ACCESS_KEY = "access_key"
@Suppress("SpellCheckingInspection")
const val APP_KEY = "appkey"
const val ACTION_KEY = "actionKey"
const val BUILD = "build"
@Suppress("SpellCheckingInspection")
const val BUILD_VERSION_ID = "buvid"
const val CHANNEL = "channel"
@Suppress("ObjectPropertyName")
const val _DEVICE = "_device"
const val DEVICE = "device"
@Suppress("ObjectPropertyName", "SpellCheckingInspection")
const val _HARDWARE_ID = "_hwid"
const val SOURCE = "src"
const val TRACE_ID = "trace_id"
const val USER_ID = "uid"
const val VERSION = "version"
@Suppress("SpellCheckingInspection")
const val MOBILE_APP = "mobi_app"
const val PLATFORM = "platform"
const val TIMESTAMP = "ts"
const val EXPIRE = "expire"
const val MID = "mid"
const val SIGN = "sign"

View File

@ -30,11 +30,20 @@ object FailureResponseInterceptor : Interceptor {
contentType.charset(Charsets.UTF_8)!!
}
//拷贝流并读取其内容
val jsonObject = body.source().also {
//拷贝流
val inputStreamReader = body.source().also {
it.request(Long.MAX_VALUE)
}.buffer.clone().inputStream().reader(charset).let {
jsonParser.parse(it).obj
}.buffer.clone().inputStream().reader(charset)
//读取其内容
val jsonObject = try {
jsonParser.parse(inputStreamReader).obj
} catch (exception: Exception) {
//如果返回内容解析失败, 说明它不是一个合法的 json
//如果在拦截器抛出 MalformedJsonException 会导致 Retrofit 的异步请求一直卡着直到超时
return response
} finally {
inputStreamReader.close()
}
//判断 code 是否为 0

View File

@ -0,0 +1,21 @@
package com.hiczp.bilibili.api.test
import com.hiczp.bilibili.api.danmaku.DanmakuParser
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
class DanmakuTest {
//339.6kib 的弹幕文件在 0.5s 内解析完毕, 通常视频的弹幕不会超过这个容量
@Test
fun fetchAndParseDanmaku() {
runBlocking {
//著名的炮姐视频 你指尖跃动的闪光是我此生不变的信仰
bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await().let {
DanmakuParser.parser(it.byteStream())
}.let { (map, list) ->
println(map)
println(list)
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.hiczp.bilibili.api.test
import com.google.gson.GsonBuilder
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
@ -17,7 +18,9 @@ class PlayUrlTest {
fun bangumiPlayUrl() {
runBlocking {
bilibiliClient.playerAPI.run {
bangumiPlayUrl(aid = 42714241, cid = 74921228).await()
bangumiPlayUrl(aid = 42714241, cid = 74921228).await().let {
GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(it)
}.let(::println)
}
}
}