Merge pull request #1 from czp3009/kotlin

merge
This commit is contained in:
向日癸 2020-01-23 21:57:02 +08:00 committed by GitHub
commit 17ce452782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
412 changed files with 11815 additions and 24371 deletions

823
README.md
View File

@ -1,483 +1,490 @@
# Bilibili API 调用库
该项目提供 Bilibili API 的 Java 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
# Bilibili API JVM 调用库
该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
由于B站即使更新客户端, 也会继续兼容以前的旧版本客户端, 所以短期内不用担心 API 失效的问题.
对于一些 Bilibili Android APP 上没有的功能, 可以先[将 token 转换为 cookie](#sso), 然后再去调用 Bilibili Web API.
# API 不完全
由于本项目还在开发初期, 大量 API 没有完成, 所以很可能没有你想要的 API.
欢迎提交 issue 或者 Merge Request.
# 添加依赖
## Gradle
compile group: 'com.hiczp', name: 'bilibili-api', version: '0.0.20'
# 名词解释
B站不少参数都是瞎取的, 并且不统一, 经常混用, 以下给出一些常见参数对应的含义
| 参数 | 含义 |
| :--- | :--- |
| mid | 用户 ID(与 userId 含义一致, 经常被混用) |
| userId | 用户 ID, 用户在B站的唯一标识, 数字 |
| uid | 用户 ID, 与 userId 同义 |
| userid | 注意这里是全小写, 它的值可能是 'bili_1178318619', 这个东西是没用的, B站并不用这个作为用户唯一标识 |
| showRoomId | 直播间 URL (Web)上的房间号(可能是一个很小的数字, 低于 1000) |
| roomId | 直播间的真实 ID(直播房间号在 1000 以下的房间, 真实 ID 是另外一个数字) |
| cid | 直播间 ID(URL 上的短房间号以及真实房间号都叫 cid) |
| ruid | 直播间房主的用户 ID |
| rcost | 该房间内消费的瓜子数量 |
(上表仅供其他开发者参照, 本调用库中已经封装为 Java 标准全写小驼峰命名法, 例如 userId, roomId, roomUserId)
使用一台虚拟的 `Pixel 2` 设备来截取数据包, 一些固定参数可能与真实设备不一致.
# 使用
## RESTFul API
由于B站 API 设计清奇, 一些显然不需要登录的 API 也需要登录, 所以所有 API 尽可能登陆后访问以免失败.
```groovy
compile group: 'com.hiczp', name: 'bilibili-api', version: '0.1.0'
```
### 登录
使用账户名和密码作为登录参数
# 技术说明
`BilibiliClient` 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
LoginResponseEntity loginResponseEntity = bilibiliAPI.login(String username, String password);
所有调用从这个类开始, 包括登陆以及访问其他各种 API.
IOException 在网络故障时抛出
使用协程来实现异步, 由于 [kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 为编译器实现, 因此并非所有 JVM 语言都能正确调用 `suspend` 方法.
LoginException 在用户名密码不匹配时抛出
本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.
CaptchaMismatchException 在验证码不正确时抛出, 见下文 [验证码问题](#验证码问题) 一节
`BilibiliClient` 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 `BilibiliClient` 实例, 而应该保存其引用.
login 方法的返回值为 LoginResponseEntity 类型, 使用
一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:
BilibiliAccount bilibiliAccount = loginResponseEntity.toBilibiliAccount();
来获得一个 BilibiliAccount 实例, 其中包含了 OAuth2 的用户凭证, 如果有需要, 可以将其持久化保存.
将一个登陆状态恢复出来(从之前保存的 BilibiliAccount 实例)使用如下代码
BilibiliAPI bilibiliAPI = new BilibiliAPI(BilibiliAccount bilibiliAccount);
注意, 如果这个 BilibiliAccount 实例含有的 accessToken 是错误的或者过期的, 需要鉴权的 API 将全部 401.
### 刷新 Token
OAuth2 的重要凭证有两个, token 与 refreshToken, token 到期之后, 并不需要再次用用户名密码登录一次, 仅需要用 refreshToken 刷新一次 token 即可(会得到新的 token 和 refreshToken, refreshToken 的有效期不是无限的. B站的 refreshToken 有效期不明确).
bilibiliAPI.refreshToken();
IOException 在网络故障时抛出
LoginException 在 token 错误,或者 refreshToken 错误或过期时抛出.
refreshToken 操作在正常情况下将在服务器返回 401(实际上 B站 不用 401 来表示未登录)时自动进行, 因此 BilibiliAPI 内部持有的 BilibiliAccount 的实例的内容可能会发生改变, 如果需要在应用关闭时持久化用户 token, 需要这样来取得最后的 BilibiliAccount 状态
BilibiliAccount bilibiliAccount = bilibiliAPI.getBilibiliAccount();
### 登出
bilibiliAPI.logout();
IOException 在网络故障时抛出
LoginException 在 accessToken 错误或过期时抛出
### 验证码问题
当对一个账户在短时间内(时长不明确)尝试多次错误的登录(密码错误)后, 再尝试登录该账号, 会被要求验证码.
此时登录操作会抛出 CaptchaMismatchException 异常, 表示必须调用另一个接口
public LoginResponseEntity login(String username,
String password,
String captcha,
String cookie) throws IOException, LoginException, CaptchaMismatchException
这个接口将带 captcha 参数地去登录, 注意这里还有一个 cookie 参数.
下面先给出一段正确使用该接口的代码, 随后会解释其步骤
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
try {
bilibiliAPI.login(username, password);
} catch (CaptchaMismatchException e) { //如果该账号现在需要验证码来进行登录, 就会抛出异常
cookie = "sid=123456"; //自己造一个 cookie 或者从服务器取得
Response response = bilibiliAPI.getCaptchaService()
.getCaptcha(cookie)
.execute();
InputStream inputStream = response.body().byteStream();
String captcha = letUserInputCaptcha(inputStream); //让用户根据图片输入验证码
bilibiliAPI.login(
username,
password,
captcha,
cookie
);
```kotlin
runBlocking {
val bilibiliClient = BilibiliClient().apply {
login(username, password)
}
val myInfo = bilibiliClient.appAPI.myInfo().await()
val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
}
```
验证码是通过访问 https://passport.bilibili.com/captcha 这个地址获得的.
如果一个请求的返回内容中的 `code`(code 是 BODY 的内容, 并非 HttpStatus) 不为 0, 将抛出异常 `BilibiliApiException`, 通过以下代码来获取服务器原始返回的 `code`:
访问这个地址需要带有一个 cookie, cookie 里面要有 "sid=xxx", 然后服务端会记录下对应关系, 也就是 sid xxx 对应验证码 yyy, 然后就可以验证了.
```kotlin
val code = bilibiliApiException.commonResponse.code
```
我们会发现, 访问任何 passport.bilibili.com 下面的地址, 都会被分发一个 cookie, 里面带有 sid 的值. 我们访问 /captcha 也会被分发一个 cookie, 但是这个通过访问 captcha 而被分发得到的 cookie 和访问得到的验证码图片, 没有对应关系. 推测是因为 cookie 的发放在请求进入甚至模块运行完毕后才进行.
一个错误返回的原始 `JSON` 如下所示:
所以我们如果不带 cookie 去访问 /captcha, 我们这样拿到的由 /captcha 返回的 cookie 和 验证码, 是不匹配的.
```json
{
"code": -629,
"message": "用户名与密码不匹配",
"ts": 1550730464
}
```
所以我们要先从其他地方获取一个 cookie.
每种不同的 API 在错误时返回的 `code` 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.
我们可以用 /api/oauth2/getKey(获取加密密码用的 hash 和公钥) 来获取一个 cookie
# 登录和登出
(Bilibili oauth2 v3)
String cookie = bilibiliAPI.getPassportService()
.getKey()
.execute()
.headers()
.get("Set-cookie");
登陆和登出均为异步方法, 需要在协程上下文中执行(接下去不会特地强调这一点).
/captcha 不验证 cookie 正确性, 我们可以直接使用假的 cookie (比如 123456)对其发起验证码请求, 它会记录下这个假的 cookie 和 验证码 的对应关系, 一样能验证成功. 但是不推荐这么做.
简单地说, 只要我们是带 cookie 访问 /captcha 的, 那么我们得到的验证码, 是和这个 cookie 绑定的. 我们接下去用这个 cookie 和 这个验证码的值 去进行带验证码的登录, 就可以成功登陆.
至于验证码怎么处理, 可以显示给最终用户, 让用户来输入, 或者用一些预训练模型自动识别验证码.
这个带验证码的登录接口也会继续抛出 CaptchaMismatchException, 如果验证码输入错误的话.
### SSO
通过 SSO API 可以将 accessToken 转为 cookie, 用 cookie 就可以访问 B站 的 Web API.
B站客户端内置的 WebView 就是通过这种方式来工作的(WebView 访问页面时, 处于登录状态).
首先, 我们需要登录
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
bilibiliAPI.login(String username, String password);
通过
bilibiliAPI.toCookies();
来得到对应的 cookies, 类型为 Map<String, List\<Cookie>>, key 为 domain(可能是通配类型的, 例如 ".bilibili.com"), value 为此 domain 对应的 cookies.
如果只想得到用于进行 SSO 操作的那条 URL, 可以这么做
String goUrl = "https://account.bilibili.com/account/home";
bilibiliAPI.getSsoUrl(goUrl);
返回值是一个 HttpUrl, 里面 url 的值差不多是这样的
https://passport.bilibili.com/api/login/sso?access_key=c3bf6002bd2e539f5bfce56308f14789&appkey=1d8b6e7d45233436&build=515000&gourl=https%3A%2F%2Faccount.bilibili.com%2Faccount%2Fhome&mobi_app=android&platform=android&ts=1520079995&sign=654e2d00aa827aa1d7acef6fbeb9ee70
如果 access_key 是正确的话, 这个 url 访问一下就登录 B站 了.
如果想跟 B站 客户端一样弄一个什么内嵌 WebView 的话, 这个 API 就可以派上用场(只需要在 WebView 初始化完毕后让 WebView 去访问这个 url, 就登陆了)(goUrl 可以是任意值, 全部的 302 重定向完成后将进入这个地址, 如果 goUrl 不存在或为空则将跳转到B站首页).
### Web API
上文讲到, 通过 SSO API, 可以将 token 转为 cookie, 在本项目中, Web API 封装在 BilibiliWebAPI 中, 可以通过如下方式得到一个已经登录了的 BilibiliWebAPI 实例
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
bilibiliAPI.login(String username, String password);
BilibiliWebAPI bilibiliWebAPI = bilibiliAPI.getBilibiliWebAPI();
IOException 在网络错误时抛出(获取 cookie 时需要进行网络请求)
如果将之前的 bilibiliAPI.toCookies() 的返回值(cookiesMap)持久化了下来的话, 下次可以通过以下方式直接获得一个已经登录了的 BilibiliWebAPI 实例(注意, cookie 没有 refreshToken 机制, 过期不会自动刷新, 因此不推荐持久化 cookie)
Map<String, List<Cookie>> cookiesMap = bilibiliAPI.toCookies();
//序列化后存储
//...
//反序列化后得到上次存储的 cookiesMap
BilibiliWebAPI bilibiliWebAPI = new BilibiliWebAPI(cookiesMap);
有了 BilibiliWebAPI 实例之后, 通过类似以下代码的形式来获取对应的 Service, API 调用方法和基于 Token 方式的 API 一致
LiveService liveService = bilibiliWebAPI.getLiveService();
(这个 LiveService 是 Web API 里的 LiveService)
由于 Web API 是有状态的, 每个 BilibiliWebAPI 内部维护的 CookieJar 是同一个, 一些验证有关的 API 可能会改变 cookie.
通过以下代码来获得一个 BilibiliWebAPI 中目前持有的 CookieJar 的引用
bilibiliWebAPI.getCookieJar();
### API 调用示例
打印一个直播间的历史弹幕
long roomId = 3;
new BilibiliAPI()
.getLiveService()
.getHistoryBulletScreens(roomId)
.execute()
.body()
.getData()
.getRoom()
.forEach(liveHistoryBulletScreenEntity ->
System.out.printf("[%s]%s: %s\n",
liveHistoryBulletScreenEntity.getTimeline(),
liveHistoryBulletScreenEntity.getNickname(),
liveHistoryBulletScreenEntity.getText())
);
签到
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
bilibiliAPI.login(username, password);
bilibiliAPI.getLiveService()
.getSignInfo()
.execute();
发送一条弹幕到指定直播间
long roomId = 3;
String username = "yourUsername";
String password = "yourPassword";
BilibiliAPI bilibiliAPI = new BilibiliAPI();
bilibiliAPI.login(username, password);
bilibiliAPI.getLiveService()
.sendBulletScreen(
new BulletScreenEntity(
roomId,
bilibiliAPI.getBilibiliAccount().getUserId(), //实际上并不需要包含 mid 就可以正常发送弹幕, 但是真实的 Android 客户端确实发送了 mid
"这是自动发送的弹幕"
)
)
.execute();
(如果要调用需要鉴权的 API, 需要先登录)
API 文档
//TODO 文档编写中
## Socket
### 获取直播间实时弹幕
long roomId = 3;
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
LiveClient liveClient = new BilibiliAPI()
.getLiveClient(eventLoopGroup, roomId)
.registerListener(new MyListener())
.connect();
.connect() 会抛出 IOException 当网络故障时.
(connect 是阻塞的)
使用 .getLiveClient() 前可以先登录也可以不登陆直接用, 如果 API 已经登录, 那么进房数据包中会带有用户ID, 尚不明确有什么作用, 可能与一些统计有关.
多个 LiveClient 可以复用同一个 EventLoopGroup.
(connect 方法运行结束只代表 socket 确实是连上了, 但是服务器还没有响应进房请求数据包)
(当服务器响应进房请求数据包时才代表真的连上了, 此时会有一个连接成功的事件, 见下文)
事件机制使用 Google Guava EventBus 实现, 监听器不需要继承任何类或者接口.
public class MyListener {
@Subscribe
public void onConnectSucceed(ConnectSucceedEvent connectSucceedEvent) {
//do something
}
@Subscribe
public void onConnectionClose(ConnectionCloseEvent connectionCloseEvent) {
//do something
}
@Subscribe
public void onDanMuMsg(DanMuMsgPackageEvent danMuMsgPackageEvent) {
DanMuMsgEntity danMuMsgEntity = danMuMsgPackageEvent.getEntity();
System.out.pintf("%s: %s\n", danMuMsgEntity.getUsername(), danMuMsgEntity.getMessage());
}
```kotlin
runBlocking {
BilibiliClient().run {
login(username, password)
logout()
}
}
```
如果持续 40 秒(心跳包为 30 秒)没有收到任何消息, 将视为掉线, 会跟服务器主动断开连接一样(这通常是发送了服务器无法读取的数据包)触发一次 ConnectionCloseEvent.
`login` 方法返回一个 `LoginResponse` 实例, 下次可以直接赋值到没有登陆的 `BilibiliClient` 实例中来恢复登陆状态.
liveClient.closeChannel();
```kotlin
BilibiliClient().apply {
this.loginResponse = loginResponse
}
```
即可阻塞关闭连接.
`LoginResponse` 继承 `Serializable`, 可被序列化(JVM 序列化).
liveClient.closeChannelAsync();
可能的错误返回有两种:
即可异步关闭连接.
-629 用户名与密码不匹配
-105 验证码错误
eventLoopGroup.shutdownGracefully();
如果仅使用用户名与密码进行登陆并且得到了 `-105` 的结果, 那么说明需要验证码(通常是由于多次错误的登陆尝试导致的).
即可关闭事件循环, 结束 Nio 工作线程(所有使用这个 EventLoopGroup 的 LiveClient 也将在此时被关闭).
原始返回如下所示
如果需要在直播间发送弹幕可以直接使用如下代码(需要先登录)
{"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}
String message = "这是一条弹幕";
liveClient.sendBulletScreen(message);
自行访问 `commonResponse.data.obj.url.string` 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:
所有的事件(有些数据包我也不知道它里面的一些值是什么含义, /record 目录下面有抓取到的 Json, 可以用来查看):
```kotlin
login(username, password, challenge, secCode, validate)
```
| 事件 | 抛出条件 | 含义 |
| :--- | :--- | :--- |
| ActivityEventPackageEvent | 收到 ACTIVITY_EVENT 数据包 | 活动事件 |
| ChangeRoomInfoPackageEvent | 收到 CHANGE_ROOM_INFO 数据包 | 更换房间背景图片 |
| ComboEndPackageEvent | 收到 COMBO_END 数据包 | 礼物连发结束 |
| ComboSendPackageEvent | 收到 COMBO_SEND 数据包 | 礼物连发开始 |
| ConnectionCloseEvent | 连接断开(主动或被动) | |
| ConnectSucceedEvent | 进房成功 | |
| CutOffPackageEvent | 收到 CUT_OFF 数据包 | 被 B站 管理员强制中断 |
| DanMuMsgPackageEvent | 收到 DANMU_MSG 数据包 | 弹幕消息 |
| EntryEffectPackageEvent | 收到 ENTRY_EFFECT 数据包 | 尚不明确 |
| EventCmdPackageEvent | 收到 EVENT_CMD 数据包 | 尚不明确 |
| GuardBuyPackageEvent | 收到 GUARD_BUY 数据包 | 船票购买 |
| GuardLotteryStartPackageEvent | 收到 GUARD_LOTTERY_START 数据包 | 船票购买后的抽奖活动 |
| GuardMsgPackageEvent | 收到 GUARD_MSG 数据包 | 舰队消息(登船) |
| LivePackageEvent | 收到 LIVE 数据包 | 开始直播 |
| NoticeMsgPackageEvent | 收到 NOTICE_MSG 数据包 | 获得大奖的通知消息 |
| PkAgainPackageEvent | 收到 PK_AGAIN 数据包 | 下面几个都是 PK 有关的事件 |
| PkClickAgainPackageEvent | 收到 PK_CLICK_AGAIN 数据包 |
| PkEndPackageEvent | 收到 PK_END 数据包 |
| PkInviteFailPackageEvent | 收到 PK_INVITE_FAIL 数据包 |
| PkInviteInitPackageEvent | 收到 PK_INVITE_INIT 数据包 |
| PkInviteSwitchClosePackageEvent | 收到 PK_INVITE_SWITCH_CLOSE 数据包 |
| PkInviteSwitchOpenPackageEvent | 收到 PK_INVITE_SWITCH_OPEN 数据包 |
| PkMatchPackageEvent | 收到 PK_MATCH 数据包 |
| PkMicEndPackageEvent | 收到 PK_MIC_END 数据包 |
| PkPrePackageEvent | 收到 PK_PRE 数据包 |
| PkProcessPackageEvent | 收到 PK_PROCESS 数据包 |
| PkSettlePackageEvent | 收到 PK_SETTLE 数据包 |
| PkStartPackageEvent | 收到 PK_START 数据包 |
| PreparingPackageEvent | 收到 PREPARING 数据包 | 停止直播 |
| RaffleEndPackageEvent | 收到 RAFFLE_END 数据包 | 抽奖结束(小奖, 通常是不定期活动) |
| RaffleStartPackageEvent | 收到 RAFFLE_START 数据包 | 抽奖开始(小奖) |
| ReceiveDataPackageDebugEvent | 该事件用于调试, 收到任何 Data 数据包时都会触发 | |
| RoomAdminsPackageEvent | 收到 ROOM_ADMINS 数据包 | 房管变更 |
| RoomBlockMsgPackageEvent | 收到 ROOM_BLOCK_MSG 数据包 | 房间黑名单(房间管理员添加了一个用户到黑名单) |
| RoomLockPackageEvent | 收到 ROOM_LOCK 数据包 | 房间被封 |
| RoomRankPackageEvent | 收到 ROOM_RANK 数据包 | 小时榜 |
| RoomShieldPackageEvent | 收到 ROOM_SHIELD 数据包 | 房间屏蔽 |
| RoomSilentOffPackageEvent | 收到 ROOM_SILENT_OFF 数据包 | 房间结束禁言 |
| RoomSilentOnPackageEvent | 收到 ROOM_SILENT_ON 数据包 | 房间开启了禁言(禁止某一等级以下的用户发言) |
| SendGiftPackageEvent | 收到 SEND_GIFT 数据包 | 送礼 |
| SendHeartBeatPackageEvent | 每次发送心跳包后触发一次 | |
| SpecialGiftPackageEvent | 收到 SPECIAL_GIFT 数据包 | 节奏风暴(20 倍以下的)(只在对应房间内有, 不会全站广播) |
| SysGiftPackageEvent | 收到 SYS_GIFT 数据包 | 系统礼物(20 倍以上节奏风暴, 活动抽奖等) |
| SysMsgPackageEvent | 收到 SYS_MSG 数据包 | 系统消息(小电视等) |
| TVEndPackageEvent | 收到 TV_END 数据包 | 小电视抽奖结束(大奖的获得者信息) |
| TVStartPackageEvent | 收到 TV_START 数据包 | 小电视抽奖开始 |
| UnknownPackageEvent | B站新增了新种类的数据包, 出现此情况请提交 issue | |
| ViewerCountPackageEvent | 收到 房间人数 数据包(不是 Json) | |
| WarningPackageEvent | 收到 WARNING 数据包 | 警告信息 |
| WelcomeActivityPackageEvent | 收到 WELCOME_ACTIVITY 数据包 | 欢迎(活动) |
| WelcomePackageEvent | 收到 WELCOME 数据包 | 欢迎(通常是 VIP) |
| WelcomeGuardPackageEvent | 收到 WELCOME_GUARD 数据包 | 欢迎(舰队) |
| WishBottlePackageEvent | 收到 WISH_BOTTLE 数据包 | 许愿瓶 |
`challenge` 为本次极验的唯一标识(在一开始给出的 url 中)
事件里面可以取到解析好的 POJO, 然后可以从里面取数据, 见上面的监听器示例.
`validate` 为极验返回值
# 特别说明
## DANMU_MSG 中的各个字段含义
在直播间实时弹幕推送流中, 存在一种类型为 DANMU_MSG 的数据包, 它里面存储的 JSON, 全部都是 JsonArray, 并且每个元素类型不一样, 含义不一样.
`secCode``"$validate|jordan"`
简单地说, 这个 JSON 完全无法自描述而且很多字段猜不到是什么含义, 它的示例见 /record 文件夹(还有一份带备注的版本, 里面记录了已经猜出的字段含义).
(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).
已经猜出的字段, 可以直接从 DanMuMsgEntity 里面用对应的方法取得, 对于没有猜出的字段, 需要类似这样来获取:
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误(想要这么做的人一定脑子瓦特了).
int something = danMuMsgEntity.getInfo().get(0).getAsJsonArray().get(2).getAsInt();
登陆后, 可以访问全部 API(注意, 有一些明显不需要登录的 API 也有可能需要登录).
如果你可以明确其中的字段含义, 欢迎提交 issue.
由于各种需要登陆的 API 在未登录时返回的 `code` 并不统一, 因此没有办法做自动 `token` 刷新, 自己看着办.
## 直播间 ID 问题
一个直播间, 我们用浏览器去访问它, 他可能是这样的
在真实的客户端上, 每次一打开 APP 就会访问[个人信息 API](#获取个人信息)来确定 `token` 是否仍然可用, 这就是 B站 自己的解决方案.
http://live.bilibili.com/3
我们可能会以为后面的 3 就是这个直播间的 room_id, 其实并不是.
# 访问 API
不要问文档, 用自动补全(心)来感受. 以下给出几个示例
我们能直接看到的这个号码, 其实是 show_room_id.
## 获取个人信息
(首先要登陆)
所有直播间号码小于 1000 的直播间, show_room_id 和 room_id 是不相等的(room_id 在不少 API 里又叫 cid).
```kotlin
val myInfo = bilibiliClient.appAPI.myInfo().await()
```
一些 API 能提供自动跳转功能, 也就是用这个 show_room_id 作为参数, 返回的信息是跳转到对应的 room_id 之后的返回信息.
返回用户 ID, vip 信息等.
简单地说, 一些 API 用 show_room_id 作为参数可以正常工作, 而另一些不能. 所以尽可能使用 room_id 作为参数来调用 API.
## 搜索
当我们想看某些内容时, 我们会首先使用搜索功能, 例如
room_id 的获取要通过
```kotlin
val searchResult = bilibiliClient.appAPI.search(keyword = "刀剑神域").await()
```
http://api.live.bilibili.com/AppRoom/index?room_id=3&platform=android
实际上这对应客户端上的 搜索 -> 综合.
其中, response.data.room_id 就是其真实的 room_id, 例子中的这个直播间的真实 room_id 为 23058
如果要搜索番剧则使用 `bilibiliClient.appAPI.searchBangumi`.
在代码中我们这样做
同理, 搜索直播, 用户, 影视, 专栏分别使用 `searchLive`, `searchUser`, `searchMovie`, `searchArticle`.
long showRoomId = 3;
long roomId = bilibiliAPI.getLiveService()
.getRoomInfo(showRoomId)
.execute()
.body()
.getData()
.getRoomId();
所有的搜索都使用 `pageNumber` 参数来控制翻页(从 1 开始).
由此, 我们获得了直播间的真实 room_id, 用它访问其他 API 就不会出错了.
## 获取视频播放地址
获取视频实际播放地址的 API 比较特殊, 被单独分了出来, 示例如下
## 服务器返回非 0 返回值时
当服务器返回的 JSON 中的 code 字段非 0 时(有错误发生), 该 JSON 可能是由服务端过滤器统一返回的, 因此其 JSON 格式(字段类型)将和实体类不一样, 此时会导致 JsonParseErrorException.
```kotlin
val videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 72913641).await()
```
为了让调用代码不需要写很多 try catch, 因此当服务器返回的 code 非 0 时, 封装好的 OkHttpClientInterceptor 将把 data 字段变为 null(发生错误时, data 字段没有实际有效的数据).
`aid` 即 av 号, 只能表示视频播放的那个页面, 如果一个视频有多个 `p`, 那么每个 `p` 都有单独的 `cid`.
因此只需要判断 code 是否是 0 即可知道 API 是否成功执行, 不需要异常捕获.
在 Web 端, URL 通常是这样的
(B站所有 API 无论是否执行成功, HttpStatus 都是 200, 判断 HTTP 状态码是无用的, 必须通过 JSON 中的 code 字段来知道 API 是否执行成功).
https://www.bilibili.com/video/av44541340/?p=2
# 测试
测试前需要先设置用户名和密码, 在 src/test/resources 目录下, 找到 config-template.json, 将其复制一份到同目录下并命名为 config.json 然后填写其中的字段即可.
实际上就是选择了该 `aid` 下的第二个 `cid`(注意, 参数里使用的 `cid` 不是这个 p 的序号, 它也是一个很长的数字).
本项目使用 JUnit 作为单元测试框架. 命令行只需要执行
简单的来说, `aid``cid` 加在一起才能表示一个视频流(为什么 `cid` 不能直接表示一个视频我也不知道).
gradle test
因此无论是获取视频播放地址, 还是获取弹幕列表, 都要同时传入 `aid``cid`.
如果要在 IDEA 上进行测试, 需要运行 test 目录中的 RuleSuite 类(在 IDEA 中打开这个类, 点击行号上的重叠的两个向右箭头图标).
`cid` 在哪里获得呢, 如下所示
# 继续开发
如果您想加入到开发中, 欢迎提交 Merge Request.
```kotlin
val view = bilibiliClient.appAPI.view(aid = 41517911).await()
```
本项目的 Http 请求全部使用 Retrofit 完成, 因此请求的地址和参数需要放在接口中统一管理, 如果您对 Retrofit 不是很熟悉, 可以看[这篇文章](http://square.github.io/retrofit/).
该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.
服务器返回值将被 Gson 转换为 Java POJO(Entity), 通过[这篇文章](https://github.com/google/gson/blob/master/UserGuide.md)来了解 Gson.
其中 `data.cid` 为默认 `p``cid`. `data.pages[n].cid` 为每个 `p``cid`. 如果只有一个 `p` 那么说明视频没有分 `p`.
POJO 使用 IDEA 插件 [GsonFormat](https://plugins.jetbrains.com/plugin/7654-gsonformat) 自动化生成, 而非手动编写, 并且尽可能避免对自动生成的结果进行修改以免导致可能出现混淆或含义不明确的情况.
请求视频地址将访问如下结构的内容
(插件必须开启 "use SerializedName" 选项从而保证字段名符合小驼峰命名法)
由于 B站 一些 JSON 是瞎鸡巴来的, 比如可能出现以下这种情况
"list": [
{
"name": "value",
```json
{
"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
}
```
此时自动生成的类型将是
(由于内容太长, 去除了一部分内容)
List<List> lists
注意, 视频下载地址有好几个(以上返回内容中被折叠成了两个), 但是实际上他们都是一样的内容, 只是清晰度不同. `data.dash.video.id` 实际上代表 `data.accept_quality`.
因此必须要为内层元素指定一个具有语义的名称, 例如 Name, 此时类型变为
视频和音频是分开的, 视频和音频都返回 `m4s` 文件, 将其合并即可得到完整的 `mp4` 文件.
List<Name> names
`data.quality` 指默认选择的清晰度, 通常情况下移动网络会自动选择 `32`, 即 "清晰 480P"(在 `data.accept_description` 中对应).
API 尽可能按照 UI 位置来排序, 例如
对于番剧来说, 也使用 `aid``cid` 来获得播放地址
侧拉抽屉 -> 直播中心 -> 我的关注
```kotlin
val bangumiPlayUrl = bilibiliClient.playerAPI.bangumiPlayUrl(aid = 42714241, cid = 74921228).await()
```
这是 "直播中心" 页面的第一个可点击控件, 那么下一个 API 或 API 组就应该是第二个可点击组件 "观看历史".
返回内容差不多是一个原理, 这里就不赘述了.
和 UI 不对应的 API, 按照执行顺序排序, 例如进入直播间会按顺序访问一系列 API, 这些 API 就按照时间顺序排序.
如何获得番剧的 `aid``cid` 呢. 我们都知道, 实际上番剧那个页面的唯一标识是 "季", 同一个番的不同 "季" 其实是不同的东西.
对于不知道怎么排的 API, 瞎鸡巴排就好了.
我们在番剧搜索页面可以得到番剧的 `season`, 这代表了一个番剧的某一季的页面.
然后我们用 `season` 来打开番剧页面.
```kotlin
val season = bilibiliClient.mainAPI.season(seasonId = 25617).await()
```
返回值中的 `result.seasons[n].season_id` 为该番所有季的 id(包含用来作为查询条件的 `seasonId`).
该 API 还可以用 `episodeId` 作为查询条件, 即以集为条件打开一个番剧页面(会跳转到对应的季).
返回值中的 `result.episodes` 包含了当前所选择的季的全部集的 `aid``cid`.
## 查看视频下面的评论
看完了视频当然要看一下傻吊网友都在说些什么. 使用以下 API 获取一个视频的评论.
```kotlin
val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
```
这里的 `oid``aid`(其他一些 API 中 `oid` 也可能指 `cid` 详见方法上面的注释).
评论是不分 `p` 的, 所有评论都是在一起的.
可以额外使用一个 `next` 参数来指定返回的起始楼层(即翻页).
楼层是越翻越小的, 所以 `next` 也要越来越小.
看到了傻吊网友们的评论是不够的, 我们还想看到杠精与其隔着屏幕对喷的场景, 因此我们要获取评论的子评论, 即评论的评论
```kotlin
val childReply = bilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
```
其中的 `root` 表示根评论的 id.
每个评论都有自己的 `replyId`, `parentId` 以及 `rootId`.
假如一个人在一个评论的子评论里发布了一个评论并且 at 了其他人发的评论, 那么其 `parentId` 是他所 at 的评论, 其 `rootId` 为所在的根评论.
如果不满足对应的层级逻辑关系(例如本身为根评论), `parentId``rootId` 可能为 0.
用额外的 `minId` 参数来指定返回的起始子楼层.
注意, 子楼层是越翻越大的.
如果一个根评论下面有很多个喷子在互喷, 会导致看不清, 客户端上有一个按钮 "查看对话" 就是解决这个问题的.
```kotlin
val chatList = bilibiliClient.mainAPI.chatList(oid = 34175504, root = 1136310360, dialog = 1136351035).await()
```
`root` 为根评论 ID, `dialog` 为父评论 ID.
`minFloor` 控制分页, 原理同上.
番剧下面的评论用一样的方式获取.
## 获得一个视频的弹幕
看评论自然不够刺激, 我们想看到弹幕!
获取弹幕非常简单
```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)
}
```
注意, 弹幕的解析是惰性的, `danmakuList` 是一个 `Sequence`. 如果同时持有很多未用完的 `danmakuList` 的引用可能会造成大量内存浪费.
客户端的弹幕屏蔽设置是对弹幕中的 `user` 属性做的. 而实际上 `danmaku.user` 是一个字符串.
这个字符串是 用户ID 的 `CRC32` 的校验和.
众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.
在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法完全确定一条弹幕到底是哪个用户发送的.
如果想获得发送这条弹幕的所有可能的用户的 ID, 可以通过以下方法:
```kotlin
val possibleUserIds = danmaku.calculatePossibleUserIds()
```
返回一个 `List<Int>`, 内容为所有可能的用户 ID(至少有一个).
注意, 第一次使用 `CRC反查` 功能将花费大约 `300ms` 来生成彩虹表, 如果想手动初始化请使用以下代码
```kotlin
Crc32Cracker
```
(`Crc32Cracker` 是一个惰性初始化的单例)
通常情况下, 一次 `CRC反查` 耗时大约 `1ms`.
由于这是一个比较耗时的操作, 请不要每条弹幕都如此操作(相比较 6000 条弹幕的解析只需要 `150ms`).
番剧的弹幕同理.
## 发送视频弹幕
光看不发憋着慌, 我们来发送一条视频弹幕:
```kotlin
bilibiliClient.mainAPI.sendDanmaku(aid = 40675923, cid = 71438168, progress = 2297, message = "2333").await()
```
其中 `progress` 是播放器时间, 其他观众将看到你的弹幕在视频的此处出现, 单位为毫秒.
`message` 应该是有长度限制的, 但是没有测过.
如果不确定视频的长度, 需要从[视频播放地址的 API](#获取视频播放地址) 中的 `data.timelength` 来获得, 单位也是毫秒.
## 获取直播弹幕
刚进入直播间时, 立即看到的十条弹幕实际上是最近的历史弹幕, 通过以下方式来获取
```kotlin
bilibiliClient.liveAPI.roomMessage(roomId).await()
```
接下来的弹幕都是实时弹幕, 直播间实时弹幕通过 `Websocket` 来推送.
```kotlin
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`):
```json
{"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` 的数据是这样的
```json
{"cmd":"WELCOME","data":{"uid":110208099,"uname":"霸刀宋壹i","is_admin":false,"svip":1}}
```
各种 `Command` 数据包的结构经常改变, 因此不提供实体类.
由于 `DANMU_MSG` 的数据结构太过意识流, 因此提供了额外的辅助工具来方便地解析它.
`DanmakuMessage` 是一个 `inline class` 请不要对其进行太过复杂的操作.
```kotlin
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/直播弹幕](record/直播弹幕) 文件夹.
注意, `onPopularityPacket`, `onCommandPacket` 这些回调不能进行耗时操作.
关闭连接
```kotlin
job.cancel()
```
## 发送直播弹幕
在直播间里发送弹幕也非常简单(必须先登陆)
```kotlin
liveClient.sendMessage("我上我也行").await()
```
注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回(`code` 为 0).
完全正常返回时(弹幕正确的被发送了), 返回内容中的 `message` 为一个空字符串.
如果不为空字符串, 则表示不完全正常
例如返回内容的 `message` 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 确实是 0.
其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.
正常返回时, 就算不完全正常, 客户端也会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).
需要额外判断返回的 `message` 是否为空字符串来确认这条弹幕有没有被正确发送.
# License
GPL V3

View File

@ -1,109 +1,150 @@
buildscript {
ext {
kotlin_version = '1.3.21'
kotlin_coroutines_version = '1.1.1'
ktor_version = '1.1.3'
jvm_target = JavaVersion.VERSION_1_8
}
repositories {
gradlePluginPortal()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
group = 'com.hiczp'
version = '0.0.22'
description = 'Bilibili android client API library written in Java'
version = '0.1.0'
description = 'Bilibili Android client API library for Kotlin'
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'kotlin'
apply plugin: 'maven-publish'
apply plugin: 'signing'
sourceCompatibility = 1.8
repositories {
mavenCentral()
mavenLocal()
}
//kotlin
dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: kotlin_coroutines_version
}
compileKotlin {
kotlinOptions {
jvmTarget = jvm_target
freeCompilerArgs = ["-Xjvm-default=enable", "-Xuse-experimental=kotlin.Experimental", "-XXLanguage:+InlineClasses"]
}
}
compileTestKotlin {
kotlinOptions.jvmTarget = jvm_target
}
//logging
dependencies {
// https://mvnrepository.com/artifact/io.github.microutils/kotlin-logging
compile group: 'io.github.microutils', name: 'kotlin-logging', version: '1.6.25'
// https://mvnrepository.com/artifact/org.slf4j/slf4j-simple
testCompile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.26'
}
//http
dependencies {
// https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit
compile group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.4.0'
compile group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.5.0'
// https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson
compile group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.4.0'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
compile group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.5.0'
// https://mvnrepository.com/artifact/com.github.salomonbrys.kotson/kotson
compile group: 'com.github.salomonbrys.kotson', name: 'kotson', version: '2.5.0'
// https://mvnrepository.com/artifact/com.jakewharton.retrofit/retrofit2-kotlin-coroutines-adapter
compile group: 'com.jakewharton.retrofit', name: 'retrofit2-kotlin-coroutines-adapter', version: '0.9.2'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.11.0'
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
// https://mvnrepository.com/artifact/io.netty/netty-all
compile group: 'io.netty', name: 'netty-all', version: '4.1.29.Final'
// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '26.0-jre'
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.14.0'
}
//ktor
dependencies {
// https://mvnrepository.com/artifact/junit/junit
testCompile group: 'junit', name: 'junit', version: '4.12'
// https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12
testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
// https://mvnrepository.com/artifact/io.ktor/ktor-client-websocket
compile group: 'io.ktor', name: 'ktor-client-websocket', version: ktor_version
// https://mvnrepository.com/artifact/io.ktor/ktor-client-cio
compile group: 'io.ktor', name: 'ktor-client-cio', version: ktor_version
}
task sourcesJar(type: Jar, dependsOn: classes) {
description 'Package source code to jar,'
classifier = 'sources'
//checksum
dependencies {
// https://mvnrepository.com/artifact/com.hiczp/crc32-crack
compile group: 'com.hiczp', name: 'crc32-crack', version: '1.0'
}
//unit test
dependencies {
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.4.1'
}
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
archiveClassifier = 'sources'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
description 'Package javadoc to jar,'
classifier = 'javadoc'
task javadocJar(type: Jar) {
from javadoc
archiveClassifier = 'javadoc'
}
artifacts {
archives sourcesJar
archives javadocJar
}
signing {
required { gradle.taskGraph.hasTask(uploadArchives) }
sign configurations.archives
}
uploadArchives {
publishing {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment ->
signing.signPom(deployment)
maven {
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username = project.properties.ossUsername
password = project.properties.ossPassword
}
}
}
if (!project.hasProperty('ossUsername')) {
ext.ossUsername = ''
}
if (!project.hasProperty('ossPassword')) {
ext.ossPassword = ''
}
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') {
authentication(userName: ossUsername, password: ossPassword)
}
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom.project {
name project.name
description project.description
url 'https://github.com/czp3009/bilibili-api'
scm {
connection 'scm:git@github.com:czp3009/bilibili-api.git'
developerConnection 'scm:git@github.com:czp3009/bilibili-api.git'
url 'git@github.com:czp3009/bilibili-api.git'
}
pom {
name = project.name
description = project.description
url = 'https://github.com/czp3009/bilibili-api'
licenses {
license {
name 'GNU GENERAL PUBLIC LICENSE Version 3'
url 'https://www.gnu.org/licenses/gpl-3.0.txt'
name = 'GNU GENERAL PUBLIC LICENSE Version 3'
url = 'https://www.gnu.org/licenses/gpl-3.0.txt'
}
}
developers {
developer {
id 'czp'
//noinspection SpellCheckingInspection
name 'ZhiPeng Chen'
email 'czp3009@gmail.com'
url 'https://www.hiczp.com/'
id = 'czp3009'
name = 'czp3009'
email = 'czp3009@gmail.com'
url = 'https://www.hiczp.com'
}
}
scm {
connection = 'scm:git:git://github.com/czp3009/bilibili-api.git'
developerConnection = 'scm:git:ssh://github.com/czp3009/bilibili-api.git'
url = 'https://github.com/czp3009/bilibili-api'
}
}
}
}
}
signing {
sign publishing.publications.mavenJava
}

Binary file not shown.

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip

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,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,14 @@
{
"cmd": "COMBO_END",
"data": {
"uname": "by_a_second",
"r_uname": "黑桐谷歌",
"combo_num": 3,
"price": 3000,
"gift_name": "给代打的礼物",
"gift_id": 30051,
"start_time": 1553410146,
"end_time": 1553410148,
"guard_level": 0
}
}

View File

@ -0,0 +1,12 @@
{
"cmd": "COMBO_SEND",
"data": {
"uid": 16811396,
"uname": "by_a_second",
"combo_num": 3,
"gift_name": "给代打的礼物",
"gift_id": 30051,
"action": "赠送",
"combo_id": "gift:combo_id:16811396:43536:30051:1553410146.471"
}
}

View File

@ -0,0 +1,54 @@
{
"cmd": "DANMU_MSG",
"info": [
[
0,
1,
25,
16750592,
1553368447,
1772673920,
0,
"169cc1f9",
0,
0,
0
],
"这头衔永久的?",
[
9973581,
"丧糕菌",
0,
1,
1,
10000,
1,
""
],
[
17,
"丧病",
"扎双马尾的丧尸",
48499,
16752445,
""
],
[
42,
0,
16746162,
13011
],
[
"title-198-1",
"title-198-1"
],
0,
0,
null,
{
"ts": 1553368447,
"ct": "98688F2F"
}
]
}

View File

@ -0,0 +1,22 @@
{
"cmd": "ENTRY_EFFECT",
"data": {
"id": 4,
"uid": 3007159,
"target_id": 43536,
"mock_effect": 0,
"face": "https://i0.hdslb.com/bfs/face/7c071f180a20512eba29e80bb13d1c8a3fe3916a.jpg",
"privilege_type": 3,
"copy_writing": "欢迎舰长 <%goodby...%> 进入直播间",
"copy_color": "",
"highlight_color": "#E6FF00",
"priority": 70,
"basemap_url": "https://i0.hdslb.com/bfs/live/1fa3cc06258e16c0ac4c209e2645fda3c2791894.png",
"show_avatar": 1,
"effective_time": 2,
"web_basemap_url": "",
"web_effective_time": 0,
"web_effect_close": 0,
"web_close_time": 0
}
}

View File

@ -0,0 +1,14 @@
{
"cmd": "GUARD_BUY",
"data": {
"uid": 1781654,
"username": "renbye",
"guard_level": 3,
"num": 1,
"price": 198000,
"gift_id": 10003,
"gift_name": "舰长",
"start_time": 1553429698,
"end_time": 1553429698
}
}

View File

@ -0,0 +1,27 @@
{
"cmd": "GUARD_LOTTERY_START",
"data": {
"id": 955580,
"roomid": 1029,
"message": "renbye 在【1029】购买了舰长请前往抽奖",
"type": "guard",
"privilege_type": 3,
"link": "https://live.bilibili.com/1029",
"payflow_id": "gds_74e19a449c1fdaaa73_201903",
"lottery": {
"id": 955580,
"sender": {
"uid": 1781654,
"uname": "renbye",
"face": "http://i1.hdslb.com/bfs/face/0b7a8be6e5d2a89a7de7ccd211a529599f03284e.jpg"
},
"keyword": "guard",
"privilege_type": 3,
"time": 1200,
"status": 1,
"mobile_display_mode": 2,
"mobile_static_asset": "",
"mobile_animation_asset": ""
}
}
}

View File

@ -0,0 +1,9 @@
{
"cmd": "GUARD_MSG",
"msg": "用户 :?鱼仔是橙子的小祖宗:? 在主播 鱼仔一点都不困 的直播间开通了总督",
"msg_new": "<%鱼仔是橙子的小祖宗%> 在 <%鱼仔一点都不困%> 的房间开通了总督并触发了抽奖点击前往TA的房间去抽奖吧",
"url": "https://live.bilibili.com/46744",
"roomid": 46744,
"buy_type": 1,
"broadcast_type": 0
}

View File

@ -0,0 +1,37 @@
{
"cmd": "NOTICE_MSG",
"full": {
"head_icon": "http://i0.hdslb.com/bfs/live/b29add66421580c3e680d784a827202e512a40a0.webp",
"tail_icon": "http://i0.hdslb.com/bfs/live/822da481fdaba986d738db5d8fd469ffa95a8fa1.webp",
"head_icon_fa": "http://i0.hdslb.com/bfs/live/49869a52d6225a3e70bbf1f4da63f199a95384b2.png",
"tail_icon_fa": "http://i0.hdslb.com/bfs/live/38cb2a9f1209b16c0f15162b0b553e3b28d9f16f.png",
"head_icon_fan": 24,
"tail_icon_fan": 4,
"background": "#66A74EFF",
"color": "#FFFFFFFF",
"highlight": "#FDFF2FFF",
"time": 20
},
"half": {
"head_icon": "http://i0.hdslb.com/bfs/live/ec9b374caec5bd84898f3780a10189be96b86d4e.png",
"tail_icon": "",
"background": "#85B971FF",
"color": "#FFFFFFFF",
"highlight": "#FDFF2FFF",
"time": 15
},
"side": {
"head_icon": "http://i0.hdslb.com/bfs/live/e41c7e12b1e08724d2ab2f369515132d30fe1ef7.png",
"background": "#F4FDE8FF",
"color": "#79B48EFF",
"highlight": "#388726FF",
"border": "#A9DA9FFF"
},
"roomid": 12124934,
"real_roomid": 12124934,
"msg_common": "全区广播:<%小苏棠の大脸猫脸大%>送给<%小苏棠i%>1个小电视飞船点击前往TA的房间去抽奖吧",
"msg_self": "全区广播:<%小苏棠の大脸猫脸大%>送给<%小苏棠i%>1个小电视飞船快来抽奖吧",
"link_url": "http://live.bilibili.com/12124934?live_lottery_type=1&broadcast_type=0&from=28003&extra_jump_from=28003",
"msg_type": 2,
"shield_uid": -1
}

View File

@ -0,0 +1,11 @@
{
"cmd": "ROOM_BLOCK_MSG",
"uid": 8305711,
"uname": "RMT0v0",
"data": {
"uid": 8305711,
"uname": "RMT0v0",
"operator": 1
},
"roomid": 1029
}

View File

@ -0,0 +1,11 @@
{
"cmd": "ROOM_RANK",
"data": {
"roomid": 1029,
"rank_desc": "单机小时榜 13",
"color": "#FB7299",
"h5_url": "https://live.bilibili.com/p/html/live-app-rankcurrent/index.html?is_live_half_webview=1&hybrid_half_ui=1,5,85p,70p,FFE293,0,30,100,10;2,2,320,100p,FFE293,0,30,100,0;4,2,320,100p,FFE293,0,30,100,0;6,5,65p,60p,FFE293,0,30,100,10;5,5,55p,60p,FFE293,0,30,100,10;3,5,85p,70p,FFE293,0,30,100,10;7,5,65p,60p,FFE293,0,30,100,10;&anchor_uid=43536&rank_type=master_realtime_area_hour&area_hour=1&area_v2_id=245&area_v2_parent_id=6",
"web_url": "https://live.bilibili.com/blackboard/room-current-rank.html?rank_type=master_realtime_area_hour&area_hour=1&area_v2_id=245&area_v2_parent_id=6",
"timestamp": 1553409901
}
}

View File

@ -0,0 +1,7 @@
{
"cmd": "ROOM_REAL_TIME_MESSAGE_UPDATE",
"data": {
"roomid": 23058,
"fans": 300958
}
}

View File

@ -2,8 +2,8 @@
"cmd": "ROOM_SILENT_ON",
"data": {
"type": "level",
"level": 1,
"second": 1520424615
"level": 20,
"second": -1
},
"roomid": 5279
}
"roomid": 1029
}

View File

@ -0,0 +1,44 @@
{
"cmd": "SEND_GIFT",
"data": {
"giftName": "辣条",
"num": 62,
"uname": "萌萌哒熊宝宝",
"face": "http://i0.hdslb.com/bfs/face/33570159b6bf28e01249b80d3f9f05fa117779c1.jpg",
"guard_level": 0,
"rcost": 123266565,
"uid": 10007727,
"top_list": [],
"timestamp": 1553369191,
"giftId": 1,
"giftType": 0,
"action": "喂食",
"super": 0,
"super_gift_num": 0,
"price": 100,
"rnd": "940348243",
"newMedal": 0,
"newTitle": 0,
"medal": [],
"title": "",
"beatId": "",
"biz_source": "live",
"metadata": "",
"remain": 0,
"gold": 0,
"silver": 0,
"eventScore": 0,
"eventNum": 0,
"smalltv_msg": [],
"specialGift": null,
"notice_msg": [],
"capsule": null,
"addFollow": 0,
"effect_block": 1,
"coin_type": "silver",
"total_coin": 6200,
"effect": 0,
"tag_image": "",
"user_count": 0
}
}

View File

@ -0,0 +1,14 @@
{
"cmd": "SYS_MSG",
"msg": "小苏棠の大脸猫脸大:?送给:?小苏棠i:?1个小电视飞船点击前往TA的房间去抽奖吧",
"msg_text": "小苏棠の大脸猫脸大:?送给:?小苏棠i:?1个小电视飞船点击前往TA的房间去抽奖吧",
"msg_common": "全区广播:<%小苏棠の大脸猫脸大%>送给<%小苏棠i%>1个小电视飞船点击前往TA的房间去抽奖吧",
"msg_self": "全区广播:<%小苏棠の大脸猫脸大%>送给<%小苏棠i%>1个小电视飞船快来抽奖吧",
"rep": 1,
"styleType": 2,
"url": "http://live.bilibili.com/12124934",
"roomid": 12124934,
"real_roomid": 12124934,
"rnd": 1553410466,
"broadcast_type": 0
}

View File

@ -0,0 +1,10 @@
{
"cmd": "USER_TOAST_MSG",
"data": {
"op_type": 1,
"uid": 1781654,
"username": "renbye",
"guard_level": 3,
"is_show": 0
}
}

View File

@ -0,0 +1,9 @@
{
"cmd": "WELCOME",
"data": {
"uid": 3173595,
"uname": "百杜Paido",
"is_admin": false,
"svip": 1
}
}

View File

@ -0,0 +1,8 @@
{
"cmd": "WELCOME_GUARD",
"data": {
"uid": 3007159,
"username": "goodbyecaroline",
"guard_level": 3
}
}

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 +0,0 @@
package com.hiczp.bilibili.api;
public class BaseUrlDefinition {
public static final String PASSPORT = "https://passport.bilibili.com/";
public static final String LIVE = "https://api.live.bilibili.com/";
}

View File

@ -1,439 +0,0 @@
package com.hiczp.bilibili.api;
import com.hiczp.bilibili.api.interceptor.*;
import com.hiczp.bilibili.api.live.LiveService;
import com.hiczp.bilibili.api.live.socket.LiveClient;
import com.hiczp.bilibili.api.passport.CaptchaService;
import com.hiczp.bilibili.api.passport.PassportService;
import com.hiczp.bilibili.api.passport.SsoService;
import com.hiczp.bilibili.api.passport.entity.InfoEntity;
import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity;
import com.hiczp.bilibili.api.passport.entity.LogoutResponseEntity;
import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity;
import com.hiczp.bilibili.api.passport.exception.CaptchaMismatchException;
import com.hiczp.bilibili.api.provider.*;
import com.hiczp.bilibili.api.web.BilibiliWebAPI;
import com.hiczp.bilibili.api.web.BrowserProperties;
import com.hiczp.bilibili.api.web.cookie.SimpleCookieJar;
import io.netty.channel.EventLoopGroup;
import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class BilibiliAPI implements BilibiliServiceProvider, BilibiliCaptchaProvider, BilibiliSsoProvider, BilibiliWebAPIProvider, LiveClientProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(BilibiliAPI.class);
private final Long apiInitTime = Instant.now().getEpochSecond(); //记录当前类被实例化的时间
private final BilibiliClientProperties bilibiliClientProperties;
private final BilibiliAccount bilibiliAccount;
private Boolean autoRefreshToken = true;
//用于阻止进行多次错误的 refreshToken 操作
private String invalidToken;
private String invalidRefreshToken;
private PassportService passportService;
private CaptchaService captchaService;
private LiveService liveService;
private BilibiliWebAPI bilibiliWebAPI;
public BilibiliAPI() {
this.bilibiliClientProperties = BilibiliClientProperties.defaultSetting();
this.bilibiliAccount = BilibiliAccount.emptyInstance();
}
public BilibiliAPI(BilibiliClientProperties bilibiliClientProperties) {
this.bilibiliClientProperties = bilibiliClientProperties;
this.bilibiliAccount = BilibiliAccount.emptyInstance();
}
public BilibiliAPI(BilibiliSecurityContext bilibiliSecurityContext) {
this.bilibiliClientProperties = BilibiliClientProperties.defaultSetting();
this.bilibiliAccount = new BilibiliAccount(bilibiliSecurityContext);
}
public BilibiliAPI(BilibiliClientProperties bilibiliClientProperties, BilibiliSecurityContext bilibiliSecurityContext) {
this.bilibiliClientProperties = bilibiliClientProperties;
this.bilibiliAccount = new BilibiliAccount(bilibiliSecurityContext);
}
@Override
public PassportService getPassportService() {
if (passportService == null) {
passportService = getPassportService(Collections.emptyList(), HttpLoggingInterceptor.Level.BASIC);
}
return passportService;
}
public PassportService getPassportService(@Nonnull List<Interceptor> interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//TODO 不明确客户端访问 passport.bilibili.com 时使用的 UA
okHttpClientBuilder
.addInterceptor(new AddFixedHeadersInterceptor(
"Buvid", bilibiliClientProperties.getBuvId(),
"User-Agent", "bili-universal/6560 CFNetwork/894 Darwin/17.4.0" //这是 IOS UA
))
.addInterceptor(new AddDynamicHeadersInterceptor(
() -> "Display-ID", () -> String.format("%s-%d", bilibiliAccount.getUserId() == null ? bilibiliClientProperties.getBuvId() : bilibiliAccount.getUserId(), apiInitTime)
))
.addInterceptor(new AddFixedParamsInterceptor(
"build", bilibiliClientProperties.getBuild(),
"mobi_app", "android",
"platform", "android"
))
.addInterceptor(new AddDynamicParamsInterceptor(
() -> "ts", () -> Long.toString(Instant.now().getEpochSecond())
))
.addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties))
.addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties))
.addInterceptor(new ErrorResponseConverterInterceptor());
interceptors.forEach(okHttpClientBuilder::addInterceptor);
okHttpClientBuilder
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel));
return new Retrofit.Builder()
.baseUrl(BaseUrlDefinition.PASSPORT)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClientBuilder.build())
.build()
.create(PassportService.class);
}
@Override
public LiveService getLiveService() {
if (liveService == null) {
liveService = getLiveService(Collections.emptyList(), HttpLoggingInterceptor.Level.BASIC);
}
return liveService;
}
public LiveService getLiveService(@Nonnull List<Interceptor> interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
okHttpClientBuilder
.addInterceptor(new AddFixedHeadersInterceptor(
"Buvid", bilibiliClientProperties.getBuvId(),
"User-Agent", String.format("Mozilla/5.0 BiliDroid/%s (bbcallen@gmail.com)", bilibiliClientProperties.getSimpleVersion()),
"Device-ID", bilibiliClientProperties.getHardwareId()
))
.addInterceptor(new AddDynamicHeadersInterceptor(
//Display-ID 的值在未登录前为 Buvid-客户端启动时间, 在登录后为 mid-客户端启动时间
() -> "Display-ID", () -> String.format("%s-%d", bilibiliAccount.getUserId() == null ? bilibiliClientProperties.getBuvId() : bilibiliAccount.getUserId(), apiInitTime)
))
.addInterceptor(new AddFixedParamsInterceptor(
"_device", "android",
"_hwid", bilibiliClientProperties.getHardwareId(),
"actionKey", "appkey",
"build", bilibiliClientProperties.getBuild(),
"mobi_app", "android",
"platform", "android",
"scale", bilibiliClientProperties.getScale(),
"src", "google",
"version", bilibiliClientProperties.getVersion()
))
.addInterceptor(new AddDynamicParamsInterceptor(
() -> "ts", () -> Long.toString(Instant.now().getEpochSecond()),
() -> "trace_id", () -> new SimpleDateFormat("yyyyMMddHHmm000ss").format(new Date())
))
.addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties))
.addInterceptor(new AutoRefreshTokenInterceptor(
this,
ServerErrorCode.Common.UNAUTHORIZED,
ServerErrorCode.Live.USER_NO_LOGIN,
ServerErrorCode.Live.PLEASE_LOGIN,
ServerErrorCode.Live.PLEASE_LOGIN0,
ServerErrorCode.Live.NO_LOGIN
))
.addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount))
.addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties))
.addInterceptor(new ErrorResponseConverterInterceptor());
interceptors.forEach(okHttpClientBuilder::addInterceptor);
okHttpClientBuilder
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel));
return new Retrofit.Builder()
.baseUrl(BaseUrlDefinition.LIVE)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClientBuilder.build())
.build()
.create(LiveService.class);
}
@Override
public CaptchaService getCaptchaService() {
if (captchaService == null) {
captchaService = getCaptchaService(Collections.emptyList(), HttpLoggingInterceptor.Level.BASIC);
}
return captchaService;
}
public CaptchaService getCaptchaService(@Nonnull List<Interceptor> interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
interceptors.forEach(okHttpClientBuilder::addInterceptor);
okHttpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(logLevel));
return new Retrofit.Builder()
.baseUrl(BaseUrlDefinition.PASSPORT)
.client(okHttpClientBuilder.build())
.build()
.create(CaptchaService.class);
}
public SsoService getSsoService() {
return getSsoService(new SimpleCookieJar());
}
//sso 需要保存 cookie, 不对 SsoService 进行缓存
@Override
public SsoService getSsoService(CookieJar cookieJar) {
return getSsoService(cookieJar, Collections.emptyList(), HttpLoggingInterceptor.Level.BASIC);
}
public SsoService getSsoService(@Nonnull CookieJar cookieJar, @Nonnull List<Interceptor> interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) {
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
okHttpClientBuilder
.cookieJar(cookieJar)
.addInterceptor(new AddFixedParamsInterceptor(
"build", bilibiliClientProperties.getBuild(),
"mobi_app", "android",
"platform", "android"
))
.addInterceptor(new AddDynamicParamsInterceptor(
() -> "ts", () -> Long.toString(Instant.now().getEpochSecond())
))
.addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount))
.addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties))
.addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties));
interceptors.forEach(okHttpClientBuilder::addInterceptor);
okHttpClientBuilder
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel));
return new Retrofit.Builder()
.baseUrl(BaseUrlDefinition.PASSPORT)
.client(okHttpClientBuilder.build())
.build()
.create(SsoService.class);
}
@Override
public HttpUrl getSsoUrl(@Nullable String goUrl) {
CancelRequestInterceptor cancelRequestInterceptor = new CancelRequestInterceptor();
try {
getSsoService(new SimpleCookieJar(), Collections.singletonList(cancelRequestInterceptor), HttpLoggingInterceptor.Level.BASIC)
.sso(goUrl)
.execute();
} catch (IOException ignored) {
}
return cancelRequestInterceptor.getRequest().url();
}
@Override
public Map<String, List<Cookie>> toCookies() throws IOException {
//用这个地址是因为这个地址一定不会改变( B站 未来的更新中)并且很省流量
return toCookies(BaseUrlDefinition.PASSPORT + "api/oauth2/getKey");
}
public Map<String, List<Cookie>> toCookies(@Nullable String goUrl) throws IOException {
SimpleCookieJar simpleCookieJar = new SimpleCookieJar();
getSsoService(simpleCookieJar).sso(goUrl).execute();
return simpleCookieJar.getCookiesMap();
}
@Override
public BilibiliWebAPI getBilibiliWebAPI() throws IOException {
return getBilibiliWebAPI(BrowserProperties.defaultSetting());
}
public BilibiliWebAPI getBilibiliWebAPI(BrowserProperties browserProperties) throws IOException {
if (bilibiliWebAPI == null) {
bilibiliWebAPI = new BilibiliWebAPI(browserProperties, toCookies());
}
return bilibiliWebAPI;
}
public LoginResponseEntity login(@Nonnull String username, @Nonnull String password) throws IOException, LoginException, CaptchaMismatchException {
return login(username, password, null, null);
}
public synchronized LoginResponseEntity login(@Nonnull String username,
@Nonnull String password,
String captcha,
String cookie) throws IOException, LoginException, CaptchaMismatchException {
LOGGER.info("Login attempting with username '{}'", username);
LoginResponseEntity loginResponseEntity = BilibiliSecurityHelper.login(
this,
username,
password,
captcha,
cookie
);
//判断返回值
switch (loginResponseEntity.getCode()) {
case ServerErrorCode.Common.OK: {
}
break;
case ServerErrorCode.Passport.USERNAME_OR_PASSWORD_INVALID: {
throw new LoginException("username or password invalid");
}
case ServerErrorCode.Passport.CANT_DECRYPT_RSA_PASSWORD: {
throw new LoginException("password error or hash expired");
}
case ServerErrorCode.Passport.CAPTCHA_NOT_MATCH: {
throw new CaptchaMismatchException("captcha mismatch");
}
default: {
throw new IOException(loginResponseEntity.getMessage());
}
}
bilibiliAccount.copyFrom(loginResponseEntity.toBilibiliAccount());
bilibiliWebAPI = null;
LOGGER.info("Login succeed with username: {}", username);
return loginResponseEntity;
}
public synchronized RefreshTokenResponseEntity refreshToken() throws IOException, LoginException {
if (isCurrentTokenAndRefreshTokenInvalid()) {
throw new LoginException("access token or refresh token not been set yet or invalid");
}
LOGGER.info("RefreshToken attempting with userId '{}'", bilibiliAccount.getUserId());
RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken(
this,
bilibiliAccount.getAccessToken(),
bilibiliAccount.getRefreshToken()
);
switch (refreshTokenResponseEntity.getCode()) {
case ServerErrorCode.Common.OK: {
}
break;
case ServerErrorCode.Passport.NO_LOGIN: {
markCurrentTokenAndRefreshTokenInvalid();
throw new LoginException("access token invalid");
}
case ServerErrorCode.Passport.REFRESH_TOKEN_NOT_MATCH: {
markCurrentTokenAndRefreshTokenInvalid();
throw new LoginException("access token and refresh token mismatch");
}
default: {
throw new IOException(refreshTokenResponseEntity.getMessage());
}
}
bilibiliAccount.copyFrom(refreshTokenResponseEntity.toBilibiliAccount());
bilibiliWebAPI = null;
LOGGER.info("RefreshToken succeed with userId: {}", bilibiliAccount.getUserId());
return refreshTokenResponseEntity;
}
public synchronized LogoutResponseEntity logout() throws IOException, LoginException {
LOGGER.info("Logout attempting with userId '{}'", bilibiliAccount.getUserId());
Long userId = bilibiliAccount.getUserId();
LogoutResponseEntity logoutResponseEntity = BilibiliSecurityHelper.logout(this, bilibiliAccount.getAccessToken());
switch (logoutResponseEntity.getCode()) {
case ServerErrorCode.Common.OK: {
}
break;
case ServerErrorCode.Passport.NO_LOGIN: {
throw new LoginException("access token invalid");
}
default: {
throw new IOException(logoutResponseEntity.getMessage());
}
}
bilibiliAccount.reset();
LOGGER.info("Logout succeed with userId: {}", userId);
return logoutResponseEntity;
}
public InfoEntity getAccountInfo() throws IOException, LoginException {
InfoEntity infoEntity = getPassportService()
.getInfo(bilibiliAccount.getAccessToken())
.execute()
.body();
switch (infoEntity.getCode()) {
case ServerErrorCode.Common.OK: {
}
break;
case ServerErrorCode.Passport.NO_LOGIN: {
throw new LoginException("no login");
}
default: {
throw new IOException(infoEntity.getMessage());
}
}
return infoEntity;
}
/**
* 2018-05-11 现在用假的房间 Id 也能正常连接弹幕推送服务器
*
* @param eventLoopGroup 用于连接弹幕推送服务器的 EventLoop
* @param roomId 房间 ID, 可以是真 ID 也可以是假 ID
* @param isRealRoomId 使用的 roomId 是否是真 ID
* @return LiveClient 实例
*/
@Override
public LiveClient getLiveClient(EventLoopGroup eventLoopGroup, long roomId, boolean isRealRoomId) {
return bilibiliAccount.getUserId() == null ?
new LiveClient(this, eventLoopGroup, roomId, isRealRoomId) :
new LiveClient(this, eventLoopGroup, roomId, isRealRoomId, bilibiliAccount.getUserId());
}
private void markCurrentTokenAndRefreshTokenInvalid() {
invalidToken = bilibiliAccount.getAccessToken();
invalidRefreshToken = bilibiliAccount.getRefreshToken();
}
public boolean isCurrentTokenAndRefreshTokenInvalid() {
//如果 accessToken refreshToken 没有被设置或者已经尝试过并明确他们是无效的
return bilibiliAccount.getAccessToken() == null ||
bilibiliAccount.getRefreshToken() == null ||
(bilibiliAccount.getAccessToken().equals(invalidToken) && bilibiliAccount.getRefreshToken().equals(invalidRefreshToken));
}
public BilibiliClientProperties getBilibiliClientProperties() {
return bilibiliClientProperties;
}
public BilibiliAccount getBilibiliAccount() {
return bilibiliAccount;
}
public boolean isAutoRefreshToken() {
return autoRefreshToken;
}
public BilibiliAPI setAutoRefreshToken(boolean autoRefreshToken) {
this.autoRefreshToken = autoRefreshToken;
return this;
}
}

View File

@ -1,97 +0,0 @@
package com.hiczp.bilibili.api;
public class BilibiliAccount implements BilibiliSecurityContext {
private String accessToken;
private String refreshToken;
private Long userId;
private Long expirationTime;
private Long loginTime;
private BilibiliAccount() {
}
public BilibiliAccount(String accessToken, String refreshToken, Long userId, Long expirationTime, Long loginTime) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.userId = userId;
this.expirationTime = expirationTime;
this.loginTime = loginTime;
}
public BilibiliAccount(BilibiliSecurityContext bilibiliSecurityContext) {
copyFrom(bilibiliSecurityContext);
}
public static BilibiliAccount emptyInstance() {
return new BilibiliAccount();
}
public BilibiliAccount copyFrom(BilibiliSecurityContext bilibiliSecurityContext) {
this.accessToken = bilibiliSecurityContext.getAccessToken();
this.refreshToken = bilibiliSecurityContext.getRefreshToken();
this.userId = bilibiliSecurityContext.getUserId();
this.expirationTime = bilibiliSecurityContext.getExpirationTime();
this.loginTime = bilibiliSecurityContext.getLoginTime();
return this;
}
public BilibiliAccount reset() {
this.accessToken = null;
this.refreshToken = null;
this.userId = null;
this.expirationTime = null;
this.loginTime = null;
return this;
}
@Override
public String getAccessToken() {
return accessToken;
}
public BilibiliAccount setAccessToken(String accessToken) {
this.accessToken = accessToken;
return this;
}
@Override
public String getRefreshToken() {
return refreshToken;
}
public BilibiliAccount setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
@Override
public Long getUserId() {
return userId;
}
public BilibiliAccount setUserId(Long userId) {
this.userId = userId;
return this;
}
@Override
public Long getExpirationTime() {
return expirationTime;
}
public BilibiliAccount setExpirationTime(Long expirationTime) {
this.expirationTime = expirationTime;
return this;
}
@Override
public Long getLoginTime() {
return loginTime;
}
public BilibiliAccount setLoginTime(Long loginTime) {
this.loginTime = loginTime;
return this;
}
}

View File

@ -1,91 +0,0 @@
package com.hiczp.bilibili.api;
import javax.annotation.Nonnull;
public class BilibiliClientProperties {
private String appKey = "1d8b6e7d45233436";
private String appSecret = "560c52ccd288fed045859ed18bffd973";
private String hardwareId = "JxdyESFAJkcjEicQbBBsCTlbal5uX2Y";
private String scale = "xxhdpi";
private String version = "5.15.0.515000";
private String simpleVersion;
private String build;
private String buvId = "JxdyESFAJkcjEicQbBBsCTlbal5uX2Yinfoc";
private BilibiliClientProperties() {
onVersionChange();
}
public static BilibiliClientProperties defaultSetting() {
return new BilibiliClientProperties();
}
private void onVersionChange() {
int lastIndexOfDot = version.lastIndexOf(".");
this.simpleVersion = version.substring(0, lastIndexOfDot);
this.build = version.substring(lastIndexOfDot + 1);
}
public String getAppKey() {
return appKey;
}
public BilibiliClientProperties setAppKey(String appKey) {
this.appKey = appKey;
return this;
}
public String getAppSecret() {
return appSecret;
}
public BilibiliClientProperties setAppSecret(String appSecret) {
this.appSecret = appSecret;
return this;
}
public String getHardwareId() {
return hardwareId;
}
public BilibiliClientProperties setHardwareId(String hardwareId) {
this.hardwareId = hardwareId;
return this;
}
public String getScale() {
return scale;
}
public BilibiliClientProperties setScale(String scale) {
this.scale = scale;
return this;
}
public String getVersion() {
return version;
}
public BilibiliClientProperties setVersion(@Nonnull String version) {
this.version = version;
onVersionChange();
return this;
}
public String getSimpleVersion() {
return simpleVersion;
}
public String getBuild() {
return build;
}
public String getBuvId() {
return buvId;
}
public BilibiliClientProperties setBuvId(String buvId) {
this.buvId = buvId;
return this;
}
}

View File

@ -1,13 +0,0 @@
package com.hiczp.bilibili.api;
public interface BilibiliSecurityContext {
String getAccessToken();
String getRefreshToken();
Long getUserId();
Long getExpirationTime();
Long getLoginTime();
}

View File

@ -1,199 +0,0 @@
package com.hiczp.bilibili.api;
import com.hiczp.bilibili.api.passport.entity.KeyEntity;
import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity;
import com.hiczp.bilibili.api.passport.entity.LogoutResponseEntity;
import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity;
import com.hiczp.bilibili.api.provider.BilibiliServiceProvider;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
public class BilibiliSecurityHelper {
/**
* 加密一个明文密码
*
* @param bilibiliServiceProvider BilibiliServiceProvider 实例
* @param password 明文密码
* @return 密文密码
* @throws IOException 网络错误
*/
private static String cipherPassword(@Nonnull BilibiliServiceProvider bilibiliServiceProvider,
@Nonnull String password) throws IOException {
KeyEntity keyEntity = bilibiliServiceProvider.getPassportService().getKey().execute().body();
//服务器返回异常错误码
if (keyEntity.getCode() != 0) {
throw new IOException(keyEntity.getMessage());
}
//构造无备注的 RSA 公钥字符串
String rsaPublicKeyString = Arrays.stream(keyEntity.getData().getKey().split("\n"))
.filter(string -> !string.startsWith("-"))
.collect(Collectors.joining());
//解析 RSA 公钥
PublicKey publicKey;
try {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(rsaPublicKeyString.getBytes()));
publicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
} catch (NoSuchAlgorithmException e) {
throw new Error(e);
} catch (InvalidKeySpecException e) {
throw new IOException("get broken RSA public key");
}
//加密密码
String cipheredPassword;
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
cipheredPassword = new String(
Base64.getEncoder().encode(
cipher.doFinal((keyEntity.getData().getHash() + password).getBytes())
)
);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) {
throw new Error(e);
} catch (InvalidKeyException e) {
throw new IOException("get broken RSA public key");
}
return cipheredPassword;
}
/**
* 计算 sign
*
* @param nameAndValues 传入值为 name1=value1 形式, 传入值必须已经排序. value 必须已经经过 URLEncode
* @param appSecret APP 密钥
* @return sign
*/
public static String calculateSign(@Nonnull List<String> nameAndValues, @Nonnull String appSecret) {
return calculateSign(nameAndValues.stream().collect(Collectors.joining("&")), appSecret);
}
/**
* 计算 sign
*
* @param encodedQuery 已经经过 URLEncode 处理的 Query 参数字符串
* @param appSecret APP 密钥
* @return sign
*/
public static String calculateSign(@Nonnull String encodedQuery, @Nonnull String appSecret) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update((encodedQuery + appSecret).getBytes());
String md5 = new BigInteger(1, messageDigest.digest()).toString(16);
//md5 不满 32 位时左边加 0
return ("00000000000000000000000000000000" + md5).substring(md5.length());
} catch (NoSuchAlgorithmException e) {
throw new Error(e);
}
}
/**
* 向一个 Query 参数字符串中添加 sign
*
* @param nameAndValues 传入值为 name1=value1 形式, 传入值必须已经排序. value 必须已经经过 URLEncode
* @param appSecret APP 密钥
* @return 添加了 sign Query 参数字符串
*/
public static String addSignToQuery(@Nonnull List<String> nameAndValues, @Nonnull String appSecret) {
return addSignToQuery(nameAndValues.stream().collect(Collectors.joining("&")), appSecret);
}
/**
* 向一个 Query 参数字符串中添加 sign
*
* @param encodedQuery 已经经过 URLEncode 处理的 Query 参数字符串
* @param appSecret APP 密钥
* @return 添加了 sign Query 参数字符串
*/
public static String addSignToQuery(@Nonnull String encodedQuery, @Nonnull String appSecret) {
return encodedQuery + String.format("&%s=%s", "sign", calculateSign(encodedQuery, appSecret));
}
/**
* 登录
*
* @param bilibiliServiceProvider BilibiliServiceProvider 实例
* @param username 用户名
* @param password 明文密码
* @return 返回值包含有 token refreshToken
* @throws IOException 网络错误
*/
public static LoginResponseEntity login(@Nonnull BilibiliServiceProvider bilibiliServiceProvider,
@Nonnull String username,
@Nonnull String password) throws IOException {
return login(bilibiliServiceProvider, username, password, null, null);
}
/**
* 带验证码的登录
* 在一段时间内使用错误的密码尝试登录多次, 再次使用这个 IP 地址登录这个账号会被要求验证码
*
* @param bilibiliServiceProvider BilibiliServiceProvider 实例
* @param username 用户名
* @param password 明文密码
* @param captcha 验证码
* @param cookie 与验证码对应的 cookies
* @return 返回值包含有 token refreshToken
* @throws IOException 网络错误
* @see com.hiczp.bilibili.api.passport.CaptchaService
*/
public static LoginResponseEntity login(@Nonnull BilibiliServiceProvider bilibiliServiceProvider,
@Nonnull String username,
@Nonnull String password,
@Nullable String captcha,
@Nullable String cookie) throws IOException {
return bilibiliServiceProvider.getPassportService()
.login(
username,
cipherPassword(bilibiliServiceProvider, password),
captcha,
cookie
).execute()
.body();
}
/**
* 刷新 Token
*
* @param bilibiliServiceProvider BilibiliServiceProvider 实例
* @param accessToken token
* @param refreshToken refreshToken
* @return 返回值包含一个新的 token refreshToken
* @throws IOException 网络错误
*/
public static RefreshTokenResponseEntity refreshToken(@Nonnull BilibiliServiceProvider bilibiliServiceProvider,
@Nonnull String accessToken,
@Nonnull String refreshToken) throws IOException {
return bilibiliServiceProvider.getPassportService().refreshToken(accessToken, refreshToken)
.execute()
.body();
}
/**
* 注销
*
* @param bilibiliServiceProvider BilibiliServiceProvider 实例
* @param accessToken token
* @return 返回 0 表示成功
* @throws IOException 网络错误
*/
public static LogoutResponseEntity logout(@Nonnull BilibiliServiceProvider bilibiliServiceProvider,
@Nonnull String accessToken) throws IOException {
return bilibiliServiceProvider.getPassportService().logout(accessToken)
.execute()
.body();
}
}

View File

@ -1,121 +0,0 @@
package com.hiczp.bilibili.api;
/**
* 不知道为什么错误码都要加负号
* 根据推断, 负数错误码表示是 APP 专用的 API, 正数错误码表示是 Web API 或者共用的 API
*/
public class ServerErrorCode {
/**
* 服务网关上鉴权失败的话, 会返回这些标准错误码
* B站 后台设计很混乱, 不是所有鉴权都在服务网关上完成
*/
public static class Common {
public static final int API_SIGN_INVALID = -3;
public static final int OK = 0;
/**
* 参数错误或已达 API 每日访问限制(比如银瓜子换硬币每日只能访问一次)
*/
public static final int BAD_REQUEST = -400;
public static final int UNAUTHORIZED = -401;
public static final int FORBIDDEN = -403;
public static final int NOT_FOUND = -404;
/**
* 一些 API 在参数错误的情况下也会引起 -500
*/
public static final int INTERNAL_SERVER_ERROR = -500;
}
/**
* 现在 access token 错误统一返回 -101
*/
public static class Passport {
/**
* "access_key not found"
*/
public static final int NO_LOGIN = -101;
/**
* 短时间内进行多次错误的登录将被要求输入验证码
*/
public static final int CAPTCHA_NOT_MATCH = -105;
/**
* 用户名不存在
*/
public static final int USERNAME_OR_PASSWORD_INVALID = -629;
/**
* 密码不可解密或者密码错误
*/
public static final int CANT_DECRYPT_RSA_PASSWORD = -662;
/**
* B站换了错误码, 现在 "access_key not found." 对应 -101
*/
@Deprecated
public static final int ACCESS_TOKEN_NOT_FOUND = -901;
/**
* refreshToken token 不匹配
*/
public static final int REFRESH_TOKEN_NOT_MATCH = -903;
}
/**
* 一些 API 未登录时返回 3, 一些返回 -101, 还有一些返回 401, 在网关上鉴权的 API 返回 -401, 甚至有一些 API 返回 32205 这种奇怪的错误码
*/
public static class Live {
/**
* "invalid params"
*/
public static final int INVALID_PARAMS = 1;
/**
* "user no login"
*/
public static final int USER_NO_LOGIN = 3;
/**
* "请登录"
*/
public static final int PLEASE_LOGIN = 401;
/**
* "每天最多能兑换 1 个"
*/
public static final int FORBIDDEN = 403;
/**
* 送礼物时房间号与用户号不匹配
* "只能送给主播(591)"
*/
public static final int ONLY_CAN_SEND_TO_HOST = 200012;
/**
* 赠送一个不存在的礼物
* "获取包裹数据失败"
*/
public static final int GET_BAG_DATA_FAIL = 200019;
/**
* "请登录"
*/
public static final int PLEASE_LOGIN0 = 32205;
/**
* "message": "invalid request"
* "data": "bad token"
*/
public static final int INVALID_REQUEST = 65530;
/**
* "请先登录"
*/
public static final int NO_LOGIN = -101;
/**
* 访问的房间号不存在
* "message": "Document is not exists."
*/
public static final int DOCUMENT_IS_NOT_EXISTS = -404;
/**
* 搜索时, 关键字字数过少或过多
* "关键字不能小于2个字节或大于50字节"
*/
public static final int KEYWORD_CAN_NOT_LESS_THAN_2_BYTES_OR_GREATER_THAN_50_BYTES = -609;
/**
* 已经领取过这个宝箱
*/
public static final int THIS_SILVER_TASK_ALREADY_TOOK = -903;
/**
* 今天所有的宝箱已经领完
*/
public static final int NO_MORE_SILVER_TASK = -10017;
}
}

View File

@ -1,21 +0,0 @@
package com.hiczp.bilibili.api.exception;
import java.io.IOException;
public class UserCancelRequestException extends IOException {
public UserCancelRequestException() {
}
public UserCancelRequestException(String message) {
super(message);
}
public UserCancelRequestException(String message, Throwable cause) {
super(message, cause);
}
public UserCancelRequestException(Throwable cause) {
super(cause);
}
}

View File

@ -1,30 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.google.common.base.Strings;
import com.hiczp.bilibili.api.BilibiliSecurityContext;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class AddAccessKeyInterceptor implements Interceptor {
private BilibiliSecurityContext bilibiliSecurityContext;
public AddAccessKeyInterceptor(BilibiliSecurityContext bilibiliSecurityContext) {
this.bilibiliSecurityContext = bilibiliSecurityContext;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
String accessKey = bilibiliSecurityContext.getAccessToken();
if (!Strings.isNullOrEmpty(accessKey)) {
httpUrlBuilder.removeAllQueryParameters("access_key")
.addQueryParameter("access_key", accessKey);
}
return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build());
}
}

View File

@ -1,26 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.hiczp.bilibili.api.BilibiliClientProperties;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class AddAppKeyInterceptor implements Interceptor {
private BilibiliClientProperties bilibiliClientDefinition;
public AddAppKeyInterceptor(BilibiliClientProperties bilibiliClientDefinition) {
this.bilibiliClientDefinition = bilibiliClientDefinition;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
return chain.proceed(request.newBuilder().url(
request.url().newBuilder()
.addQueryParameter("appkey", bilibiliClientDefinition.getAppKey())
.build()
).build());
}
}

View File

@ -1,29 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.function.Supplier;
public class AddDynamicHeadersInterceptor implements Interceptor {
private Supplier<String>[] headerAndValues;
@SafeVarargs
public AddDynamicHeadersInterceptor(Supplier<String>... headerAndValues) {
if (headerAndValues.length % 2 != 0) {
throw new IllegalArgumentException("Header must have value");
}
this.headerAndValues = headerAndValues;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder requestBuilder = chain.request().newBuilder();
for (int i = 0; i < headerAndValues.length; i += 2) {
requestBuilder.addHeader(headerAndValues[i].get(), headerAndValues[i + 1].get());
}
return chain.proceed(requestBuilder.build());
}
}

View File

@ -1,31 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.function.Supplier;
public class AddDynamicParamsInterceptor implements Interceptor {
private Supplier<String>[] paramAndValues;
@SafeVarargs
public AddDynamicParamsInterceptor(Supplier<String>... paramAndValues) {
if (paramAndValues.length % 2 != 0) {
throw new IllegalArgumentException("Parameter must have value");
}
this.paramAndValues = paramAndValues;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
for (int i = 0; i < paramAndValues.length; i += 2) {
httpUrlBuilder.addQueryParameter(paramAndValues[i].get(), paramAndValues[i + 1].get());
}
return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build());
}
}

View File

@ -1,27 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class AddFixedHeadersInterceptor implements Interceptor {
private String[] headerAndValues;
public AddFixedHeadersInterceptor(String... headerAndValues) {
if (headerAndValues.length % 2 != 0) {
throw new IllegalArgumentException("Header must have value");
}
this.headerAndValues = headerAndValues;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request.Builder requestBuilder = chain.request().newBuilder();
for (int i = 0; i < headerAndValues.length; i += 2) {
requestBuilder.addHeader(headerAndValues[i], headerAndValues[i + 1]);
}
return chain.proceed(requestBuilder.build());
}
}

View File

@ -1,29 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class AddFixedParamsInterceptor implements Interceptor {
private String[] paramAndValues;
public AddFixedParamsInterceptor(String... paramAndValues) {
if (paramAndValues.length % 2 != 0) {
throw new IllegalArgumentException("Parameter must have value");
}
this.paramAndValues = paramAndValues;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
for (int i = 0; i < paramAndValues.length; i += 2) {
httpUrlBuilder.addQueryParameter(paramAndValues[i], paramAndValues[i + 1]);
}
return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build());
}
}

View File

@ -1,66 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.hiczp.bilibili.api.BilibiliAPI;
import com.hiczp.bilibili.api.ServerErrorCode;
import okhttp3.Interceptor;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.stream.IntStream;
/**
* 自动刷新 token
* 如果一次请求的返回值表示鉴权失败, 会尝试刷新一次 token 然后自动重放请求
* 刷新 token 的行为将只发生一次, 如果刷新 token 失败, 下次请求的时候不会再次执行刷新 token 操作而会直接返回原本的返回内容
*/
public class AutoRefreshTokenInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(AutoRefreshTokenInterceptor.class);
private BilibiliAPI bilibiliAPI;
private int[] codes;
public AutoRefreshTokenInterceptor(BilibiliAPI bilibiliAPI, int... codes) {
this.bilibiliAPI = bilibiliAPI;
this.codes = codes;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
if (!bilibiliAPI.isAutoRefreshToken()) {
return response;
}
JsonObject jsonObject = InterceptorHelper.getJsonInBody(response);
JsonElement codeElement = jsonObject.get("code");
if (codeElement == null) {
return response;
}
int codeValue = codeElement.getAsInt();
if (codeValue == ServerErrorCode.Common.OK) {
return response;
}
if (IntStream.of(codes).noneMatch(code -> code == codeValue)) {
return response;
}
if (bilibiliAPI.isCurrentTokenAndRefreshTokenInvalid()) {
return response;
}
try {
bilibiliAPI.refreshToken();
response = chain.proceed(chain.request());
} catch (Exception e) {
LOGGER.error("refresh token failed: {}", e.getMessage());
}
return response;
}
}

View File

@ -1,28 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.hiczp.bilibili.api.exception.UserCancelRequestException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
/**
* 这个拦截器用于取消请求
* 如果需要让数据经过其他拦截器处理, 但是不想发生真实的网络请求, 就可以使用这个
*
* @see UserCancelRequestException
*/
public class CancelRequestInterceptor implements Interceptor {
private Request request;
@Override
public Response intercept(Chain chain) throws IOException {
request = chain.request();
throw new UserCancelRequestException();
}
public Request getRequest() {
return request;
}
}

View File

@ -1,60 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.google.gson.*;
import com.hiczp.bilibili.api.ServerErrorCode;
import okhttp3.Interceptor;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* 错误返回码内容转换拦截器
* 由于服务器返回错误时的 data 字段类型不固定, 会导致 json 反序列化出错.
* 该拦截器将在返回的 code 不为 0 , response 转换为包含一个空 data json 字符串.
*/
public class ErrorResponseConverterInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseConverterInterceptor.class);
private static final Gson GSON = new Gson();
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
ResponseBody responseBody = response.body();
JsonObject jsonObject = InterceptorHelper.getJsonInBody(response);
JsonElement code = jsonObject.get("code");
//code 字段不存在
if (code == null || code.isJsonNull()) {
return response;
}
//code 0
try {
if (code.getAsInt() == ServerErrorCode.Common.OK) {
return response;
}
} catch (NumberFormatException e) { //如果 code 不是数字的话直接返回
return response;
}
//打印 body
LOGGER.error("Get error response below: \n{}",
new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(jsonObject)
);
//data 字段不存在
if (jsonObject.get("data") == null) {
return response;
}
jsonObject.add("data", JsonNull.INSTANCE);
return response.newBuilder()
.body(ResponseBody.create(
responseBody.contentType(),
GSON.toJson(jsonObject))
).build();
}
}

View File

@ -1,26 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
class InterceptorHelper {
private static final JsonParser JSON_PARSER = new JsonParser();
static JsonObject getJsonInBody(Response response) throws IOException {
ResponseBody responseBody = response.body();
BufferedSource bufferedSource = responseBody.source();
bufferedSource.request(Long.MAX_VALUE);
Buffer buffer = bufferedSource.buffer();
return JSON_PARSER.parse(
//必须要 clone 一次, 否则将导致流关闭
buffer.clone().readString(StandardCharsets.UTF_8)
).getAsJsonObject();
}
}

View File

@ -1,51 +0,0 @@
package com.hiczp.bilibili.api.interceptor;
import com.hiczp.bilibili.api.BilibiliClientProperties;
import com.hiczp.bilibili.api.BilibiliSecurityHelper;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SortParamsAndSignInterceptor implements Interceptor {
private BilibiliClientProperties bilibiliClientProperties;
public SortParamsAndSignInterceptor(BilibiliClientProperties bilibiliClientProperties) {
this.bilibiliClientProperties = bilibiliClientProperties;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl httpUrl = request.url();
List<String> nameAndValues = new ArrayList<>(httpUrl.querySize() + 1);
httpUrl.queryParameterNames().stream()
.filter(parameterName -> !parameterName.equals("sign"))
.forEach(name ->
httpUrl.queryParameterValues(name).forEach(value -> {
try {
nameAndValues.add(String.format("%s=%s", name, URLEncoder.encode(value, StandardCharsets.UTF_8.toString())));
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
)
);
Collections.sort(nameAndValues);
return chain.proceed(
request.newBuilder()
.url(httpUrl.newBuilder()
.encodedQuery(BilibiliSecurityHelper.addSignToQuery(nameAndValues, bilibiliClientProperties.getAppSecret()))
.build()
).build()
);
}
}

View File

@ -1,635 +0,0 @@
package com.hiczp.bilibili.api.live;
import com.hiczp.bilibili.api.BilibiliClientProperties;
import com.hiczp.bilibili.api.live.entity.*;
import retrofit2.Call;
import retrofit2.http.*;
/**
* 常见参数含义
* mid: 用户 id, 也可能是指主播的用户 id
* cid: 房间 id, 可以指 room_id 也可以指 show_room_id, 推荐所有 API 都使用 room_id 进行访问
*/
public interface LiveService {
/**
* 获取弹幕设置
*
* @param type 必须是 "all", 否则返回的所有字段的值都是 0
*/
@GET("AppRoom/danmuConfig")
Call<BulletScreenConfigEntity> getBulletScreenConfig(@Query("type") String type);
/**
* 获取弹幕设置的快捷调用
*/
default Call<BulletScreenConfigEntity> getBulletScreenConfig() {
return getBulletScreenConfig("all");
}
/**
* 获得房间的历史弹幕(十条)
*
* @param roomId 房间号
*/
@GET("AppRoom/msg")
Call<LiveHistoryBulletScreensEntity> getHistoryBulletScreens(@Query("room_id") long roomId);
/**
* 获取直播间信息
* 登录后访问该 API 将在服务器新增一条直播间观看历史
*
* 2018-05-11 现在用假的房间 ID 也能获得正确的信息
*
* @param roomId 房间号
*/
@GET("AppRoom/index")
Call<LiveRoomInfoEntity> getRoomInfo(@Query("room_id") long roomId);
/**
* 获得是否关注了一个主播
*
* @param hostUserId 主播的用户 ID
* @return 未登录时返回 401
*/
@POST("feed/v1/feed/isFollowed")
Call<IsFollowedResponseEntity> isFollowed(@Query("follow") long hostUserId);
//TODO sendDaily
// API 意义不明
@GET("AppBag/sendDaily")
Call<SendDailyResponseEntity> sendDaily();
/**
* 获得所有礼物的列表
*/
@GET("AppIndex/getAllItem")
Call<ItemsEntity> getAllItem();
/**
* 查看可用的小电视抽奖
*
* @param roomId 房间号
* @return 当目标房间没有可用的小电视抽奖时返回 -400
*/
@GET("AppSmallTV/index")
Call<AppSmallTVEntity> getAppSmallTV(@Query("roomid") long roomId);
/**
* 参与小电视抽奖
* 房间号必须与小电视号对应
* SYS_MSG 里面取得的小电视编号是一个字符串, 实际上它肯定是一个数字
*
* @param roomId 房间号
* @param tvId 小电视号
* @return 目标小电视不存在时(房间号与小电视号不匹配时也视为不存在)返回 -400 "不存在小电视信息"
*/
@POST("AppSmallTV/join")
Call<JoinAppSmallTVResponseEntity> joinAppSmallTV(@Query("roomid") long roomId, @Query("id") String tvId);
/**
* 通过 getAppSmallTV 取得的小电视编号是一个数字
*
* @param roomId 房间号
* @param tvId 小电视号
*/
default Call<JoinAppSmallTVResponseEntity> joinAppSmallTV(long roomId, long tvId) {
return joinAppSmallTV(roomId, String.valueOf(tvId));
}
/**
* 获得小电视抽奖结果(不访问这个 API, 奖励也会自动进入背包)
*
* @param tvId 小电视号
* @return 返回内容中的 status 0 , 表示返回正常开奖结果, 1 为没有参与抽奖或小电视已过期, 2 为正在开奖过程中
*/
@GET("AppSmallTV/getReward")
Call<GetAppSmallTVRewardResponseEntity> getAppSmallTVReward(@Query("id") long tvId);
/**
* 获得所有头衔的列表
* 这里的 Title 是头衔的意思
*/
@GET("appUser/getTitle")
Call<TitlesEntity> getTitle();
//TODO 查看房间里是否有节奏风暴
@GET("SpecialGift/room/{roomId}")
Call<SpecialGiftEntity> getSpecialGift(@Path("roomId") long roomId);
//TODO 参与节奏风暴抽奖
//TODO 查看节奏风暴奖励
/**
* 获取自己的用户信息(live 站的个人信息, 非总站)
*
* @return 未登录时返回 3
*/
@GET("mobile/getUser")
Call<UserInfoEntity> getUserInfo();
/**
* 获取一个直播间的流地址(flv)
*
* @param cid 必须用实际的 room_id, 不能使用 show_room_id, 否则得不到 playUrl. 实际 room_id 要首先通过 getRoomInfo() 获取
* @param outputType 为固定值 "json", 否则返回一个空的 JsonArray (以前是返回一个 XML)
*/
@GET("api/playurl")
Call<PlayUrlEntity> getPlayUrl(@Query("cid") long cid, @Query("otype") String outputType);
/**
* 获取直播间推流地址的快捷调用
*
* @param cid 房间号
*/
default Call<PlayUrlEntity> getPlayUrl(long cid) {
return getPlayUrl(cid, "json");
}
/**
* 获取当前这段时间的活动(不定期活动, 每次持续几周)和信仰任务
*
* @param roomId 房间号
*/
@GET("activity/v1/Common/mobileActivity")
Call<MobileActivityEntity> getMobileActivity(@Query("roomid") long roomId);
/**
* 获取用户的信仰任务列表
*
* @return 2018-02 现在只有 double_watch_task 这个任务是有效的
*/
@GET("activity/v1/task/user_tasks")
Call<UserTasksEntity> getUserTasks();
/**
* 领取一个信仰任务
*
* @param taskId 任务名
* @return 任务未完成或者已领取返回 -400
*/
@POST("activity/v1/task/receive_award")
Call<ReceiveUserTaskAward> receiveUserTaskAward(@Query("task_id") String taskId);
/**
* 领取 double_watch_task 任务的奖励
*/
default Call<ReceiveUserTaskAward> receiveDoubleWatchTaskAward() {
return receiveUserTaskAward("double_watch_task");
}
//TODO 查看一个房间是否有活动抽奖
//TODO 参与活动抽奖
//TODO 查看活动抽奖奖励
/**
* 发送一个 Restful 心跳包, 五分钟一次. 这被用于统计观看直播的时间, 可以提升观众等级
* 2018-03-06 开始, 只有老爷才能通过观看直播获得经验
*
* @param roomId 房间号
* @param scale 屏幕大小
* @return 未登录时返回 3
*/
@POST("mobile/userOnlineHeart")
@FormUrlEncoded
Call<SendOnlineHeartResponseEntity> sendOnlineHeart(@Field("room_id") long roomId, @Field("scale") String scale);
/**
* 发送心跳包的快捷调用
*
* @param roomId 房间号
*/
default Call<SendOnlineHeartResponseEntity> sendOnlineHeart(long roomId) {
return sendOnlineHeart(roomId, BilibiliClientProperties.defaultSetting().getScale());
}
/**
* 发送一条弹幕
*
* @param roomId 房间号
* @param userId 自己的用户 ID
* @param message 内容
* @param random 随机数
* @param mode 弹幕模式
* @param pool 弹幕池
* @param type 必须为 "json"
* @param color 颜色
* @param fontSize 字体大小
* @param playTime 播放时间
* @see BulletScreenEntity
*/
@POST("api/sendmsg")
@FormUrlEncoded
Call<SendBulletScreenResponseEntity> sendBulletScreen(@Field("cid") long roomId,
@Field("mid") long userId,
@Field("msg") String message,
@Field("rnd") long random,
@Field("mode") int mode,
@Field("pool") int pool,
@Field("type") String type,
@Field("color") int color,
@Field("fontsize") int fontSize,
@Field("playTime") String playTime);
/**
* 发送弹幕的快捷调用
*
* @param bulletScreenEntity 弹幕实体类
*/
default Call<SendBulletScreenResponseEntity> sendBulletScreen(BulletScreenEntity bulletScreenEntity) {
return sendBulletScreen(
bulletScreenEntity.getRoomId(),
bulletScreenEntity.getUserId(),
bulletScreenEntity.getMessage(),
bulletScreenEntity.getRandom(),
bulletScreenEntity.getMode(),
bulletScreenEntity.getPool(),
bulletScreenEntity.getType(),
bulletScreenEntity.getColor(),
bulletScreenEntity.getFontSize(),
bulletScreenEntity.getPlayTime()
);
}
/**
* 获取下一个宝箱任务的信息
*/
@GET("mobile/freeSilverCurrentTask")
Call<FreeSilverCurrentTaskEntity> getFreeSilverCurrentTask();
/**
* 领取宝箱
*/
@GET("mobile/freeSilverAward")
Call<FreeSilverAwardEntity> getFreeSilverAward();
/**
* 查看自己的背包(礼物)
*/
@GET("AppBag/playerBag")
Call<PlayerBagEntity> getPlayerBag();
/**
* 查看哪些礼物是活动礼物, 在客户端上, 活动礼物会有一个右上角标记 "活动"
*
* @param roomId 房间号
*/
@GET("AppRoom/activityGift")
Call<ActivityGiftsEntity> getActivityGifts(@Query("room_id") long roomId);
/**
* 送礼物
*
* @param giftId 礼物 ID
* @param number 数量
* @param roomUserId 主播的用户 ID
* @param roomId 房间号
* @param timeStamp 时间戳
* @param bagId 礼物在自己背包里的 ID
* @param random 随机数
* @return roomUserId roomId 不匹配时返回 200012
* bagId 错误时(背包里没有这个礼物)返回 200019
*/
@POST("AppBag/send")
@FormUrlEncoded
Call<SendGiftResponseEntity> sendGift(@Field("giftId") long giftId,
@Field("num") long number,
@Field("ruid") long roomUserId,
@Field("roomid") long roomId,
@Field("timestamp") long timeStamp,
@Field("bag_id") long bagId,
@Field("rnd") long random);
/**
* 送礼物的快捷调用
*
* @param giftEntity 礼物实体类
*/
default Call<SendGiftResponseEntity> sendGift(GiftEntity giftEntity) {
return sendGift(
giftEntity.getGiftId(),
giftEntity.getNumber(),
giftEntity.getRoomUserId(),
giftEntity.getRoomId(),
giftEntity.getTimeStamp(),
giftEntity.getBagId(),
giftEntity.getRandom()
);
}
/**
* 获得礼物榜(七日榜)
*
* @param roomId 房间号
*/
@GET("AppRoom/getGiftTop")
Call<GiftTopEntity> getGiftTop(@Query("room_id") int roomId);
/**
* "直播" 页面(这个页面对应的后台数据, 包括 banner, 推荐主播, 各种分区的推荐等)
*
* @param device 这个 API 会读取 "_device"(固定参数) 或者 "device" 来判断平台, 只需要有一个就能正常工作, 客户端上是两个都有, 且值都为 "android"
*/
@GET("room/v1/AppIndex/getAllList")
Call<AllListEntity> getAllList(@Query("device") String device);
/**
* 获取 "直播" 页面数据的快捷调用
*/
default Call<AllListEntity> getAllList() {
return getAllList("android");
}
/**
* 刷新 "推荐主播" 区域, 必须有 device, platform, scala
* scala xxhdpi 时返回 12 , 客户端显示六个, 刷新两次后再次访问该 API
* API 返回的内容结构与 getAllList 返回的内容中的 recommend_data 字段是一样的
* API 返回的 banner_data 是在普通分区的推荐的上面的那个 banner, 在新版 APP , 点击这个 banner 会固定的跳转到 bilibili 相簿的 画友 标签页
*
* @param device 设备类型
*/
@GET("room/v1/AppIndex/recRefresh")
Call<RecommendRoomRefreshResponseEntity> recommendRefresh(@Query("device") String device);
/**
* 刷新 "推荐主播" 区域 的快捷调用
*/
default Call<RecommendRoomRefreshResponseEntity> recommendRefresh() {
return recommendRefresh("android");
}
/**
* 获取对应分类和状态的直播间
*
* @param areaId 分区 ID
* @param categoryId 不明确其含义
* @param parentAreaId 父分区 ID
* @param sortType 排序方式
* @param page 页码, 可以为 null(第一页)
*/
@GET("room/v1/Area/getRoomList")
Call<RoomListEntity> getRoomList(
@Query("area_id") int areaId,
@Query("cate_id") int categoryId,
@Query("parent_area_id") int parentAreaId,
@Query("sort_type") String sortType,
@Query("page") Long page
);
/**
* 直播页面 下面的 普通分区(复数) 的刷新, 一次会返回 20 个结果, 客户端显示 6 , 数据用完了之后再次访问该 API
*
* @param parentAreaId 父分区 ID
*/
default Call<RoomListEntity> getRoomList(int parentAreaId) {
return getRoomList(0, 0, parentAreaId, "dynamic", null);
}
/**
* 直播 -> 某个分区 -> 查看更多
* 获取该页面上方的分类标签
*
* @param parentAreaId 父分区 ID
*/
@GET("room/v1/Area/getList")
Call<AreaListEntity> getAreaList(@Query("parent_id") int parentAreaId);
/**
* 获取该页面下的的直播间(areaId 0 表示选择了 "全部"(上方的分类标签), areaId 如果和 parentAreaId 不匹配将返回空的 data 字段)
*
* @param areaId 分区 ID
* @param parentAreaId 父分区 ID
* @param page 页码
*/
default Call<RoomListEntity> getRoomList(int areaId, int parentAreaId, long page) {
return getRoomList(areaId, 0, parentAreaId, "online", page);
}
/**
* 直播 -> 全部直播(直播页面的最下面的一个按钮)
*
* @param areaId 分区 ID
* @param page 页码
* @param sort 分类
*/
@GET("mobile/rooms")
Call<RoomsEntity> getRooms(@Query("area_id") int areaId, @Query("page") int page, @Query("sort") String sort);
/**
* 推荐直播
*
* @param page 页码
*/
default Call<RoomsEntity> getSuggestionRooms(int page) {
return getRooms(0, page, "suggestion");
}
/**
* 最热直播
*
* @param page 页码
*/
default Call<RoomsEntity> getHottestRooms(int page) {
return getRooms(0, page, "hottest");
}
/**
* 最新直播
*
* @param page 页码
*/
default Call<RoomsEntity> getLatestRooms(int page) {
return getRooms(0, page, "latest");
}
/**
* 视频轮播
*
* @param page 页码
*/
default Call<RoomsEntity> getRoundRooms(int page) {
return getRooms(0, page, "roundroom");
}
/**
* live 站的搜索("直播" 页面)
*
* @param keyword 关键字
* @param page 页码
* @param pageSize 页容量
* @param type room 时只返回 房间 的搜索结果, user 时只返回 用户 的搜索结果, all 房间 用户 的搜索结果都有
*/
@GET("AppSearch/index")
Call<SearchResponseEntity> search(@Query("keyword") String keyword, @Query("page") long page, @Query("pagesize") long pageSize, @Query("type") String type);
/**
* 搜索的快捷调用
*
* @param keyword 关键字
* @param page 页码
* @param pageSize 页容量
*/
default Call<SearchResponseEntity> search(String keyword, long page, long pageSize) {
return search(keyword, page, pageSize, "all");
}
/**
* 侧拉抽屉 -> 直播中心 -> 右上角日历图标
* 签到(live 站签到, 非总站(虽然我也不知道总站有没有签到功能))
*
* @return 无论是否已经签到, 返回的 code 都是 0. 除了字符串比对, 要想知道是否已经签到要通过 getUserInfo().getIsSign()
*/
@GET("AppUser/getSignInfo")
Call<SignInfoEntity> getSignInfo();
/**
* 侧拉抽屉 -> 直播中心 -> 我的关注
* 获得关注列表
* 未登录时返回 32205
*
* @param page 页码
* @param pageSize 页容量
*/
@GET("AppFeed/index")
Call<FollowedHostsEntity> getFollowedHosts(@Query("page") long page, @Query("pagesize") long pageSize);
/**
* 侧拉抽屉 -> 直播中心 -> 观看历史
*
* @param page 页码
* @param pageSize 页容量
*/
@GET("AppUser/history")
Call<HistoryEntity> getHistory(@Query("page") long page, @Query("pagesize") long pageSize);
/**
* 佩戴中心
* 侧拉抽屉 -> 直播中心 -> 佩戴中心 -> 粉丝勋章
* 获得用户拥有的粉丝勋章
*/
@GET("AppUser/medal")
Call<MyMedalListEntity> getMyMedalList();
/**
* 佩戴粉丝勋章
*
* @param medalId 勋章 ID
*/
@POST("AppUser/wearMedal")
Call<WearMedalResponseEntity> wearMedal(@Query("medal_id") int medalId);
/**
* 取消佩戴粉丝勋章(取消佩戴当前佩戴着的粉丝勋章)
* URL 上的 canel 不是拼写错误, 它原本就是这样的
*/
@GET("AppUser/canelMedal")
Call<CancelMedalResponseEntity> cancelMedal();
/**
* 侧拉抽屉 -> 直播中心 -> 佩戴中心 -> 我的头衔
* 获得用户拥有的头衔
*/
@GET("appUser/myTitleList")
Call<MyTitleListEntity> getMyTitleList();
/**
* 获得当前佩戴着的头衔的详情
*
* @return 当前未佩戴任何东西时, 返回的 code -1, message "nodata"
*/
@GET("appUser/getWearTitle")
Call<WearTitleEntity> getWearTitle();
/**
* 佩戴头衔
*
* @param title 头衔名
*/
@POST("AppUser/wearTitle")
Call<WearTitleResponseEntity> wearTitle(@Query("title") String title);
/**
* 取消佩戴头衔(取消佩戴当前佩戴着的头衔)
*/
@GET("appUser/cancelTitle")
Call<CancelTitleResponseEntity> cancelTitle();
//TODO 头衔工坊(没有可升级头衔, 暂不明确此 API)
/**
* 侧拉抽屉 -> 直播中心 -> 获奖记录
* 获得用户的获奖记录
*/
@GET("AppUser/awards")
Call<AwardsEntity> getAwardRecords();
/**
* 瓜子商店
* 侧拉抽屉 -> 直播中心 -> 瓜子商店 -> 银瓜子兑换 -> 硬币银瓜子互换 -> 兑换硬币
* 700 银瓜子兑换为 1 硬币, 每个用户每天只能换一次
*
* @return 已经兑换过时返回 403
* 2018-03-15 访问此 API 必须有一个合法的 UA, 否则返回 65530
*/
@POST("AppExchange/silver2coin")
Call<Silver2CoinResponseEntity> silver2Coin();
/**
* 扭蛋机
* 侧拉抽屉 -> 直播中心 -> 扭蛋机 -> 普通扭蛋
* 获得 扭蛋机(普通扭蛋) 这个页面对应的后台数据
*/
@GET("AppUser/capsuleInfo")
Call<CapsuleInfoEntity> getCapsuleInfo();
/**
* 抽扭蛋
*
* @param count 数量, 只能为 1, 10, 100
* @param type 扭蛋类型, 只能为 "normal" "colorful"
*/
@POST("AppUser/capsuleInfoOpen")
@FormUrlEncoded
Call<OpenCapsuleResponseEntity> openCapsule(@Field("count") int count, @Field("type") String type);
/**
* 抽普通扭蛋
* 侧拉抽屉 -> 直播中心 -> 扭蛋机 -> 普通扭蛋 ->
* 普通扭蛋的 type "normal"
*
* @param count 数量, 只能为 1, 10, 100
*/
default Call<OpenCapsuleResponseEntity> openNormalCapsule(int count) {
return openCapsule(count, "normal");
}
/**
* 抽梦幻扭蛋
*
* @param count 数量, 只能为 1, 10, 100
*/
default Call<OpenCapsuleResponseEntity> openColorfulCapsule(int count) {
return openCapsule(count, "colorful");
}
/**
* 房间设置
* 侧拉抽屉 -> 直播中心 -> 房间设置 -> (上面的个人信息, 包括 房间号, 粉丝数, UP 经验)
* 根据用户 ID 来获取房间信息, 通常用于获取自己的直播间信息(可以用来获取他人的房间信息)
* API 不会增加直播间观看历史
*
* @param userId 用户 ID
*/
@GET("assistant/getRoomInfo")
Call<AssistantRoomInfoEntity> getAssistantRoomInfo(@Query("uId") long userId);
/**
* 侧拉抽屉 -> 直播中心 -> 房间设置 -> 我的封面
* 获取自己的直播间的封面
*
* @param roomId 房间号
* @return 获取其他人的封面会 -403
*/
@GET("mhand/assistant/getCover")
Call<CoverEntity> getCover(@Query("roomId") long roomId);
//TODO 粉丝勋章(尚未达到开通粉丝勋章的最低要求, 无法对该 API 截包)
}

View File

@ -1,5 +0,0 @@
package com.hiczp.bilibili.api.live.bulletScreen;
public class BulletScreenConstDefinition {
public static final int DEFAULT_MESSAGE_LENGTH_LIMIT = 20;
}

View File

@ -1,17 +0,0 @@
package com.hiczp.bilibili.api.live.bulletScreen;
import javax.annotation.Nonnull;
public class BulletScreenHelper {
public static String[] splitMessageByFixedLength(@Nonnull String message, int lengthLimit) {
int count = message.length() / lengthLimit;
if (message.length() % lengthLimit != 0) {
count++;
}
String[] messages = new String[count];
for (int i = 0; i < count; i++) {
messages[i] = message.substring(i * lengthLimit, i != count - 1 ? (i + 1) * lengthLimit : message.length());
}
return messages;
}
}

View File

@ -1,10 +0,0 @@
package com.hiczp.bilibili.api.live.bulletScreen;
import com.hiczp.bilibili.api.live.entity.BulletScreenEntity;
import com.hiczp.bilibili.api.live.entity.SendBulletScreenResponseEntity;
public interface BulletScreenSendingCallback {
void onResponse(BulletScreenEntity bulletScreenEntity, SendBulletScreenResponseEntity sendBulletScreenResponseEntity);
void onFailure(BulletScreenEntity bulletScreenEntity, Throwable throwable);
}

View File

@ -1,28 +0,0 @@
package com.hiczp.bilibili.api.live.bulletScreen;
import com.hiczp.bilibili.api.live.entity.BulletScreenEntity;
import com.hiczp.bilibili.api.provider.BilibiliServiceProvider;
public class BulletScreenSendingTask {
private BilibiliServiceProvider bilibiliServiceProvider;
private BulletScreenEntity bulletScreenEntity;
private BulletScreenSendingCallback bulletScreenSendingCallback;
public BulletScreenSendingTask(BilibiliServiceProvider bilibiliServiceProvider, BulletScreenEntity bulletScreenEntity, BulletScreenSendingCallback bulletScreenSendingCallback) {
this.bilibiliServiceProvider = bilibiliServiceProvider;
this.bulletScreenEntity = bulletScreenEntity;
this.bulletScreenSendingCallback = bulletScreenSendingCallback;
}
public BilibiliServiceProvider getBilibiliServiceProvider() {
return bilibiliServiceProvider;
}
public BulletScreenEntity getBulletScreenEntity() {
return bulletScreenEntity;
}
public BulletScreenSendingCallback getBulletScreenSendingCallback() {
return bulletScreenSendingCallback;
}
}

View File

@ -1,151 +0,0 @@
package com.hiczp.bilibili.api.live.entity;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
public class ActivityGiftsEntity extends ResponseEntity {
/**
* code : 0
* message : OK
* data : [{"id":102,"bag_id":48843715,"name":"秘银水壶","num":9,"img":"http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift-static-icon/gift-102.png?20171010161652","gift_url":"http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift/3/102.gif?20171010161652","combo_num":5,"super_num":225,"count_set":"1,5,9","count_map":{"1":"","5":"连击","9":"全部"}}]
*/
@SerializedName("code")
private int code;
@SerializedName("data")
private List<Data> data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public List<Data> getData() {
return data;
}
public void setData(List<Data> data) {
this.data = data;
}
public static class Data {
/**
* id : 102
* bag_id : 48843715
* name : 秘银水壶
* num : 9
* img : http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift-static-icon/gift-102.png?20171010161652
* gift_url : http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift/3/102.gif?20171010161652
* combo_num : 5
* super_num : 225
* count_set : 1,5,9
* count_map : {"1":"","5":"连击","9":"全部"}
*/
@SerializedName("id")
private int id;
@SerializedName("bag_id")
private int bagId;
@SerializedName("name")
private String name;
@SerializedName("num")
private int number;
@SerializedName("img")
private String img;
@SerializedName("gift_url")
private String giftUrl;
@SerializedName("combo_num")
private int comboNum;
@SerializedName("super_num")
private int superNum;
@SerializedName("count_set")
private String countSet;
@SerializedName("count_map")
private Map<String, String> countMap;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getBagId() {
return bagId;
}
public void setBagId(int bagId) {
this.bagId = bagId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getImg() {
return img;
}
public void setImg(String img) {
this.img = img;
}
public String getGiftUrl() {
return giftUrl;
}
public void setGiftUrl(String giftUrl) {
this.giftUrl = giftUrl;
}
public int getComboNum() {
return comboNum;
}
public void setComboNum(int comboNum) {
this.comboNum = comboNum;
}
public int getSuperNum() {
return superNum;
}
public void setSuperNum(int superNum) {
this.superNum = superNum;
}
public String getCountSet() {
return countSet;
}
public void setCountSet(String countSet) {
this.countSet = countSet;
}
public Map<String, String> getCountMap() {
return countMap;
}
public void setCountMap(Map<String, String> countMap) {
this.countMap = countMap;
}
}
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More