This commit is contained in:
czp3009
2019-06-26 15:54:29 +08:00
parent f156e494fd
commit c62fa9b870
127 changed files with 553 additions and 11206 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,5 @@
target
out
build
src/test/resources/config.json
.gradle
gradlew.bat

510
README.md
View File

@@ -1,490 +1,70 @@
# Bilibili API JVM 调用库
项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
# Bilibili API library for Kotlin
项目封装一些 Bilibili API 以方便在 Kotlin 中使用(也可用于其他 JVM 语言).
协议来自对 Bilibili Android APP 的逆向工程以及截包分析.
使用一台虚拟的 `Pixel 2` 设备来截取数据包, 一些固定参数可能与真实设备不一致.
# 使用
# 引入依赖
RestFul API
```groovy
compile group: 'com.hiczp', name: 'bilibili-api', version: '0.1.0'
compile group: 'com.hiczp', name: 'bilibili-api-rest', version: '0.2.0'
```
# 技术说明
`BilibiliClient` 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.
Websocket(用于连接直播间弹幕推送服务器以获取实时弹幕)
```groovy
compile group: 'com.hiczp', name: 'bilibili-api-websocket', version: '0.2.0'
```
所有调用从这个类开始, 包括登陆以及访问其他各种 API.
使用协程来实现异步, 由于 [kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 为编译器实现, 因此并非所有 JVM 语言都能正确调用 `suspend` 方法.
本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.
`BilibiliClient` 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 `BilibiliClient` 实例, 而应该保存其引用.
一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:
全部引入
```groovy
compile group: 'com.hiczp', name: 'bilibili-api', version: '0.2.0'
```
# 获取直播间实时弹幕
举个例子
```kotlin
runBlocking {
val bilibiliClient = BilibiliClient().apply {
login(username, password)
}
val myInfo = bilibiliClient.appAPI.myInfo().await()
val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
}
```
如果一个请求的返回内容中的 `code`(code 是 BODY 的内容, 并非 HttpStatus) 不为 0, 将抛出异常 `BilibiliApiException`, 通过以下代码来获取服务器原始返回的 `code`:
```kotlin
val code = bilibiliApiException.commonResponse.code
```
一个错误返回的原始 `JSON` 如下所示:
```json
{
"code": -629,
"message": "用户名与密码不匹配",
"ts": 1550730464
}
```
每种不同的 API 在错误时返回的 `code` 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.
# 登录和登出
(Bilibili oauth2 v3)
登陆和登出均为异步方法, 需要在协程上下文中执行(接下去不会特地强调这一点).
```kotlin
runBlocking {
BilibiliClient().run {
login(username, password)
logout()
}
}
```
`login` 方法返回一个 `LoginResponse` 实例, 下次可以直接赋值到没有登陆的 `BilibiliClient` 实例中来恢复登陆状态.
```kotlin
BilibiliClient().apply {
this.loginResponse = loginResponse
}
```
`LoginResponse` 继承 `Serializable`, 可被序列化(JVM 序列化).
可能的错误返回有两种:
-629 用户名与密码不匹配
-105 验证码错误
如果仅使用用户名与密码进行登陆并且得到了 `-105` 的结果, 那么说明需要验证码(通常是由于多次错误的登陆尝试导致的).
原始返回如下所示
{"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}
自行访问 `commonResponse.data.obj.url.string` 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:
```kotlin
login(username, password, challenge, secCode, validate)
```
`challenge` 为本次极验的唯一标识(在一开始给出的 url 中)
`validate` 为极验返回值
`secCode``"$validate|jordan"`
(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误(想要这么做的人一定脑子瓦特了).
登陆后, 可以访问全部 API(注意, 有一些明显不需要登录的 API 也有可能需要登录).
由于各种需要登陆的 API 在未登录时返回的 `code` 并不统一, 因此没有办法做自动 `token` 刷新, 自己看着办.
在真实的客户端上, 每次一打开 APP 就会访问[个人信息 API](#获取个人信息)来确定 `token` 是否仍然可用, 这就是 B站 自己的解决方案.
# 访问 API
不要问文档, 用自动补全(心)来感受. 以下给出几个示例
## 获取个人信息
(首先要登陆)
```kotlin
val myInfo = bilibiliClient.appAPI.myInfo().await()
```
返回用户 ID, vip 信息等.
## 搜索
当我们想看某些内容时, 我们会首先使用搜索功能, 例如
```kotlin
val searchResult = bilibiliClient.appAPI.search(keyword = "刀剑神域").await()
```
实际上这对应客户端上的 搜索 -> 综合.
如果要搜索番剧则使用 `bilibiliClient.appAPI.searchBangumi`.
同理, 搜索直播, 用户, 影视, 专栏分别使用 `searchLive`, `searchUser`, `searchMovie`, `searchArticle`.
所有的搜索都使用 `pageNumber` 参数来控制翻页(从 1 开始).
## 获取视频播放地址
获取视频实际播放地址的 API 比较特殊, 被单独分了出来, 示例如下
```kotlin
val videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 72913641).await()
```
`aid` 即 av 号, 只能表示视频播放的那个页面, 如果一个视频有多个 `p`, 那么每个 `p` 都有单独的 `cid`.
在 Web 端, URL 通常是这样的
https://www.bilibili.com/video/av44541340/?p=2
实际上就是选择了该 `aid` 下的第二个 `cid`(注意, 参数里使用的 `cid` 不是这个 p 的序号, 它也是一个很长的数字).
简单的来说, `aid``cid` 加在一起才能表示一个视频流(为什么 `cid` 不能直接表示一个视频我也不知道).
因此无论是获取视频播放地址, 还是获取弹幕列表, 都要同时传入 `aid``cid`.
`cid` 在哪里获得呢, 如下所示
```kotlin
val view = bilibiliClient.appAPI.view(aid = 41517911).await()
```
该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.
其中 `data.cid` 为默认 `p``cid`. `data.pages[n].cid` 为每个 `p``cid`. 如果只有一个 `p` 那么说明视频没有分 `p`.
请求视频地址将访问如下结构的内容
```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
}
```
(由于内容太长, 去除了一部分内容)
注意, 视频下载地址有好几个(以上返回内容中被折叠成了两个), 但是实际上他们都是一样的内容, 只是清晰度不同. `data.dash.video.id` 实际上代表 `data.accept_quality`.
视频和音频是分开的, 视频和音频都返回 `m4s` 文件, 将其合并即可得到完整的 `mp4` 文件.
`data.quality` 指默认选择的清晰度, 通常情况下移动网络会自动选择 `32`, 即 "清晰 480P"(在 `data.accept_description` 中对应).
对于番剧来说, 也使用 `aid``cid` 来获得播放地址
```kotlin
val bangumiPlayUrl = bilibiliClient.playerAPI.bangumiPlayUrl(aid = 42714241, cid = 74921228).await()
```
返回内容差不多是一个原理, 这里就不赘述了.
如何获得番剧的 `aid``cid` 呢. 我们都知道, 实际上番剧那个页面的唯一标识是 "季", 同一个番的不同 "季" 其实是不同的东西.
我们在番剧搜索页面可以得到番剧的 `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"
val liveClient = LiveClient(roomId = 23058) {
resolvedPackets.consumeEach {
when (it) {
is CommandPacket -> {
val command = it.content
println("[${command.cmd}] $command")
}
is PopularityPacket -> {
println("Popularity: ${it.content}")
}
} else {
jsonObject.toString()
}
)
}
}
runBlocking {
liveClient.connect()
}
```
输出:
`resolvedPackets` 是一个输送解析后的数据包的 `Channel`, 解析后的数据包有 `CommandPacket` 以及 `PopularityPacket` 两种类型.
```
[甜甜天 7] [UL25] czp3009: 233
```
`CommandPacket` 的本体是一段 JSON, 由于其内容经常发生改变, 所以不提供 POJO.
更多 `Command` 数据包的数据结构详见本项目的 [/record/直播弹幕](record/直播弹幕) 文件夹.
`Command.cmd` 是一个快捷方式, 可以快速取得其中的 `cmd` 字段从而判断其类别.
注意, `onPopularityPacket`, `onCommandPacket` 这些回调不能进行耗时操作.
作为样本的 JSON 数据在本项目 `/record` 文件夹下.
关闭连接
大部分 JSON 都有光怪陆离的数据结构. 其中 `DANMU_MSG` 尤为恶劣, 通篇都是数组. 为了方便对其解析, 故提供内联类 `DanmakuMessage`.
使用方法如下
```kotlin
job.cancel()
val command = commandPacket.content
val danmakuMessage = command.asDanmakuMessage()
with(danmakuMessage) {
println("$nickname: $message")
}
```
## 发送直播弹幕
在直播间里发送弹幕也非常简单(必须先登陆)
`PopularityPacket` 的本体是一个 `Int` 数字, 表示当前房间的人气值. 该数据包每 30秒 收到一次.
```kotlin
liveClient.sendMessage("我上我也行").await()
```
如果要为读取操作设定超时, 可以设定为 40秒.
注意, 除了弹幕超长(普通用户20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回(`code` 为 0).
完全正常返回时(弹幕正确的被发送了), 返回内容中的 `message` 为一个空字符串.
如果不为空字符串, 则表示不完全正常
例如返回内容的 `message` 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 确实是 0.
其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.
正常返回时, 就算不完全正常, 客户端也会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).
需要额外判断返回的 `message` 是否为空字符串来确认这条弹幕有没有被正确发送.
注意: 如果使用短房间号来连接弹幕推送服务器, 可能会得不到正确的人气值信息(一直为 0 或者一直为 1). 因此在连接弹幕推送服务器前应当首先获取直播间基本信息. 同时, 弹幕服务器不是唯一的, 在构造 `LiveClient` 时可以传入从房间基本信息中获取到的其他服务器地址.
# License
GPL V3

View File

@@ -1,9 +1,9 @@
buildscript {
ext {
kotlin_version = '1.3.21'
kotlin_coroutines_version = '1.1.1'
ktor_version = '1.1.3'
kotlin_version = '1.3.31'
kotlin_coroutines_version = '1.2.1'
jvm_target = JavaVersion.VERSION_1_8
ktor_version = '1.1.4'
}
repositories {
@@ -15,136 +15,132 @@ buildscript {
}
}
group = 'com.hiczp'
version = '0.1.0'
description = 'Bilibili Android client API library for Kotlin'
allprojects {
group = 'com.hiczp'
version = '0.2.0'
description = 'Bilibili API library for Kotlin'
apply plugin: 'kotlin'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'kotlin'
apply plugin: 'maven-publish'
apply plugin: 'signing'
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.5.0'
// https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson
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.14.0'
}
//ktor
dependencies {
// 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
}
//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) {
from javadoc
archiveClassifier = 'javadoc'
}
publishing {
repositories {
maven {
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username = project.properties.ossUsername
password = project.properties.ossPassword
}
mavenCentral()
}
configurations.all {
resolutionStrategy {
force "io.ktor:ktor-client-cio:$ktor_version"
}
}
}
subprojects {
//kotlin
dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8
api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
api 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
freeCompilerArgs = ["-Xjvm-default=enable", "-Xuse-experimental=kotlin.Experimental", "-XXLanguage:+InlineClasses"]
}
}
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
//logging
dependencies {
// https://mvnrepository.com/artifact/io.github.microutils/kotlin-logging
api group: 'io.github.microutils', name: 'kotlin-logging', version: '1.6.26'
// https://mvnrepository.com/artifact/org.slf4j/slf4j-simple
testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.26'
}
pom {
name = project.name
description = project.description
url = 'https://github.com/czp3009/bilibili-api'
//unit test
dependencies {
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.4.2'
}
licenses {
license {
name = 'GNU GENERAL PUBLIC LICENSE Version 3'
url = 'https://www.gnu.org/licenses/gpl-3.0.txt'
}
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
archiveClassifier = 'sources'
}
task javadocJar(type: Jar) {
from javadoc
archiveClassifier = 'javadoc'
}
publishing {
def moduleName = "${rootProject.name}-${project.name}"
repositories {
maven {
url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username = project.properties.ossUsername
password = project.properties.ossPassword
}
}
}
developers {
developer {
id = 'czp3009'
name = 'czp3009'
email = 'czp3009@gmail.com'
url = 'https://www.hiczp.com'
}
}
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
artifactId moduleName
scm {
connection = 'scm:git:git://github.com/czp3009/bilibili-api.git'
developerConnection = 'scm:git:ssh://github.com/czp3009/bilibili-api.git'
pom {
name = moduleName
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'
}
}
developers {
developer {
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
}
}
signing {
sign publishing.publications.mavenJava
dependencies {
api project(':rest')
api project(':websocket')
}
jar {
enabled = false
}

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-5.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

View File

@@ -2,6 +2,7 @@
"cmd": "ROOM_REAL_TIME_MESSAGE_UPDATE",
"data": {
"roomid": 23058,
"fans": 300958
"fans": 297141,
"red_notice": -1
}
}
}

1
rest/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
src/test/resources/config.json

19
rest/build.gradle Normal file
View File

@@ -0,0 +1,19 @@
//http
dependencies {
// https://mvnrepository.com/artifact/com.hiczp/caeruleum
api group: 'com.hiczp', name: 'caeruleum', version: '1.1.0'
}
//json
dependencies {
// https://mvnrepository.com/artifact/com.google.code.gson/gson
api group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
// https://mvnrepository.com/artifact/com.github.salomonbrys.kotson/kotson
api group: 'com.github.salomonbrys.kotson', name: 'kotson', version: '2.5.0'
}
//checksum
dependencies {
// https://mvnrepository.com/artifact/com.hiczp/crc32-crack
api group: 'com.hiczp', name: 'crc32-crack', version: '1.0'
}

View File

@@ -0,0 +1,2 @@
package com.hiczp.bilibili.rest

View File

@@ -1 +1,4 @@
rootProject.name='bilibili-api'
rootProject.name = 'bilibili-api'
include 'rest'
include 'websocket'

View File

@@ -1,41 +0,0 @@
package com.hiczp.bilibili.api
/**
* 各个站点的域名
*/
object BaseUrl {
/**
* 用户鉴权
*/
const val passport = "https://passport.bilibili.com"
/**
* 消息
*/
const val message = "https://message.bilibili.com"
/**
* 主站
*/
const val app = "https://app.bilibili.com"
/**
* 这也是主站
*/
const val main = "https://api.bilibili.com"
/**
* 小视频
*/
const val vc = "https://api.vc.bilibili.com"
/**
* 创作中心
*/
const val member = "https://member.bilibili.com"
/**
* 直播
*/
const val live = "https://api.live.bilibili.com"
}

View File

@@ -1,325 +0,0 @@
package com.hiczp.bilibili.api
import com.hiczp.bilibili.api.app.AppAPI
import com.hiczp.bilibili.api.danmaku.DanmakuAPI
import com.hiczp.bilibili.api.live.LiveAPI
import com.hiczp.bilibili.api.main.MainAPI
import com.hiczp.bilibili.api.member.MemberAPI
import com.hiczp.bilibili.api.message.MessageAPI
import com.hiczp.bilibili.api.passport.PassportAPI
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.player.PlayerAPI
import com.hiczp.bilibili.api.player.PlayerInterceptor
import com.hiczp.bilibili.api.retrofit.Header
import com.hiczp.bilibili.api.retrofit.Param
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
import com.hiczp.bilibili.api.retrofit.interceptor.CommonHeaderInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.CommonParamInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.FailureResponseInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.SortAndSignInterceptor
import com.hiczp.bilibili.api.vc.VcAPI
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import okhttp3.ConnectionPool
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.*
import javax.crypto.Cipher
/**
* 此类表示一个模拟的 Bilibili 客户端(Android), 所有调用由此开始.
* 多个 BilibiliClient 实例之间不共享登陆状态.
* 不能严格保证线程安全.
*
* @param billingClientProperties 客户端的固有属性, 是一种常量
* @param logLevel 日志打印等级
*/
@Suppress("unused")
class BilibiliClient(
@Suppress("MemberVisibilityCanBePrivate")
val billingClientProperties: BilibiliClientProperties = BilibiliClientProperties(),
private val logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE
) {
/**
* 客户端被打开的时间(BilibiliClient 被实例化的时间)
*/
private val initTime = Instant.now().epochSecond
/**
* 登陆操作得到的 Response
*/
var loginResponse: LoginResponse? = null
/**
* 是否已登录
*/
val isLogin
get() = loginResponse != null
//快捷方式
@Suppress("MemberVisibilityCanBePrivate")
val userId
get() = loginResponse?.userId
@Suppress("MemberVisibilityCanBePrivate")
val token
get() = loginResponse?.token
@Suppress("SpellCheckingInspection")
private val defaultCommonHeaderInterceptor = CommonHeaderInterceptor(
Header.DISPLAY_ID to { "${billingClientProperties.buildVersionId}-$initTime" },
Header.BUILD_VERSION_ID to { billingClientProperties.buildVersionId },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent },
Header.DEVICE_ID to { billingClientProperties.hardwareId }
)
@Suppress("SpellCheckingInspection")
private val defaultCommonParamArray = arrayOf(
Param.ACCESS_KEY to { token },
Param.APP_KEY to { billingClientProperties.appKey },
Param.BUILD to { billingClientProperties.build },
Param.CHANNEL to { billingClientProperties.channel },
Param.MOBILE_APP to { billingClientProperties.platform },
Param.PLATFORM to { billingClientProperties.platform },
Param.TIMESTAMP to { Instant.now().epochSecond.toString() }
)
private val defaultCommonParamInterceptor = CommonParamInterceptor(*defaultCommonParamArray)
/**
* 用户鉴权相关的接口
*/
@Suppress("SpellCheckingInspection")
val passportAPI by lazy {
createAPI<PassportAPI>(BaseUrl.passport,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(
Param.APP_KEY to { billingClientProperties.appKey },
Param.BUILD to { billingClientProperties.build },
Param.CHANNEL to { billingClientProperties.channel },
Param.MOBILE_APP to { billingClientProperties.platform },
Param.PLATFORM to { billingClientProperties.platform },
Param.TIMESTAMP to { Instant.now().epochSecond.toString() }
)
)
}
/**
* 消息通知有关的接口
*/
@Suppress("SpellCheckingInspection")
val messageAPI by lazy {
createAPI<MessageAPI>(BaseUrl.message,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(*defaultCommonParamArray,
Param.ACTION_KEY to { Param.APP_KEY },
"has_up" to { "1" }
)
)
}
/**
* 总站 API
*/
@Suppress("SpellCheckingInspection")
val appAPI by lazy {
createAPI<AppAPI>(BaseUrl.app,
defaultCommonHeaderInterceptor,
defaultCommonParamInterceptor
)
}
/**
* 这也是总站 API
*/
@Suppress("SpellCheckingInspection")
val mainAPI by lazy {
createAPI<MainAPI>(BaseUrl.main,
CommonHeaderInterceptor(
//如果未登陆则没有 Display-ID
Header.DISPLAY_ID to { userId?.let { "$it-$initTime" } },
Header.BUILD_VERSION_ID to { billingClientProperties.buildVersionId },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent },
Header.DEVICE_ID to { billingClientProperties.hardwareId }
),
defaultCommonParamInterceptor
)
}
/**
* 小视频相关接口
*/
@Suppress("SpellCheckingInspection")
val vcAPI by lazy {
createAPI<VcAPI>(BaseUrl.vc,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(*defaultCommonParamArray,
Param._DEVICE to { billingClientProperties.platform },
Param._HARDWARE_ID to { billingClientProperties.hardwareId },
Param.SOURCE to { billingClientProperties.channel },
Param.TRACE_ID to { generateTraceId() },
Param.USER_ID to { userId?.toString() },
Param.VERSION to { billingClientProperties.version }
)
)
}
/**
* 创作中心
*/
val memberAPI by lazy {
createAPI<MemberAPI>(BaseUrl.member,
defaultCommonHeaderInterceptor,
defaultCommonParamInterceptor
)
}
/**
* 播放器所需的 API, 用于获取视频播放地址
*/
val playerAPI: PlayerAPI by lazy {
Retrofit.Builder()
.baseUrl("https://bilibili.com") //这里的 baseUrl 是没用的
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
addInterceptor(PlayerInterceptor(billingClientProperties) { loginResponse })
addInterceptor(FailureResponseInterceptor)
addNetworkInterceptor(httpLoggingInterceptor)
connectionPool(connectionPool)
}.build())
.build()
.create(PlayerAPI::class.java)
}
/**
* 获取弹幕所用的 API
*/
val danmakuAPI: DanmakuAPI by lazy {
Retrofit.Builder()
.baseUrl(BaseUrl.main)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
addInterceptor(CommonHeaderInterceptor(
Header.ACCEPT to { "application/xhtml+xml,application/xml" },
Header.ACCEPT_ENCODING to { "gzip, deflate" },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent }
))
addInterceptor(defaultCommonParamInterceptor)
addInterceptor(sortAndSignInterceptor)
addNetworkInterceptor(httpLoggingInterceptor)
connectionPool(connectionPool)
}.build())
.build()
.create(DanmakuAPI::class.java)
}
/**
* 直播站
*/
val liveAPI by lazy {
createAPI<LiveAPI>(BaseUrl.live,
CommonHeaderInterceptor(
//如果未登陆则没有 Display-ID
Header.DISPLAY_ID to { userId?.let { "$it-$initTime" } },
Header.BUILD_VERSION_ID to { billingClientProperties.buildVersionId },
Header.USER_AGENT to { billingClientProperties.defaultUserAgent },
Header.DEVICE_ID to { billingClientProperties.hardwareId }
),
CommonParamInterceptor(*defaultCommonParamArray,
Param.ACTION_KEY to { Param.APP_KEY },
Param.DEVICE to { billingClientProperties.platform }
)
)
}
/**
* 登陆
* v3 登陆接口会同时返回 cookies 和 token
* 如果要求验证码, 访问 data 中提供的 url 将打开一个弹窗, 里面会加载 js 并显示极验
* 极验会调用 https://api.geetest.com/ajax.php 上传滑动轨迹, 然后获得 validate 的值
* secCode 的值为 "$validate|jordan"
*
* @throws BilibiliApiException 用户名与密码不匹配(-629)或者需要验证码(极验)(-105)
*/
@Throws(BilibiliApiException::class)
suspend fun login(
username: String, password: String,
//如果登陆请求返回了 "验证码错误!"(-105) 的结果, 那么下一次发送登陆请求就需要带上验证码
challenge: String? = null,
secCode: String? = null,
validate: String? = null
): LoginResponse {
//取得 hash 和 RSA 公钥
val (hash, key) = passportAPI.getKey().await().data.let { data ->
data.hash to data.key.split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "")
}
//解析 RSA 公钥
val publicKey = X509EncodedKeySpec(Base64.getDecoder().decode(key)).let {
KeyFactory.getInstance("RSA").generatePublic(it)
}
//加密密码
//兼容 Android
val cipheredPassword = Cipher.getInstance("RSA/ECB/PKCS1Padding").apply {
init(Cipher.ENCRYPT_MODE, publicKey)
}.doFinal((hash + password).toByteArray()).let {
Base64.getEncoder().encode(it)
}.let {
String(it)
}
return passportAPI.login(username, cipheredPassword, challenge, secCode, validate).await().also {
this.loginResponse = it
}
}
/**
* 登出
* 这个方法不一定是线程安全的, 登出的同时如果进行登陆操作可能引发错误
*/
suspend fun logout() {
val response = loginResponse ?: return
val cookieMap = response.data.cookieInfo.cookies
.associate {
it.name to it.value
}
passportAPI.revoke(cookieMap, response.token).await()
loginResponse = null
}
private val sortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
private val httpLoggingInterceptor = HttpLoggingInterceptor().setLevel(logLevel)
private inline fun <reified T : Any> createAPI(
baseUrl: String,
vararg interceptors: Interceptor
) = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
interceptors.forEach {
addInterceptor(it)
}
addInterceptor(sortAndSignInterceptor)
addInterceptor(FailureResponseInterceptor)
addNetworkInterceptor(httpLoggingInterceptor)
connectionPool(connectionPool)
}.build())
.build()
.create(T::class.java)
companion object {
@Suppress("SpellCheckingInspection")
private val gsonConverterFactory = GsonConverterFactory.create()
private val coroutineCallAdapterFactory = CoroutineCallAdapterFactory()
private val connectionPool = ConnectionPool()
private val traceIdFormat = SimpleDateFormat("yyyyMMddHHmm000ss")
private fun generateTraceId() = traceIdFormat.format(Date())
}
}

View File

@@ -1,74 +0,0 @@
package com.hiczp.bilibili.api
/**
* 客户端固有属性. 包括版本号, 密钥以及硬件编码.
* 默认值对应 5.37.0(release-b220051) 版本.
*/
class BilibiliClientProperties {
/**
* 默认 UA, 用于大多数访问
*/
@Suppress("SpellCheckingInspection")
var defaultUserAgent = "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)"
/**
* Android 平台的 appKey(该默认值为普通版客户端, 非概念版)
*/
var appKey = "1d8b6e7d45233436"
/**
* 由反编译 so 文件得到的 appSecret, 与 appKey 必须匹配
*/
@Suppress("SpellCheckingInspection")
var appSecret = "560c52ccd288fed045859ed18bffd973"
/**
* 获取视频播放地址使用的 appKey, 与访问其他 RestFulAPI 所用的 appKey 是不一样的
*/
@Suppress("SpellCheckingInspection")
var videoAppKey = "iVGUTjsxvpLeuDCf"
/**
* 获取视频播放地址所用的 appSecret
*/
@Suppress("SpellCheckingInspection")
var videoAppSecret = "aHRmhWMLkdeMuILqORnYZocwMBpMEOdt"
/**
* 客户端平台
*/
var platform = "android"
/**
* 客户端类型
* 此属性在旧版客户端不存在
*/
var channel = "html5_app_bili"
/**
* 硬件 ID, 尚不明确生成算法
*/
@Suppress("SpellCheckingInspection")
var hardwareId = "aBRoDWAVeRhsA3FDewMzS3lLMwM"
/**
* 屏幕尺寸, 大屏手机(已经没有小屏手机了)统一为 xxhdpi
* 此参数在新版客户端已经较少使用
*/
var scale = "xxhdpi"
/**
* 版本号
*/
var version = "5.37.0.5370000"
/**
* 构建版本号
*/
var build = "5370000"
/**
* 构建版本 ID, 可能是某种 Hash
*/
var buildVersionId = "XXD9E43D7A1EBB6669597650E3EE417D9E7F5"
}

View File

@@ -1,23 +0,0 @@
package com.hiczp.bilibili.api
import java.security.MessageDigest
//MD5
private val md5Instance = MessageDigest.getInstance("MD5")
fun String.md5() =
StringBuilder(32).apply {
//优化过的 md5 字符串生成算法
md5Instance.digest(toByteArray()).forEach {
val value = it.toInt() and 0xFF
val high = value / 16
val low = value % 16
append(if (high <= 9) '0' + high else 'a' - 10 + high)
append(if (low <= 9) '0' + low else 'a' - 10 + low)
}
}.toString()
/**
* 签名算法为 "$排序后的参数字符串$appSecret".md5()
*/
internal fun calculateSign(sortedQuery: String, appSecret: String) = (sortedQuery + appSecret).md5()

View File

@@ -1,10 +0,0 @@
package com.hiczp.bilibili.api
import kotlin.experimental.ExperimentalTypeInference
@UseExperimental(ExperimentalTypeInference::class)
internal inline fun <T> list(@BuilderInference block: MutableList<T>.() -> Unit): List<T> {
val list = ArrayList<T>()
block(list)
return list
}

View File

@@ -1,8 +0,0 @@
package com.hiczp.bilibili.api
import com.google.gson.Gson
import com.google.gson.JsonParser
internal val gson = Gson()
internal val jsonParser = JsonParser()

View File

@@ -1,9 +0,0 @@
package com.hiczp.bilibili.api
import com.google.gson.JsonArray
@Suppress("NOTHING_TO_INLINE")
inline fun JsonArray.isEmpty() = size() == 0
@Suppress("NOTHING_TO_INLINE")
inline fun JsonArray.isNotEmpty() = size() != 0

View File

@@ -1,56 +0,0 @@
package com.hiczp.bilibili.api
import com.hiczp.bilibili.api.thirdpart.commons.BoundedInputStream
import io.ktor.util.InternalAPI
import kotlinx.io.errors.EOFException
import java.io.InputStream
//减少包引入
//https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/IOUtils.java
fun InputStream.readFully(length: Int): ByteArray {
if (length < 0) {
throw IllegalArgumentException("Length must not be negative: $length")
}
val byteArray = ByteArray(length)
var remaining = length
while (remaining > 0) {
val count = read(byteArray, length - remaining, remaining)
if (count == -1) break
remaining -= count
}
val actual = length - remaining
if (actual != length) {
throw EOFException("Length to read: $length actual: $actual")
}
return byteArray
}
/**
* 以大端模式从流中读取一个 int
*/
@UseExperimental(ExperimentalUnsignedTypes::class)
fun InputStream.readInt(): Int {
val byteArray = readFully(4)
return (byteArray[0].toUByte().toInt() shl 24) or
(byteArray[1].toUByte().toInt() shl 16) or
(byteArray[2].toUByte().toInt() shl 8) or
(byteArray[3].toUByte().toInt())
}
/**
* 以大端模式从流中读取一个 unsigned int
*/
@UseExperimental(ExperimentalUnsignedTypes::class)
fun InputStream.readUInt() = readInt().toUInt()
fun InputStream.bounded(size: Long) = BoundedInputStream(this, size)
@UseExperimental(ExperimentalUnsignedTypes::class)
fun InputStream.bounded(size: UInt) = bounded(size.toLong())
@UseExperimental(InternalAPI::class)
internal fun ByteArray.toPrettyPrintString() = joinToString(prefix = "[", postfix = "]") { "0x%02x".format(it) }

View File

@@ -1,383 +0,0 @@
package com.hiczp.bilibili.api.app
import com.google.gson.JsonObject
import com.hiczp.bilibili.api.app.model.*
import com.hiczp.bilibili.api.retrofit.CommonResponse
import kotlinx.coroutines.Deferred
import retrofit2.http.*
import java.time.Instant
/**
* 总站 API
*/
@Suppress("DeferredIsResult")
interface AppAPI {
/**
* 打开 APP 时将访问此接口来获得 UI 排布顺序
* 包括下方 tab(首页, 频道, 动态, 会员购), 首页的上方 tab(直播, 推荐, 热门, 追番) 以及右上角的 游戏中心, 离线下载, 消息
*/
@GET("/x/resource/show/tab")
fun tab(): Deferred<Tab>
/**
* 登陆完成后将请求一次此接口以获得个人资料
* 如果未登录将返回 {"code":-101,"message":"账号未登录","ttl":1}
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/account/myinfo")
fun myInfo(): Deferred<MyInfo>
/**
* 登陆后也会访问此接口, 返回内容大致与 myInfo() 相同
*/
@GET("/x/v2/account/mine")
fun mine(): Deferred<Mine>
/**
* 侧边栏中动态增加的按钮, 返回信息包含 URI 地址(到对应的 activity)
* 侧拉抽屉
*/
@GET("/x/resource/sidebar")
fun sidebar(): Deferred<Sidebar>
/**
* 首页内容(客户端通过解析返回的内容来生成页面内容, 下同)
* 该 API 没有翻页参数, 同样的参数每次请求都会返回不一样的内容. 刷新和下拉只是简单的重新访问此接口.
* 首页 -> 推荐
*
* @param pull 如果是通过滑动到最顶端来刷新页面的, 那么将是 true, 将页面滑动到最底端来获取更多内容将是 false
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/feed/index")
fun homePage(
@Query("ad_extra") adExtra: String? = null,
@Query("autoplay_card") autoplayCard: Int = 0,
@Query("banner_hash") bannerHash: String? = null,
@Query("column") column: Int = 2,
@Query("device_type") deviceType: Int = 0,
@Query("flush") flush: Int = 0,
@Query("fnval") fnVal: Int = 16,
@Query("fnver") fnVer: Int = 0,
@Query("force_host") forceHost: Int = 0,
@Query("idx") index: Long = Instant.now().epochSecond,
@Query("login_event") loginEvent: Int = 0,
@Query("network") network: String = "mobile",
@Query("open_event") openEvent: String? = null,
@Query("pull") pull: Boolean = true,
@Query("qn") qn: Int = 32,
@Query("recsys_mode") recsysMode: Int = 0
): Deferred<HomePage>
/**
* 热门页面
* 首页 -> 热门
*
* @param index 翻页参数, 一开始为 0, 然后每次滑动到底端就会加 10
* @param ver 第一次请求时没有这个参数, 第二次开始这个参数为上一次请求此接口时的返回值中的 `ver`
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/show/popular/index")
fun popularPage(
@Query("fnval") fnVal: Int = 16,
@Query("fnver") fnVer: Int = 0,
@Query("force_host") forceHost: Int = 0,
@Query("idx") index: Long = 0,
@Query("last_param") lastParam: String? = null,
@Query("login_event") loginEvent: Int = 0,
@Query("qn") qn: Int = 32,
@Query("ver") ver: Long? = null
): Deferred<PopularPage>
/**
* 视频页面(普通视频, 非番剧)
* 包含视频基本信息, 推荐和广告
* 从这个接口得到视频的 cid
* 如果返回内容里的 pages 有多个表明有多个 p, 每个 p 有自己的 cid(外层的 cid 为默认的那个 p 的 cid)
*
* @param aid av 号
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/view")
fun view(
@Query("ad_extra") adExtra: String? = null,
@Query("aid") aid: Long,
@Query("autoplay") autoplay: Int = 0,
@Query("fnval") fnVal: Int = 16,
@Query("fnver") fnVer: Int = 0,
@Query("force_host") forceHost: Int = 0,
@Query("from") from: Int? = null,
@Query("plat") plat: Int = 0,
@Query("qn") qn: Int = 32,
@Query("trackid") trackId: String? = null //all_10.shylf-ai-recsys-120.1550674524909.237
): Deferred<View>
/**
* 点赞(视频)
*
* @param aid 视频的唯一标识
* @param like 为 0 时表示点赞, 为 1 时表示取消点赞
* @param dislike 正常为 0, 为 1 时(like 为 0)表示 取消不喜欢的同时为该视频点赞(等于做了两个操作, 下同)
*/
@POST("/x/v2/view/like")
@FormUrlEncoded
fun like(
@Field("aid") aid: Long,
@Field("like") like: Int = 0,
@Field("dislike") dislike: Int = 0,
@Field("from") from: Int? = null
): Deferred<LikeResponse>
/**
* 不喜欢(视频)
*
* @param aid 视频的唯一标识
* @param dislike 为 0 时表示不喜欢, 为 1 时表示取消不喜欢
* @param like 正常为 0, 为 1 时(dislike 为 0)表示 取消点赞的同时不喜欢该视频
*/
@POST("/x/v2/view/dislike")
@FormUrlEncoded
fun dislike(
@Field("aid") aid: Long,
@Field("like") like: Int = 0,
@Field("dislike") dislike: Int = 0,
@Field("from") from: Int? = null
): Deferred<CommonResponse>
/**
* 投币
* 自制视频能投两个, 转载视频只能投一个. 是转载还是自制在获取视频页面的 API 的 copyright.
*
* @param multiply 投币数量
* @param selectLike 为 1 表示投币的同时为视频点赞, 对番剧投币时, 该值总为 0
* @param upId 该值似乎总为 0
*
* @see view
*/
@Suppress("SpellCheckingInspection")
@POST("/x/v2/view/coin/add")
@FormUrlEncoded
fun addCoin(
@Field("aid") aid: Long,
@Field("avtype") avType: Int = 1,
@Field("from") from: Int? = null,
@Field("multiply") multiply: Int,
@Field("select_like") selectLike: Int = 0,
@Field("upid") upId: Long? = 0
): Deferred<AddCoinResponse>
/**
* 查看某个用户的主页(也可以查看自己)
*
* @param vmId 欲查看的用户的 id
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/space")
fun space(
@Query("from") from: Int? = 0,
@Query("ps") pageSize: Int = 10,
@Query("vmid") vmId: Long
): Deferred<Space>
/**
* 收藏页面
* 侧拉抽屉 -> 收藏
*
* @param vmId 所查看的用户的 id(看自己的收藏也要有该参数)
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/favorite")
fun favoritePage(
@Query("aid") aid: Long = 0,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("vmid") vmId: Long
): Deferred<FavoritePage>
/**
* 收藏的视频
* 侧拉抽屉 -> 收藏 -> 视频 -> (打开一个收藏夹)
*
* @param fid 收藏夹的 id, 在拉取收藏页面时获得
* @param tid 不明确
* @param vmId 用户 id
*
* @see favoritePage
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/favorite/video")
fun favoriteVideo(
@Query("fid") fid: Long,
@Query("order") order: String = "ftime",
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("tid") tid: Long = 0,
@Query("vmid") vmId: Long
): Deferred<FavoriteVideo>
/**
* 收藏的文章
* 这个 API 的返回内容里没有总页数, 真实的客户端会直接访问下一页来确认当前页是不是最后一页
* 侧拉抽屉 -> 收藏 -> 专栏
*/
@GET("/x/v2/favorite/article")
fun favoriteArticle(
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20
): Deferred<FavoriteArticle>
/**
* 大家都在搜(热搜关键字列表)
*
* 上方搜索栏 -> 搜索提示
*
* @param limit 分页大小
*/
@GET("/x/v2/search/hot")
fun searchHot(@Query("limit") limit: Int = 50): Deferred<SearchHot>
/**
* 默认搜索词, 当点击搜索框但是没输入内容时就会显示该词条.
*
* 上方搜索框 -> placeholder
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/search/defaultwords")
fun searchDefaultWords(): Deferred<SearchDefaultWords>
/**
* 搜索联想
*
* 上方搜索框 -> 搜索时的显示的候选项
*/
@GET("/x/v2/search/suggest3")
fun searchSuggest(
@Query("highlight") highlight: Int = 1,
@Query("keyword") keyword: String
): Deferred<SearchSuggest>
/**
* 搜索(综合)
*
* 上方搜索栏
*
* @param order 排序. null/default 默认排序, view 播放多, pubdate 新发布, danmaku 弹幕多
* @param duration 视频时长. 0 全部时长, 1 0-10分钟, 2 10-30分钟, 3 30-60分钟, 4 60+分钟
* @param rid 按某种分区搜索, 编号为数字. null 全部分区.
* @param keyword 搜索的关键字, 下同
* @param from_source 来源, 如果是直接搜索的则为 app_search, 在历史记录里点击的则为 apphistory_search, 从搜索时的候选项里点的为 appsuggest_search
* @param pageNumber 分页, 从 1 开始
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/search")
fun search(
@Query("duration") duration: Int = 0,
@Query("from_source") from_source: String = "app_search",
@Query("highlight") highlight: Int = 1,
@Query("keyword") keyword: String,
@Query("order") order: String? = null,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("recommend") recommend: Int = 1,
@Query("rid") rid: Int? = null
): Deferred<SearchResult>
/**
* 搜索直播间
*
* 上方搜索栏 -> 直播
*
* @param type 搜索的内容的类型, 每种搜索的 type 都是固定的, 下同
*/
@GET("/x/v2/search/live")
fun searchLive(
@Query("keyword") keyword: String,
@Suppress("SpellCheckingInspection")
@Query("order") order: String = "totalrank",
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 4
): Deferred<SearchLiveResult>
/**
* 根据某个类型来进行搜索(自定义)
*/
@GET("/x/v2/search/type")
fun searchType(
@Query("keyword") keyword: String,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int,
@QueryMap additionParam: Map<String, String>
): Deferred<JsonObject>
/**
* 搜索番剧
*
* 上方搜索栏 -> 番剧
*/
@GET("/x/v2/search/type")
fun searchBangumi(
@Query("keyword") keyword: String,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 7
): Deferred<SearchBangumiResult>
/**
* 搜索用户
*
* 上方搜索栏 -> 用户
*
* @param order 排序维度. totalrank 默认排序,fans 粉丝, level 等级.
* @param orderSort 排序顺序. 0 从高到低, 1 从低到高.
* @param userType 用户类型. 0 全部用户, 1 up主, 2 普通用户, 3 认证用户.
*
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/search/type")
fun searchUser(
@Query("highlight") highlight: Int = 1,
@Query("keyword") keyword: String,
@Query("order") order: String = "totalrank",
@Query("order_sort") orderSort: Int? = null,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 2,
@Query("user_type") userType: Int = 0
): Deferred<SearchUserResult>
/**
* 搜索影视(包括动漫的剧场版和纪录片)
*
* 上方搜索栏 -> 影视
*/
@GET("/x/v2/search/type")
fun searchMovie(
@Query("keyword") keyword: String,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 8
): Deferred<SearchMovieResult>
/**
* 搜索文章
*
* 上方搜索栏 -> 专栏
*
* @param order 排序. null 默认排序, pubdate 发布时间, click 按阅读数, scores 按评论数, attention 按点赞数.
* @param categoryId 分类, 编号为数字. 0 全部分类.
*
* @see com.hiczp.bilibili.api.main.MainAPI.articleCategories
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/search/type")
fun searchArticle(
@Query("category_id") categoryId: Int = 0,
@Query("highlight") highlight: Int = 1,
@Query("keyword") keyword: String,
@Query("order") order: String? = null,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 6
): Deferred<SearchArticleResult>
}

View File

@@ -1,21 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class AddCoinResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("like")
var like: Boolean, // false
@SerializedName("prompt")
var prompt: Boolean? // true
)
}

View File

@@ -1,46 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class FavoriteArticle(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("count")
var count: Int, // 1
@SerializedName("items")
var items: List<Item>
) {
data class Item(
@SerializedName("banner_url")
var bannerUrl: String, // https://i0.hdslb.com/bfs/article/97cddc6d048297aedcc5cc498cbfb090358567eb.jpg
@SerializedName("favorite_time")
var favoriteTime: Int, // 1551348963
@SerializedName("goto")
var goto: String, // article
@SerializedName("id")
var id: Long, // 2165049
@SerializedName("image_urls")
var imageUrls: List<String>,
@SerializedName("name")
var name: String, // 京八贱
@SerializedName("param")
var `param`: String, // 2165049
@SerializedName("summary")
var summary: String, // 相信不少玩家在完破了《荒野大镖客救赎2》的单人剧情模式后就开始了线上模式。近期官方推出的更新给大家带来了不少新内容比如新的对决模式、竞速、衣物和表情动作等等还强化了游戏内的通缉和小地图等系统尝试给予玩家更加多样的体验。不过更新上线后所带来的改变反而激起了玩家的怒火首当其冲的便是打猎相关的更动在游戏中玩家可把肢解完毕的猎物尸体卖给肉贩而经历了这次的更新后多人线上模式这项打猎物品的价格直接被砍半玩家也发现不少猎物的掉落物数量有减少的迹象让不少喜爱悠闲打猎生活的玩家大感不满。猎人
@SerializedName("template_id")
var templateId: Int, // 4
@SerializedName("title")
var title: String, // 《荒野大镖客2》线上模式的近期更新引起玩家强烈不满引发争议
@SerializedName("uri")
var uri: String // bilibili://article/2165049
)
}
}

View File

@@ -1,84 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class FavoritePage(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("favorite")
var favorite: Favorite,
@SerializedName("tab")
var tab: Tab
) {
data class Favorite(
@SerializedName("count")
var count: Int, // 1
@SerializedName("items")
var items: List<Item>
) {
data class Item(
/**
* 如果该收藏夹最后一个视频被删除了, 那么将没有封面
*/
@SerializedName("cover")
var cover: List<Cover>?, // null
@SerializedName("cur_count")
var curCount: Int, // 1
@SerializedName("fid")
var fid: Long, // 795158
@SerializedName("media_id")
var mediaId: Long, // 79515830
@SerializedName("mid")
var mid: Long, // 20293030
@SerializedName("name")
var name: String, // 默认收藏夹
@SerializedName("state")
var state: Int // 0
) {
data class Cover(
@SerializedName("aid")
var aid: Long, // 9498716
@SerializedName("pic")
var pic: String, // http://i2.hdslb.com/bfs/archive/3536b8de71da4dd7bf01200db1e6c710b5f4aa0e.png
@SerializedName("type")
var type: Int // 2
)
}
}
data class Tab(
@SerializedName("albums")
var albums: Boolean, // false
@SerializedName("article")
var article: Boolean, // true
@SerializedName("audios")
var audios: Boolean, // false
@SerializedName("cinema")
var cinema: Boolean, // true
@SerializedName("clips")
var clips: Boolean, // false
@SerializedName("favorite")
var favorite: Boolean, // true
@SerializedName("menu")
var menu: Boolean, // false
@SerializedName("pgc_menu")
var pgcMenu: Boolean, // false
@SerializedName("product")
var product: Boolean, // false
@SerializedName("specil")
var specil: Boolean, // false
@SerializedName("ticket")
var ticket: Boolean, // false
@SerializedName("topic")
var topic: Boolean // false
)
}
}

View File

@@ -1,44 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class FavoriteVideo(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("count")
var count: Int, // 1
@SerializedName("items")
var items: List<Item>
) {
data class Item(
@SerializedName("aid")
var aid: Int, // 30702
@SerializedName("danmaku")
var danmaku: Int, // 19363
@SerializedName("goto")
var goto: String, // av
@SerializedName("name")
var name: String, // ⑨搬运君
@SerializedName("param")
var `param`: String, // 30702
@SerializedName("pic")
var pic: String, // http://i1.hdslb.com/bfs/archive/76a045020b6aaa830121132d4a6536d6b82660f4.jpg
@SerializedName("play_num")
var playNum: Long, // 371718
@SerializedName("title")
var title: String, // 【整理发布】妄想学生会
@SerializedName("ugc_pay")
var ugcPay: Long, // 0
@SerializedName("uri")
var uri: String // bilibili://video/30702
)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class LikeResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
/**
* 取消点赞时 toast 为 ""
*/
@SerializedName("toast")
var toast: String // 点赞收到!视频可能推荐哦
)
}

View File

@@ -1,60 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class Mine(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("audio_type")
var audioType: Int, // 0
@SerializedName("bcoin")
var bcoin: Int, // 5
@SerializedName("coin")
var coin: Double, // 892.7
@SerializedName("dynamic")
var `dynamic`: Int, // 8
@SerializedName("face")
var face: String, // http://i0.hdslb.com/bfs/face/0434dccc0ec4de223e8ca374dea06a6e1e8eb471.jpg
@SerializedName("follower")
var follower: Int, // 512
@SerializedName("following")
var following: Int, // 106
@SerializedName("level")
var level: Int, // 5
@SerializedName("mid")
var mid: Long, // 2866663
@SerializedName("name")
var name: String, // hyx5020
@SerializedName("new_followers")
var newFollowers: Int, // 0
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("rank")
var rank: Int, // 10000
@SerializedName("sex")
var sex: Int, // 0
@SerializedName("show_creative")
var showCreative: Int, // 1
@SerializedName("show_videoup")
var showVideoup: Int, // 1
@SerializedName("silence")
var silence: Int, // 0
@SerializedName("vip_type")
var vipType: Int // 2
) {
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
}
}

View File

@@ -1,67 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class MyInfo(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("birthday")
var birthday: String, // 1995-11-18
@SerializedName("coins")
var coins: Int, // 1025
@SerializedName("email_status")
var emailStatus: Int, // 0
@SerializedName("face")
var face: String, // http://i1.hdslb.com/bfs/face/4f65e79399ad5a1bf3f877851b2f819d5870b494.jpg
@SerializedName("identification")
var identification: Int, // 1
@SerializedName("level")
var level: Int, // 4
@SerializedName("mid")
var mid: Long, // 20293030
@SerializedName("name")
var name: String, // czp3009
@SerializedName("official")
var official: Official,
@SerializedName("rank")
var rank: Int, // 10000
@SerializedName("sex")
var sex: Int, // 0
@SerializedName("sign")
var sign: String,
@SerializedName("silence")
var silence: Int, // 0
@SerializedName("tel_status")
var telStatus: Int, // 1
@SerializedName("vip")
var vip: Vip
) {
data class Official(
@SerializedName("desc")
var desc: String,
@SerializedName("role")
var role: Int, // 0
@SerializedName("title")
var title: String
)
data class Vip(
@SerializedName("due_date")
var dueDate: Long, // 0
@SerializedName("status")
var status: Int, // 0
@SerializedName("type")
var type: Int, // 0
@SerializedName("vip_pay_type")
var vipPayType: Int // 0
)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchArticleResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("items")
var items: List<Item>,
@SerializedName("pages")
var pages: Int, // 50
@SerializedName("total")
var total: Int, // 0
@SerializedName("trackid")
var trackid: String // 2251647613743955310
) {
data class Item(
@SerializedName("badge")
var badge: String, // 专栏
@SerializedName("desc")
var desc: String, // 今天来和大家说下《刀剑神域3》第19集的先行图剧情与其说是第19集确切的讲应该是18.5集根据先行图来看这一集是前面的合集主要讲的是桐人和尤吉欧的经历。在第18集结尾桐人不小心说出了赛鲁卡的名字导致爱丽丝记忆发生了错乱隐约想起了自己的妹妹而接下来就是桐人会和爱丽丝说关于她的事了而所说的方式根据先行图来看事回忆杀方式。下面就简单和大家说下官方给出的先行图。先行图01a姐植入记忆水晶桐人既然要告诉爱丽丝真相也会给她说这一切的元凶就是最高祭师a姐从上图先行图可以看出这是a姐
@SerializedName("goto")
var goto: String, // article
@SerializedName("id")
var id: Long, // 2052980
@SerializedName("image_urls")
var imageUrls: List<String>,
@SerializedName("like")
var like: Int, // 116
@SerializedName("mid")
var mid: Int, // 12043763
@SerializedName("name")
var name: String, // 老白与动漫
@SerializedName("param")
var `param`: String, // 2052980
@SerializedName("play")
var play: Int, // 19101
@SerializedName("reply")
var reply: Int, // 82
@SerializedName("template_id")
var templateId: Int, // 3
@SerializedName("title")
var title: String, // <em class="keyword">刀剑神域</em>3第19集先行桐人告诉爱丽丝真相又是回忆杀剧情
@SerializedName("uri")
var uri: String, // bilibili://article/2052980
@SerializedName("view")
var view: Int // 19101
)
}
}

View File

@@ -1,100 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchBangumiResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("items")
var items: List<Item>,
@SerializedName("pages")
var pages: Int, // 1
@SerializedName("total")
var total: Int, // 1
@SerializedName("trackid")
var trackid: String // 1357843021891149439
) {
data class Item(
@SerializedName("area")
var area: String, // 日本
@SerializedName("badge")
var badge: String, // 番剧
@SerializedName("badges")
var badges: List<Badge>,
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/bangumi/4d9f43eb3dba572797f8915f8f28efce9e58d756.jpg
@SerializedName("cv")
var cv: String, // 桐人(桐谷和人):松冈祯丞亚丝娜(结城明日奈):户松遥爱丽丝:茅野爱衣尤吉欧:岛崎信长赛鲁卡:前田佳织里罗妮耶·亚拉贝尔:近藤玲奈蒂洁·修特利尼:石原夏织神代凛子:小林沙苗菊冈诚二郎:森川智之莱欧斯·安提诺斯:岩濑周平温贝尔·吉泽克:木岛隆一索尔狄丽娜·塞路尔特:潘惠美沃罗·利凡玎:村田太志诗乃(朝田诗乃):泽城美雪 强尼·布莱克(金本敦):逢坂良太 西莉卡(绫野珪子):日高里菜 莉兹贝特(筱崎里香):高垣彩阳
@SerializedName("episodes")
var episodes: List<Episode>,
@SerializedName("goto")
var goto: String, // bangumi
@SerializedName("is_atten")
var isAtten: Int, // 1
@SerializedName("is_selection")
var isSelection: Int, // 1
@SerializedName("label")
var label: String, // 小说改/热血/奇幻/战斗/励志
@SerializedName("media_type")
var mediaType: Int, // 1
@SerializedName("param")
var `param`: String, // 130412
@SerializedName("ptime")
var ptime: Int, // 1538841600
@SerializedName("rating")
var rating: Double, // 9.2
@SerializedName("season_id")
var seasonId: Int, // 25510
@SerializedName("season_type")
var seasonType: Int, // 1
@SerializedName("season_type_name")
var seasonTypeName: String, // 番剧
@SerializedName("staff")
var staff: String, // 原作:川原砾原作插画 / 角色设计草案abec导演小野学助理导演佐久间贵史角色设计足立慎吾、铃木豪、西口智也总作画监督铃木豪、西口智也动作作画监督菅野芳弘、竹内哲也美术导演小川友佳子、渡边佳人美术设定森冈贤一、谷内优穗色彩设计中野尚美CG导演云藤隆太音响导演岩浪美和效果小山恭正音响制作ソニルード音乐梶浦由记制片EGG FIRM、Straight Edge制作A-1 Pictures
@SerializedName("style")
var style: String, // 小说改/热血/奇幻/战斗/励志
@SerializedName("title")
var title: String, // 刀剑神域 Alicization
@SerializedName("uri")
var uri: String, // https://www.bilibili.com/bangumi/play/ss25510/
@SerializedName("vote")
var vote: Int // 48497
) {
data class Badge(
@SerializedName("bg_color")
var bgColor: String, // #FB7299
@SerializedName("bg_color_night")
var bgColorNight: String, // #BB5B76
@SerializedName("bg_style")
var bgStyle: Int, // 1
@SerializedName("border_color")
var borderColor: String, // #FB7299
@SerializedName("border_color_night")
var borderColorNight: String, // #BB5B76
@SerializedName("text")
var text: String, // 会员抢先
@SerializedName("text_color")
var textColor: String, // #FFFFFF
@SerializedName("text_color_night")
var textColorNight: String // #E5E5E5
)
data class Episode(
@SerializedName("index")
var index: String, // 21
@SerializedName("param")
var `param`: String, // 250557
@SerializedName("uri")
var uri: String // https://www.bilibili.com/bangumi/play/ep250557
)
}
}
}

View File

@@ -1,25 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchDefaultWords(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("param")
var `param`: String, // 5157193909505109430
@SerializedName("show")
var show: String, // 如果绝地求生也能滑铲
@SerializedName("trackid")
var trackid: String, // 14016083035920227490
@SerializedName("word")
var word: String // av46169875
)
}

View File

@@ -1,30 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchHot(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("list")
var list: List<Hot>,
@SerializedName("trackid")
var trackid: String // 3482080616107297898
) {
data class Hot(
@SerializedName("keyword")
var keyword: String, // 凹凸世界
@SerializedName("name_type")
var nameType: String,
@SerializedName("status")
var status: String
)
}
}

View File

@@ -1,82 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchLiveResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("live_master")
var liveMaster: LiveMaster,
@SerializedName("live_room")
var liveRoom: LiveRoom,
@SerializedName("pages")
var pages: Int, // 3
@SerializedName("total")
var total: Int, // 0
@SerializedName("trackid")
var trackid: String // 14587616663833842975
) {
data class LiveRoom(
@SerializedName("items")
var items: List<Item>,
@SerializedName("pages")
var pages: Int, // 3
@SerializedName("total")
var total: Int, // 57
@SerializedName("trackid")
var trackid: String
) {
data class Item(
@SerializedName("area_v2_name")
var areaV2Name: String, // 300英雄
@SerializedName("attentions")
var attentions: Int, // 2
@SerializedName("badge")
var badge: String, // 直播
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/live/16181996f2260024f861db58d0d3dea2dd390930.jpg
@SerializedName("goto")
var goto: String, // live
@SerializedName("live_status")
var liveStatus: Int, // 2
@SerializedName("mid")
var mid: Long, // 82542745
@SerializedName("name")
var name: String, // SAO刀剑神域
@SerializedName("online")
var online: Int, // 7
@SerializedName("param")
var `param`: String, // 3387258
@SerializedName("region")
var region: Int, // 4
@SerializedName("roomid")
var roomid: Long, // 3387258
@SerializedName("tags")
var tags: String, // 点点关注
@SerializedName("title")
var title: String, // SAO刀剑神域的直播间
@SerializedName("type")
var type: String, // live_room
@SerializedName("uri")
var uri: String // bilibili://live/3387258?broadcast_type=0
)
}
data class LiveMaster(
@SerializedName("pages")
var pages: Int, // 0
@SerializedName("total")
var total: Int, // 0
@SerializedName("trackid")
var trackid: String
)
}
}

View File

@@ -1,92 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchMovieResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("items")
var items: List<Item>,
@SerializedName("pages")
var pages: Int, // 1
@SerializedName("total")
var total: Int, // 2
@SerializedName("trackid")
var trackid: String // 11415000413319714311
) {
data class Item(
@SerializedName("area")
var area: String, // 日本
@SerializedName("badge")
var badge: String, // 电影
/**
* badge 的特殊样式
*/
@SerializedName("badges")
var badges: List<Map<String, String>>,
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/bangumi/aef914fac9edfa518c4df9a82d5d3d0cba08a451.jpg
/**
* 类型为 纪录片 时没有 cv, 其他一些字段同理
*/
@SerializedName("cv")
var cv: String?, // 曹旭鹏、阎萌萌、碧涓、惠霖、梦娇
/**
* 有些纪录片有分集, 有些没有
*/
@SerializedName("episodes")
var episodes: List<Episode>?,
@SerializedName("goto")
var goto: String, // movie
/**
* 一些纪录片没有 label, 其他一些字段同理
*/
@SerializedName("label")
var label: String?, // 演员:曹旭鹏、阎萌萌、碧涓、惠霖、梦娇
/**
* 2 为剧场版动画, 3 为纪录片
*/
@SerializedName("media_type")
var mediaType: Int, // 2
@SerializedName("param")
var `param`: String, // 115472
@SerializedName("ptime")
var ptime: Long, // 1505404800
@SerializedName("rating")
var rating: Double, // 5.8
@SerializedName("season_id")
var seasonId: Int, // 12767
@SerializedName("season_type")
var seasonType: Int, // 2
@SerializedName("season_type_name")
var seasonTypeName: String, // 电影
@SerializedName("staff")
var staff: String?, // 导演:伊藤智彦编剧:川原砾
@SerializedName("style")
var style: String, // 科幻/动画
@SerializedName("title")
var title: String, // 刀剑神域:序列之争(中文)
@SerializedName("uri")
var uri: String, // https://www.bilibili.com/bangumi/play/ss12767/
@SerializedName("vote")
var vote: Int? // 581
) {
data class Episode(
@SerializedName("index")
var index: String, // 4
@SerializedName("param")
var `param`: String, // 250006
@SerializedName("uri")
var uri: String // https://www.bilibili.com/bangumi/play/ep250006
)
}
}
}

View File

@@ -1,186 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class SearchResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("array")
var array: Int, // 1
@SerializedName("attribute")
var attribute: Int, // 1
@SerializedName("item")
var item: List<Item>,
@SerializedName("items")
var items: JsonElement,
@SerializedName("nav")
var nav: List<Nav>,
@SerializedName("page")
var page: Int, // 1
@SerializedName("trackid")
var trackid: String // 9256129479667154639
) {
data class Nav(
@SerializedName("name")
var name: String, // 专栏
@SerializedName("pages")
var pages: Int, // 50
@SerializedName("total")
var total: Int, // 1000
@SerializedName("type")
var type: Int // 6
)
data class Item(
@SerializedName("area")
var area: String, // 日本
@SerializedName("author")
var author: String, // 那位滑稽
@SerializedName("badge")
var badge: String, // 专栏
@SerializedName("badges")
var badges: List<Badge>,
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/archive/a4a58b6772d0de16df6e9a7d3e208fd52a552710.jpg
@SerializedName("cv")
var cv: String, // 曹旭鹏、阎萌萌、碧涓、惠霖、梦娇
@SerializedName("danmaku")
var danmaku: Int, // 196
@SerializedName("desc")
var desc: String, // 刀剑神域第一季作品以 2022 年为舞台大厂牌电子机械制造商“ARGUS”开发出“NERvGear”能连结虚拟世界的机器。完全的虚拟实境终于能够实现。主角桐人使用 NERvGear 游玩 VR MMORPG《Sword Art Online》的玩家幸运地参与过封测并买下正式版的桐人和正式营运就马上“完全潜行”享受着正式版的 SAO 世界。就在游戏四小时多后,桐人发现到“登出”指令竟然消失。认为只是系统暂时出错的桐人和开始陷入混乱的所有玩家们一起被传送到开始地点广场,并传来游戏设计者的死亡游戏
@SerializedName("duration")
var duration: String, // 591:52
@SerializedName("episodes")
var episodes: List<Episode>,
@SerializedName("face")
var face: String, // http://i0.hdslb.com/bfs/face/d34c34fca6471f07e60db3a7007cc5c2eb6bd785.jpg
@SerializedName("goto")
var goto: String, // recommend_word
@SerializedName("id")
var id: Int, // 2227576
@SerializedName("image_urls")
var imageUrls: List<String>,
@SerializedName("is_atten")
var isAtten: Int, // 1
@SerializedName("is_selection")
var isSelection: Int, // 1
@SerializedName("label")
var label: String, // 演员:曹旭鹏、阎萌萌、碧涓、惠霖、梦娇
@SerializedName("like")
var like: Int, // 13
@SerializedName("linktype")
var linktype: String, // query_rec
@SerializedName("list")
var list: List<X>,
@SerializedName("media_type")
var mediaType: Int, // 2
@SerializedName("mid")
var mid: Long, // 382820503
@SerializedName("new_rec_tags")
var newRecTags: List<NewRecTag>,
@SerializedName("param")
var `param`: String, // 33673993
@SerializedName("play")
var play: Int, // 324938
@SerializedName("position")
var position: Int, // 21
@SerializedName("ptime")
var ptime: Long, // 1505404800
@SerializedName("rating")
var rating: Double, // 5.8
@SerializedName("rec_tags")
var recTags: List<String>,
@SerializedName("reply")
var reply: Int, // 4
@SerializedName("season_id")
var seasonId: Int, // 12767
@SerializedName("season_type")
var seasonType: Int, // 2
@SerializedName("season_type_name")
var seasonTypeName: String, // 电影
@SerializedName("staff")
var staff: String, // 导演:伊藤智彦编剧:川原砾
@SerializedName("style")
var style: String, // 科幻/动画
@SerializedName("template_id")
var templateId: Int, // 4
@SerializedName("title")
var title: String, // 相关推荐
@SerializedName("trackid")
var trackid: String, // 9256129479667154639
@SerializedName("uri")
var uri: String, // bilibili://video/33673993?player_width=352&player_height=288&player_rotate=0
@SerializedName("view")
var view: Int, // 2310
@SerializedName("vote")
var vote: Int // 581
) {
data class NewRecTag(
@SerializedName("bg_color")
var bgColor: String, // #FAAB4B
@SerializedName("bg_color_night")
var bgColorNight: String, // #BA833F
@SerializedName("bg_style")
var bgStyle: Int, // 1
@SerializedName("border_color")
var borderColor: String, // #FAAB4B
@SerializedName("border_color_night")
var borderColorNight: String, // #BA833F
@SerializedName("text")
var text: String, // MINECRAFT
@SerializedName("text_color")
var textColor: String, // #FFFFFFFF
@SerializedName("text_color_night")
var textColorNight: String // #E5E5E5
)
data class Badge(
@SerializedName("bg_color")
var bgColor: String, // #FB7299
@SerializedName("bg_color_night")
var bgColorNight: String, // #BB5B76
@SerializedName("bg_style")
var bgStyle: Int, // 1
@SerializedName("border_color")
var borderColor: String, // #FB7299
@SerializedName("border_color_night")
var borderColorNight: String, // #BB5B76
@SerializedName("text")
var text: String, // 会员抢先
@SerializedName("text_color")
var textColor: String, // #FFFFFF
@SerializedName("text_color_night")
var textColorNight: String // #E5E5E5
)
data class X(
@SerializedName("from_source")
var fromSource: String, // query_rec_search
@SerializedName("param")
var `param`: String, // 7852399559249609627
@SerializedName("title")
var title: String, // FATE 刀剑神域
@SerializedName("type")
var type: String // query_rec
)
data class Episode(
@SerializedName("index")
var index: String, // 21
@SerializedName("param")
var `param`: String, // 250557
@SerializedName("uri")
var uri: String // https://www.bilibili.com/bangumi/play/ep250557
)
}
}
}

View File

@@ -1,36 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchSuggest(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("list")
var list: List<Suggest>,
@SerializedName("trackid")
var trackid: String // 8544564822819789247
) {
data class Suggest(
@SerializedName("from")
var from: String, // search
@SerializedName("keyword")
var keyword: String, // 刀剑神域 ALICIZATION
@SerializedName("position")
var position: Int, // 10
@SerializedName("sug_type")
var sugType: String?, // 番剧
@SerializedName("term_type")
var termType: Int, // 8
@SerializedName("title")
var title: String // 刀剑神域 ALICIZATION
)
}
}

View File

@@ -1,86 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class SearchUserResult(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("items")
var items: List<Item>,
@SerializedName("pages")
var pages: Int, // 2
@SerializedName("total")
var total: Int, // 0
@SerializedName("trackid")
var trackid: String // 15623048138266462990
) {
data class Item(
@SerializedName("archives")
var archives: Int, // 1
@SerializedName("av_items")
var avItems: List<AvItem>,
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/face/c3200c52ae76004fbbab44010990431d0604aee5.jpg
@SerializedName("fans")
var fans: Int, // 3
@SerializedName("goto")
var goto: String, // author
@SerializedName("is_up")
var isUp: Boolean, // true
@SerializedName("level")
var level: Int, // 3
@SerializedName("live_status")
var liveStatus: Int, // 1
@SerializedName("live_uri")
var liveUri: String, // bilibili://live/3234638?broadcast_type=0
@SerializedName("mid")
var mid: Long, // 32557668
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("param")
var `param`: String, // 32557668
@SerializedName("roomid")
var roomid: Long, // 3234638
@SerializedName("sign")
var sign: String, // 担心额刚好阿西
@SerializedName("title")
var title: String, // 刀剑神域小漠
@SerializedName("uri")
var uri: String // bilibili://author/32557668
) {
data class OfficialVerify(
@SerializedName("type")
var type: Int // 127
)
data class AvItem(
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/archive/95be7c1a940dda2bbf4c33213df94eb650e44d10.jpg
@SerializedName("ctime")
var ctime: Int, // 1535755416
@SerializedName("danmaku")
var danmaku: Int, // 1
@SerializedName("duration")
var duration: String, // 3:1
@SerializedName("goto")
var goto: String, // av
@SerializedName("param")
var `param`: String, // 30843572
@SerializedName("play")
var play: Int, // 15
@SerializedName("title")
var title: String, // 官方认证:非洲正品大酋长
@SerializedName("uri")
var uri: String // bilibili://video/30843572?player_width=1920&player_height=1080&player_rotate=0
)
}
}
}

View File

@@ -1,33 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class Sidebar(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<SidebarElement>,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class SidebarElement(
@SerializedName("id")
var id: Int, // 13
@SerializedName("logo")
var logo: String, // http://i0.hdslb.com/bfs/archive/91f7ba40e54502f7479c8d355e4298989bb8ebce.png
@SerializedName("module")
var module: Int, // 1
@SerializedName("name")
var name: String, // 会员购中心
@SerializedName("online_time")
var onlineTime: Int, // 0
@SerializedName("param")
var `param`: String, // bilibili://mall/mine?msource=mine
@SerializedName("rank")
var rank: Int, // 300
@SerializedName("tip")
var tip: Int // 1
)
}

View File

@@ -1,500 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class Space(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("album")
var album: Album,
/**
* 投稿
*/
@SerializedName("archive")
var archive: Archive,
@SerializedName("article")
var article: Article,
@SerializedName("audios")
var audios: Audios,
@SerializedName("card")
var card: Card,
@SerializedName("clip")
var clip: Clip,
/**
* 最近投币
*/
@SerializedName("coin_archive")
var coinArchive: CoinArchive,
@SerializedName("elec")
var elec: Elec,
@SerializedName("favourite")
var favourite: Favourite,
@SerializedName("images")
var images: Images,
/**
* Ta推荐的视频
*/
@SerializedName("like_archive")
var likeArchive: LikeArchive,
@SerializedName("live")
var live: Live,
@SerializedName("medal")
var medal: Int, // 1
@SerializedName("relation")
var relation: Int, // 1
@SerializedName("season")
var season: Season,
@SerializedName("setting")
var setting: Setting,
@SerializedName("tab")
var tab: Tab
) {
data class Archive(
@SerializedName("count")
var count: Int, // 8
@SerializedName("item")
var item: List<Item>
) {
data class Item(
@SerializedName("author")
var author: String, // hyx5020
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/archive/603e9b0d67400ce05199e0c8ff14cc4204c7e4e5.jpg
@SerializedName("ctime")
var ctime: Int, // 1475686898
@SerializedName("danmaku")
var danmaku: Int, // 2
@SerializedName("duration")
var duration: Int, // 3720
@SerializedName("goto")
var goto: String, // av
@SerializedName("length")
var length: String,
@SerializedName("param")
var `param`: String, // 6557595
@SerializedName("play")
var play: Int, // 246
@SerializedName("title")
var title: String, // 蓝色起源 New Shepard In-flight Escape Test
@SerializedName("tname")
var tname: String, // 趣味科普人文
@SerializedName("ugc_pay")
var ugcPay: Int, // 0
@SerializedName("uri")
var uri: String // bilibili://video/6557595
)
}
data class Favourite(
@SerializedName("count")
var count: Int, // 0
@SerializedName("item")
var item: List<Item>
) {
data class Item(
@SerializedName("atten_count")
var attenCount: Int, // 0
@SerializedName("ctime")
var ctime: Long, // 1451133174
@SerializedName("cur_count")
var curCount: Int, // 1
@SerializedName("fid")
var fid: Long, // 795158
@SerializedName("max_count")
var maxCount: Int, // 50000
@SerializedName("media_id")
var mediaId: Long, // 79515830
@SerializedName("mid")
var mid: Long, // 20293030
@SerializedName("mtime")
var mtime: Int, // 1544629663
@SerializedName("name")
var name: String, // 默认收藏夹
@SerializedName("state")
var state: Int // 0
)
}
data class Images(
@SerializedName("imgUrl")
var imgUrl: String
)
data class Season(
@SerializedName("count")
var count: Int, // 0
@SerializedName("item")
var item: List<Item>
) {
data class Item(
@SerializedName("attention")
var attention: String, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/de944b7c9306932d8dd3dcaeaf2eeec8670deec5.png
@SerializedName("finish")
var finish: Int, // 0
@SerializedName("goto")
var goto: String, // bangumi
@SerializedName("index")
var index: String,
@SerializedName("is_finish")
var isFinish: String,
@SerializedName("is_started")
var isStarted: Int, // 1
@SerializedName("mtime")
var mtime: Int, // 0
@SerializedName("newest_ep_id")
var newestEpId: String,
@SerializedName("newest_ep_index")
var newestEpIndex: String, // 8
@SerializedName("param")
var `param`: String, // 26284
@SerializedName("title")
var title: String, // 盾之勇者成名录
@SerializedName("total_count")
var totalCount: String, // 25
@SerializedName("uri")
var uri: String // http://bangumi.bilibili.com/anime/26284
)
}
data class Article(
@SerializedName("count")
var count: Int, // 0
@SerializedName("item")
var item: List<JsonElement>,
@SerializedName("lists")
var lists: List<JsonElement>,
@SerializedName("lists_count")
var listsCount: Int // 0
)
data class Album(
@SerializedName("count")
var count: Int, // 0
@SerializedName("has_more")
var hasMore: Int, // 0
@SerializedName("item")
var item: List<JsonElement>,
@SerializedName("next_offset")
var nextOffset: Int // 0
)
data class CoinArchive(
@SerializedName("count")
var count: Int, // 1
@SerializedName("item")
var item: List<Item>
) {
data class Item(
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/archive/0b49549eeefd58441ad88613fe460630182d1afe.jpg
@SerializedName("ctime")
var ctime: Int, // 1548875105
@SerializedName("danmaku")
var danmaku: Int, // 2
@SerializedName("duration")
var duration: Int, // 169
@SerializedName("goto")
var goto: String, // av
@SerializedName("length")
var length: String,
@SerializedName("param")
var `param`: String, // 42179433
@SerializedName("play")
var play: Int, // 3373
@SerializedName("title")
var title: String, // 《吹响吧上低音号》Dream Solister 上低音号四重奏
@SerializedName("tname")
var tname: String,
@SerializedName("ugc_pay")
var ugcPay: Int, // 0
@SerializedName("uri")
var uri: String // bilibili://video/42179433
)
}
data class Elec(
@SerializedName("elec_num")
var elecNum: Int, // 0
@SerializedName("elec_set")
var elecSet: ElecSet,
@SerializedName("list")
var list: List<JsonElement>,
@SerializedName("show")
var show: Boolean // true
) {
data class ElecSet(
@SerializedName("elec_list")
var elecList: List<Elec>,
@SerializedName("elec_theme")
var elecTheme: Int, // 0
@SerializedName("integrity_rate")
var integrityRate: Double, // 10.0
@SerializedName("rmb_rate")
var rmbRate: Double, // 10.0
@SerializedName("round_mode")
var roundMode: Int // 0
) {
data class Elec(
@SerializedName("elec_num")
var elecNum: Int, // 0
@SerializedName("is_customize")
var isCustomize: Int, // 1
@SerializedName("max_elec")
var maxElec: Int, // 99999
@SerializedName("min_elec")
var minElec: Int, // 20
@SerializedName("title")
var title: String // 自定义
)
}
}
data class Setting(
@SerializedName("bangumi")
var bangumi: Int, // 0
@SerializedName("channel")
var channel: Int, // 1
@SerializedName("coins_video")
var coinsVideo: Int, // 1
@SerializedName("fav_video")
var favVideo: Int, // 0
@SerializedName("groups")
var groups: Int, // 0
@SerializedName("likes_video")
var likesVideo: Int, // 1
@SerializedName("played_game")
var playedGame: Int // 0
)
data class Card(
@SerializedName("DisplayRank")
var displayRank: String,
@SerializedName("approve")
var approve: Boolean, // false
@SerializedName("article")
var article: Int, // 0
@SerializedName("attention")
var attention: Int, // 113
@SerializedName("attentions")
var attentions: JsonElement?, // null
@SerializedName("birthday")
var birthday: String,
@SerializedName("description")
var description: String,
@SerializedName("end_time")
var endTime: Int, // 0
@SerializedName("face")
var face: String, // http://i0.hdslb.com/bfs/face/0434dccc0ec4de223e8ca374dea06a6e1e8eb471.jpg
@SerializedName("fans")
var fans: Int, // 539
@SerializedName("friend")
var friend: Int, // 0
@SerializedName("level_info")
var levelInfo: LevelInfo,
@SerializedName("mid")
var mid: String, // 2866663
@SerializedName("name")
var name: String, // hyx5020
@SerializedName("nameplate")
var nameplate: Nameplate,
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("pendant")
var pendant: Pendant,
@SerializedName("place")
var place: String,
@SerializedName("rank")
var rank: String,
@SerializedName("regtime")
var regtime: Int, // 0
@SerializedName("sex")
var sex: String, // 保密
@SerializedName("sign")
var sign: String, // 简介?不存在的
@SerializedName("silence")
var silence: Int, // 0
@SerializedName("silence_url")
var silenceUrl: String,
@SerializedName("spacesta")
var spacesta: Int, // 0
@SerializedName("vip")
var vip: Vip
) {
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 1623081600000
@SerializedName("vipStatus")
var vipStatus: Int, // 1
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 2
)
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("role")
var role: Int, // 0
@SerializedName("title")
var title: String,
@SerializedName("type")
var type: Int // -1
)
data class Pendant(
@SerializedName("expire")
var expire: Int, // 0
@SerializedName("image")
var image: String,
@SerializedName("name")
var name: String,
@SerializedName("pid")
var pid: Int // 0
)
data class Nameplate(
@SerializedName("condition")
var condition: String,
@SerializedName("image")
var image: String,
@SerializedName("image_small")
var imageSmall: String,
@SerializedName("level")
var level: String,
@SerializedName("name")
var name: String,
@SerializedName("nid")
var nid: Int // 0
)
data class LevelInfo(
@SerializedName("current_exp")
var currentExp: Int, // 11224
@SerializedName("current_level")
var currentLevel: Int, // 5
@SerializedName("current_min")
var currentMin: Int, // 10800
@SerializedName("next_exp")
var nextExp: Int // 28800
)
}
data class Tab(
@SerializedName("album")
var album: Boolean, // false
@SerializedName("archive")
var archive: Boolean, // true
@SerializedName("article")
var article: Boolean, // false
@SerializedName("audios")
var audios: Boolean, // false
@SerializedName("bangumi")
var bangumi: Boolean, // false
@SerializedName("clip")
var clip: Boolean, // false
@SerializedName("coin")
var coin: Boolean, // true
@SerializedName("community")
var community: Boolean, // false
@SerializedName("dynamic")
var `dynamic`: Boolean, // true
@SerializedName("favorite")
var favorite: Boolean, // false
@SerializedName("like")
var like: Boolean, // true
@SerializedName("mall")
var mall: Boolean, // false
@SerializedName("shop")
var shop: Boolean // false
)
data class Audios(
@SerializedName("count")
var count: Int, // 0
@SerializedName("item")
var item: List<JsonElement>
)
data class LikeArchive(
@SerializedName("count")
var count: Int, // 7
@SerializedName("item")
var item: List<Item>
) {
data class Item(
@SerializedName("cover")
var cover: String, // http://i1.hdslb.com/bfs/archive/6e246c830b26591924984f0f9275eede61621d80.jpg
@SerializedName("ctime")
var ctime: Int, // 1527760833
@SerializedName("danmaku")
var danmaku: Int, // 11309
@SerializedName("duration")
var duration: Int, // 215
@SerializedName("goto")
var goto: String, // av
@SerializedName("length")
var length: String,
@SerializedName("param")
var `param`: String, // 24180113
@SerializedName("play")
var play: Int, // 1325557
@SerializedName("title")
var title: String, // 【洛天依/言和原创曲】反派死于话多 (真实童话 Act.3)【PV付】
@SerializedName("tname")
var tname: String,
@SerializedName("ugc_pay")
var ugcPay: Int, // 0
@SerializedName("uri")
var uri: String // bilibili://video/24180113
)
}
data class Live(
@SerializedName("broadcast_type")
var broadcastType: Int, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/live/af5786fa6d011c143fde5275c7af011f2c54a619.jpg
@SerializedName("liveStatus")
var liveStatus: Int, // 0
@SerializedName("online")
var online: Int, // 103
@SerializedName("roomStatus")
var roomStatus: Int, // 1
@SerializedName("roomid")
var roomid: Long, // 29434
@SerializedName("roundStatus")
var roundStatus: Int, // 0
@SerializedName("title")
var title: String, // 直播
@SerializedName("url")
var url: String // http://live.bilibili.com/29434
)
data class Clip(
@SerializedName("count")
var count: Int, // 0
@SerializedName("has_more")
var hasMore: Int, // 0
@SerializedName("item")
var item: List<JsonElement>,
@SerializedName("next_offset")
var nextOffset: Int // 0
)
}
}

View File

@@ -1,29 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class Tab(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Map<String, List<UIElement>>,
@SerializedName("message")
var message: String, // 0
@SerializedName("ver")
var ver: String // 5720051238481856755
) {
data class UIElement(
@SerializedName("default_selected")
var defaultSelected: Int, // 1
@SerializedName("id")
var id: Int, // 30
@SerializedName("name")
var name: String, // 追番
@SerializedName("pos")
var pos: Int, // 4
@SerializedName("tab_id")
var tabId: String, // 追番Tab
@SerializedName("uri")
var uri: String // bilibili://pgc/home
)
}

View File

@@ -1,449 +0,0 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class View(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("aid")
var aid: Int, // 44172743
@SerializedName("attribute")
var attribute: Int, // 16512
@SerializedName("cid")
var cid: Int, // 77356986
@SerializedName("cm_config")
var cmConfig: CmConfig,
@SerializedName("cms")
var cms: List<Cm>,
/**
* copyright 为 1 时表示自制, 2 表示转载
*/
@SerializedName("copyright")
var copyright: Int, // 1
@SerializedName("ctime")
var ctime: Int, // 1550654012
@SerializedName("desc")
var desc: String,
@SerializedName("dimension")
var dimension: Dimension,
@SerializedName("dislike_reasons")
var dislikeReasons: List<DislikeReason>,
@SerializedName("dm_seg")
var dmSeg: Int, // 1
@SerializedName("duration")
var duration: Int, // 444
@SerializedName("dynamic")
var `dynamic`: String, // #流浪地球##木星##太阳#
@SerializedName("elec")
var elec: Elec,
@SerializedName("owner")
var owner: Owner,
@SerializedName("owner_ext")
var ownerExt: OwnerExt,
@SerializedName("pages")
var pages: List<Page>,
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/archive/783445f04541299ee84de21a2479cce88d8268ff.jpg
@SerializedName("pubdate")
var pubdate: Int, // 1550654012
@SerializedName("relates")
var relates: List<Relate>,
@SerializedName("req_user")
var reqUser: ReqUser,
@SerializedName("rights")
var rights: Rights,
@SerializedName("staff")
var staff: List<Staff>,
@SerializedName("stat")
var stat: Stat,
@SerializedName("state")
var state: Int, // 0
@SerializedName("tag")
var tag: List<Tag>,
@SerializedName("tid")
var tid: Int, // 96
@SerializedName("title")
var title: String, // 模拟流浪地球进入木星的洛希极限,太阳要膨胀吞没地球这事是真的吗?
@SerializedName("tname")
var tname: String, // 星海
@SerializedName("videos")
var videos: Int // 1
) {
data class Cm(
@SerializedName("ad_info")
var adInfo: JsonElement, // {}
@SerializedName("client_ip")
var clientIp: String, // 218.205.81.101
@SerializedName("index")
var index: Int, // 1
@SerializedName("is_ad_loc")
var isAdLoc: Boolean, // true
@SerializedName("request_id")
var requestId: String, // 1550675871470q172a22a56a79q738
@SerializedName("rsc_id")
var rscId: Int, // 2337
@SerializedName("src_id")
var srcId: Int // 2338
)
data class Owner(
@SerializedName("face")
var face: String, // http://i1.hdslb.com/bfs/face/9a586d1ef659b322af150c925976a134ad046a74.jpg
@SerializedName("mid")
var mid: Long, // 393484294
@SerializedName("name")
var name: String // 娱乐酱鸭
)
data class Elec(
@SerializedName("elec_set")
var elecSet: ElecSet,
@SerializedName("list")
var list: List<JsonElement>,
@SerializedName("show")
var show: Boolean // true
) {
data class ElecSet(
@SerializedName("elec_list")
var elecList: List<Elec>,
@SerializedName("elec_theme")
var elecTheme: Int, // 0
@SerializedName("integrity_rate")
var integrityRate: Double, // 10.0
@SerializedName("rmb_rate")
var rmbRate: Double, // 10.0
@SerializedName("round_mode")
var roundMode: Int // 0
) {
data class Elec(
@SerializedName("elec_num")
var elecNum: Int, // 0
@SerializedName("is_customize")
var isCustomize: Int, // 1
@SerializedName("max_elec")
var maxElec: Int, // 99999
@SerializedName("min_elec")
var minElec: Int, // 20
@SerializedName("title")
var title: String // 自定义
)
}
}
data class ReqUser(
@SerializedName("attention")
var attention: Int, // -999
@SerializedName("coin")
var coin: Int, // 0
@SerializedName("dislike")
var dislike: Int, // 0
@SerializedName("favorite")
var favorite: Int, // 0
@SerializedName("like")
var like: Int // 0
)
data class Stat(
@SerializedName("aid")
var aid: Int, // 44172743
@SerializedName("coin")
var coin: Int, // 23
@SerializedName("danmaku")
var danmaku: Int, // 19
@SerializedName("dislike")
var dislike: Int, // 0
@SerializedName("favorite")
var favorite: Int, // 10
@SerializedName("his_rank")
var hisRank: Int, // 0
@SerializedName("like")
var like: Int, // 35
@SerializedName("now_rank")
var nowRank: Int, // 0
@SerializedName("reply")
var reply: Int, // 11
@SerializedName("share")
var share: Int, // 0
@SerializedName("view")
var view: Int // 1995
)
data class OwnerExt(
@SerializedName("assists")
var assists: JsonElement?, // null
@SerializedName("fans")
var fans: Int, // 275
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("vip")
var vip: Vip
) {
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 0
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 0
)
}
data class Tag(
@SerializedName("attribute")
var attribute: Int, // 0
@SerializedName("cover")
var cover: String,
@SerializedName("hated")
var hated: Int, // 0
@SerializedName("hates")
var hates: Int, // 0
@SerializedName("is_activity")
var isActivity: Int, // 0
@SerializedName("liked")
var liked: Int, // 0
@SerializedName("likes")
var likes: Int, // 0
@SerializedName("tag_id")
var tagId: Int, // 7534
@SerializedName("tag_name")
var tagName: String // 未来
)
data class Staff(
@SerializedName("attention")
var attention: Int, // 0
@SerializedName("face")
var face: String, // http://i1.hdslb.com/bfs/face/9a586d1ef659b322af150c925976a134ad046a74.jpg
@SerializedName("mid")
var mid: Long, // 393484294
@SerializedName("name")
var name: String, // 娱乐酱鸭
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("title")
var title: String, // UP主
@SerializedName("vip")
var vip: Vip
) {
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 0
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 0
)
}
data class DislikeReason(
@SerializedName("reason_id")
var reasonId: Int, // 8
@SerializedName("reason_name")
var reasonName: String // 营销广告
)
data class Relate(
@SerializedName("ad_index")
var adIndex: Int, // 2
@SerializedName("aid")
var aid: Int, // 38496110
@SerializedName("card_index")
var cardIndex: Int, // 3
@SerializedName("cid")
var cid: Int, // 67669037
@SerializedName("client_ip")
var clientIp: String, // 218.205.81.101
@SerializedName("duration")
var duration: Int, // 189
@SerializedName("goto")
var goto: String, // av
@SerializedName("is_ad_loc")
var isAdLoc: Boolean, // true
@SerializedName("owner")
var owner: Owner,
@SerializedName("param")
var `param`: String, // 38496110
@SerializedName("pic")
var pic: String, // http://i2.hdslb.com/bfs/archive/ca80fb7c554e083716feb910370b77caa5e124b3.jpg
@SerializedName("request_id")
var requestId: String, // 1550675871470q172a22a56a79q738
@SerializedName("src_id")
var srcId: Int, // 2334
@SerializedName("stat")
var stat: Stat,
@SerializedName("title")
var title: String, // 《流浪地球》发布创想特辑,从无到有呈现刘慈欣科幻想象
@SerializedName("trackid")
var trackid: String, // related_0.shylf-ai-recsys-87.1550675871470.909
@SerializedName("uri")
var uri: String // bilibili://video/38496110?player_width=1920&player_height=1080&player_rotate=0&trackid=related_0.shylf-ai-recsys-87.1550675871470.909
) {
data class Owner(
@SerializedName("face")
var face: String, // http://static.hdslb.com/images/member/noface.gif
@SerializedName("mid")
var mid: Long, // 334512441
@SerializedName("name")
var name: String // 达岸电影2018
)
data class Stat(
@SerializedName("aid")
var aid: Int, // 38496110
@SerializedName("coin")
var coin: Int, // 130
@SerializedName("danmaku")
var danmaku: Int, // 152
@SerializedName("dislike")
var dislike: Int, // 0
@SerializedName("favorite")
var favorite: Int, // 194
@SerializedName("his_rank")
var hisRank: Int, // 0
@SerializedName("like")
var like: Int, // 341
@SerializedName("now_rank")
var nowRank: Int, // 0
@SerializedName("reply")
var reply: Int, // 264
@SerializedName("share")
var share: Int, // 278
@SerializedName("view")
var view: Int // 19397
)
}
data class Page(
@SerializedName("cid")
var cid: Int, // 77356986
@SerializedName("dimension")
var dimension: Dimension,
@SerializedName("dm")
var dm: Dm,
@SerializedName("dmlink")
var dmlink: String, // http://comment.bilibili.com/77356986.xml
@SerializedName("duration")
var duration: Int, // 444
@SerializedName("from")
var from: String, // vupload
@SerializedName("metas")
var metas: List<Meta>,
@SerializedName("page")
var page: Int, // 1
@SerializedName("part")
var part: String, // 2.20.2
@SerializedName("vid")
var vid: String,
@SerializedName("weblink")
var weblink: String
) {
data class Dm(
@SerializedName("closed")
var closed: Boolean, // false
@SerializedName("count")
var count: Int, // 19
@SerializedName("mask")
var mask: JsonElement, // {}
@SerializedName("real_name")
var realName: Boolean, // false
@SerializedName("subtitles")
var subtitles: JsonElement? // null
)
data class Dimension(
@SerializedName("height")
var height: Int, // 720
@SerializedName("rotate")
var rotate: Int, // 0
@SerializedName("width")
var width: Int // 1280
)
data class Meta(
@SerializedName("format")
var format: String,
@SerializedName("quality")
var quality: Int, // 48
@SerializedName("size")
var size: Int // 81074
)
}
data class Rights(
@SerializedName("autoplay")
var autoplay: Int, // 1
@SerializedName("bp")
var bp: Int, // 0
@SerializedName("download")
var download: Int, // 1
@SerializedName("elec")
var elec: Int, // 1
@SerializedName("hd5")
var hd5: Int, // 0
@SerializedName("is_cooperation")
var isCooperation: Int, // 0
@SerializedName("movie")
var movie: Int, // 0
@SerializedName("no_reprint")
var noReprint: Int, // 1
@SerializedName("pay")
var pay: Int, // 0
@SerializedName("ugc_pay")
var ugcPay: Int // 0
)
data class Dimension(
@SerializedName("height")
var height: Int, // 720
@SerializedName("rotate")
var rotate: Int, // 0
@SerializedName("width")
var width: Int // 1280
)
data class CmConfig(
@SerializedName("ads_control")
var adsControl: AdsControl
) {
data class AdsControl(
@SerializedName("has_danmu")
var hasDanmu: Int // 0
)
}
}
}

View File

@@ -1,66 +0,0 @@
package com.hiczp.bilibili.api.danmaku
import com.hiczp.crc32crack.Crc32Cracker
data class Danmaku(
/**
* 弹幕 id
*/
val id: Long,
/**
* TODO 下标 1, 不明属性
*/
val unknownAttribute1: String,
/**
* 弹幕出现时间(播放器时间)(ms)
*/
val time: Long,
/**
* 弹幕模式
* (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
*/
val mode: Int,
/**
* 字号
*/
val fontSize: Int,
/**
* 颜色
*/
val color: Int,
/**
* 弹幕的发送时间(时间戳)(s)
*/
val timestamp: Long,
/**
* TODO 下标 7, 不明属性
*/
val unknownAttribute7: String,
/**
* 弹幕发送者的 hash(用户 id 的 CRC32 校验和)
*/
val user: String,
/**
* 弹幕的内容
* 注意, 不一定是一个自然语言字符串, 可能是以 [ 开头的具有语义的文本, 如下所示
* [0,0,"1-1",4.5,"天下第一电击公主,贯穿天地的惊艳落雷!我炮傲娇永世长存!",0,0,0,0.99,500,0,1,"SimHei",true]
* 这可能表示某种特殊的输出格式
*/
val content: String
) {
/**
* 计算弹幕发送者 ID(可能有多个)
* 第一次调用 Crc32Cracker 将花费大约 300ms 来生成反查表
* hash 反查通常不超过 1ms
*/
fun calculatePossibleUserIds() = Crc32Cracker.crack(user)
}

View File

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

View File

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

View File

@@ -1,233 +0,0 @@
package com.hiczp.bilibili.api.live
import com.hiczp.bilibili.api.live.model.*
import com.hiczp.bilibili.api.retrofit.CommonResponse
import com.hiczp.bilibili.api.retrofit.Header
import kotlinx.coroutines.Deferred
import retrofit2.http.*
import kotlin.random.Random
/**
* 直播站 API
*/
@Suppress("DeferredIsResult")
interface LiveAPI {
/**
* 获取一个房间的基本信息
*
* @param id 房间号或房间短号
*/
@GET("/room/v1/Room/mobileRoomInit")
fun mobileRoomInit(@Query("id") id: Long): Deferred<MobileRoom>
/**
* 进入房间时客户端将访问该接口
* 访问该接口将在自己的账户中产生一条观看直播的历史记录
*
* @param roomId 房间号(没试过能不能用短号, 下同)
*/
@POST("/room/v1/Room/room_entry_action")
@FormUrlEncoded
fun roomEntryAction(
@Field("room_id") roomId: Long,
@Field("jumpFrom") jumpFrom: Int? = 0
): Deferred<CommonResponse>
/**
* 获取一个房间的详细信息
*
* @param id 房间号
*/
@GET("/room/v1/Room/get_info")
fun getInfo(@Query("id") id: Long): Deferred<RoomInfo>
/**
* 获取弹幕服务器
*
* @param roomId 房间号
*/
@GET("/room/v1/Danmu/getConf")
fun getDanmakuConfig(
@Query("room_id") roomId: Long
): Deferred<DanmakuConfig>
/**
* 获取该房间的主播的头像和等级一类的信息
*
* @param roomId 房间号
*/
@Suppress("SpellCheckingInspection")
@GET("/live_user/v1/UserInfo/get_anchor_in_room")
fun getAnchorInRoom(@Query("roomid") roomId: Long): Deferred<AnchorInRoom>
/**
* 获取自己在直播站的基本信息, 包括自己的直播间号, 银瓜子, 金瓜子数量等
*/
@GET("/mobile/getUser")
fun getUser(): Deferred<User>
/**
* 获取自己在当前直播间的信息, 包括自己的权限以及是否是管理员等
*
* @param roomId 房间号
*/
@Suppress("SpellCheckingInspection")
@GET("/live_user/v1/UserInfo/get_info_in_room")
fun getUserInfoInRoom(@Query("roomid") roomId: Long): Deferred<UserInfoInRoom>
/**
* 获取所有头衔
*
* @param scale 屏幕尺寸
*/
@GET("/appUser/getTitle")
fun getTitle(@Query("scale") scale: String = "xxhdpi"): Deferred<Title>
/**
* 查询是否关注了当前主播
*
* @param follow 所查询的主播的用户 ID
*/
@POST("/relation/v1/Feed/isFollowed")
@FormUrlEncoded
fun isFollowed(@Field("follow") follow: Long): Deferred<Follow>
/**
* 进入直播间的时候, 客户端会访问该接口来动态获取上方的 Tab. 包括 互动, 主播, 贡献榜 等
*
* @param roomId 房间号
*/
@Suppress("SpellCheckingInspection")
@GET("/room/v2/Room/mobileTab")
fun mobileTab(@Query("roomid") roomId: Long): Deferred<MobileTab>
/**
* 获取房间的历史弹幕(10条)
*
* @param roomId 房间号
*/
@GET("/AppRoom/msg")
fun roomMessage(@Query("room_id") roomId: Long): Deferred<RoomMessage>
/**
* 获取进房后右下角显示的那些东西, 通常是一些活动, 它们导向 H5 页面
*
* @param roomId 房间号
* @param roomUserId 主播的用户 ID
*/
@Suppress("SpellCheckingInspection")
@GET("/activity/v1/Common/mobileRoomBanner")
fun mobileRoomBanner(
@Query("area_v2_id") areaV2Id: Int,
@Query("area_v2_parent_id") areaV2ParentId: Int,
@Query("roomid") roomId: Long,
@Query("ruid") roomUserId: Long
): Deferred<MobileRoomBanner>
/**
* 获取各种礼物的基本信息, 包括贴图地址, 描述, 价格等
*/
@Suppress("SpellCheckingInspection")
@GET("/gift/v3/live/gift_config")
fun getGiftConfig(
@Query("area_v2_id") areaV2Id: Int,
@Query("area_v2_parent_id") areaV2ParentId: Int,
@Query("roomid") roomId: Long
): Deferred<GiftConfig>
/**
* 获取访问 小时总榜 的地址(H5)
*/
@Suppress("SpellCheckingInspection")
@GET("/rankdb/v1/Common/roomRank")
fun roomRank(
@Query("area_v2_id") areaV2Id: Int,
@Query("area_v2_parent_id") areaV2ParentId: Int,
@Query("roomid") roomId: Long,
@Query("ruid") roomUserId: Long
): Deferred<RoomRank>
/**
* 直播站首页
* 首页 -> 直播
*/
@Suppress("SpellCheckingInspection")
@GET("/xlive/app-interface/v2/index/getAllList")
fun homePage(
@Query("quality") quality: Int = 0,
@Query("rec_page") recPage: Int = 2,
@Query("relation_page") relationPage: Int = 2,
@Query("scale") scale: String = "xxhdpi"
): Deferred<HomePage>
/**
* 获取某个直播分类下的全部子分类
*/
@GET("/room/v1/Area/getList")
fun getAreaList(@Query("parent_id") parentId: Int): Deferred<AreaList>
/**
* 根据某种维度来获取房间列表
* area, parent, category 为 0 表示不筛选这些维度
* sortType 为 null 表示不排序
*
* 首页 -> 直播 -> 查看更多/全部直播
*
* @param page 分页, 从 1 开始
* @param sortType 排序维度, 已知的有 online(最热直播), live_time(最新开播)
*/
@GET("/room/v3/Area/getRoomList")
fun getRoomList(
@Query("area_id") areaId: Int = 0,
@Query("parent_area_id") parentAreaId: Int = 0,
@Query("cate_id") categoryId: Int = 0,
@Query("page") page: Int = 1,
@Query("page_size") pageSize: Int = 30,
@Query("sort_type") sortType: String? = null
): Deferred<RoomList>
/**
* 发送弹幕(直播)
*
* @param bubble 气泡, 不明确含义
* @param cid 房间号
* @param mid 发送者的用户 ID
* @param message 弹幕内容
* @param random 随机数, 不包括符号位有 9 位 或者 10 位
* @param mode 弹幕模式, 可能与视频弹幕的模式含义相同, 可能需要特殊身份才能使用额外模式, 下同
* @param pool 弹幕池
* @param type 固定为 "json"
* @param color 弹幕颜色
* @param fontSize 弹幕字号
* @param playTime 不明确
*/
@Suppress("SpellCheckingInspection")
@POST("/api/sendmsg")
@FormUrlEncoded
@Headers(Header.FORCE_QUERY)
fun sendMessage(
@Field("bubble") bubble: Int = 0,
@Field("cid") cid: Long,
@Field("mid") mid: Long,
@Field("msg") message: String,
@Field("rnd") random: Int = (if (Random.nextBoolean()) 1 else -1) * Random.nextInt(100000000, Int.MAX_VALUE),
@Field("mode") mode: Int = 1,
@Field("pool") pool: Int = 0,
@Field("type") type: String = "json",
@Field("color") color: Int = 16777215,
@Field("fontsize") fontSize: Int = 25,
@Field("playTime") playTime: Float = 0.0f
): Deferred<CommonResponse>
/**
* 用于确认客户端在看直播的心跳包(与弹幕推送无关)
* 每五分钟发送一次
*/
@POST("/mobile/userOnlineHeart")
@FormUrlEncoded
@Headers(Header.FORCE_QUERY)
fun userOnlineHeart(
@Field("room_id") roomId: Long,
@Field("scale") scale: String = "xxhdpi"
): Deferred<CommonResponse>
}

View File

@@ -1,103 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class AnchorInRoom(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("info")
var info: Info,
@SerializedName("level")
var level: Level,
@SerializedName("san")
var san: Int // 12
) {
data class Info(
@SerializedName("face")
var face: String, // https://i0.hdslb.com/bfs/face/0434dccc0ec4de223e8ca374dea06a6e1e8eb471.jpg
@SerializedName("gender")
var gender: Int, // 0
@SerializedName("identification")
var identification: Int?, // 1
@SerializedName("mobile_verify")
var mobileVerify: Int, // 0
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("platform_user_level")
var platformUserLevel: Int, // 5
@SerializedName("rank")
var rank: String, // 10000
@SerializedName("uid")
var uid: Long, // 2866663
@SerializedName("uname")
var uname: String, // hyx5020
@SerializedName("vip_type")
var vipType: Int // 2
) {
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("role")
var role: Int, // 0
@SerializedName("type")
var type: Int // -1
)
}
data class Level(
@SerializedName("anchor_score")
var anchorScore: Int, // 36685
@SerializedName("color")
var color: Int, // 5805790
@SerializedName("cost")
var cost: Int, // 29431298
@SerializedName("master_level")
var masterLevel: MasterLevel,
@SerializedName("rcost")
var rcost: Long, // 3668592
@SerializedName("svip")
var svip: Int, // 0
@SerializedName("svip_time")
var svipTime: String, // 2019-02-09 11:03:54
@SerializedName("uid")
var uid: Int, // 2866663
@SerializedName("update_time")
var updateTime: String, // 2019-03-12 23:00:42
@SerializedName("user_level")
var userLevel: Int, // 22
@SerializedName("user_score")
var userScore: String, // 0
@SerializedName("vip")
var vip: Int, // 0
@SerializedName("vip_time")
var vipTime: String // 2019-02-09 11:03:54
) {
data class MasterLevel(
@SerializedName("anchor_score")
var anchorScore: Int, // 36685
@SerializedName("color")
var color: Int, // 5805790
@SerializedName("current")
var current: List<Int>,
@SerializedName("level")
var level: Int, // 11
@SerializedName("master_level_color")
var masterLevelColor: Int, // 5805790
@SerializedName("next")
var next: List<Int>,
@SerializedName("sort")
var sort: String, // >10000
@SerializedName("upgrade_score")
var upgradeScore: Int // 2925
)
}
}
}

View File

@@ -1,39 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class AreaList(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Data>,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("act_id")
var actId: String, // 0
@SerializedName("area_type")
var areaType: Int, // 0
@SerializedName("hot_status")
var hotStatus: Int, // 0
@SerializedName("id")
var id: String, // 34
@SerializedName("lock_status")
var lockStatus: String, // 0
@SerializedName("name")
var name: String, // 音乐台
@SerializedName("old_area_id")
var oldAreaId: String, // 7
@SerializedName("parent_id")
var parentId: String, // 1
@SerializedName("parent_name")
var parentName: String, // 娱乐
@SerializedName("pic")
var pic: String, // https://i0.hdslb.com/bfs/vc/8537694f4fe68ab0798dd5d493d3ca5deb908088.png
@SerializedName("pk_status")
var pkStatus: String // 0
)
}

View File

@@ -1,67 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class DanmakuConfig(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // ok
@SerializedName("msg")
var msg: String // ok
) {
data class Data(
/**
* 推荐服务器
*/
@SerializedName("host")
var host: String, // broadcastlv.chat.bilibili.com
/**
* 服务器列表
*/
@SerializedName("host_server_list")
var hostServerList: List<HostServer>,
@SerializedName("max_delay")
var maxDelay: Int, // 5000
/**
* 这里的端口是非 TSL 的 websocket 端口, 需要到 host_server_list 中寻找推荐服务器对应的 websocket TSL 端口(通常是 443)
*/
@SerializedName("port")
var port: Int, // 2243
@SerializedName("refresh_rate")
var refreshRate: Int, // 100
@SerializedName("refresh_row_factor")
var refreshRowFactor: Double, // 0.125
/**
* 如果 DNS 失效可以使用该列表中的 IP
*/
@SerializedName("server_list")
var serverList: List<Server>
) {
data class Server(
@SerializedName("host")
var host: String, // broadcastlv.chat.bilibili.com
@SerializedName("port")
var port: Int // 80
)
data class HostServer(
@SerializedName("host")
var host: String, // broadcastlv.chat.bilibili.com
@SerializedName("port")
var port: Int, // 2243
/**
* websocket 端口
*/
@SerializedName("ws_port")
var wsPort: Int, // 2244
/**
* websocket TSL 端口
*/
@SerializedName("wss_port")
var wssPort: Int // 443
)
}
}

View File

@@ -1,19 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class Follow(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("follow")
var follow: Int // 0
)
}

View File

@@ -1,88 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class GiftConfig(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Data>,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("animation_frame_num")
var animationFrameNum: Int, // 12
@SerializedName("bag_gift")
var bagGift: Int, // 0
@SerializedName("broadcast")
var broadcast: Int, // 0
@SerializedName("bullet_head")
var bulletHead: String,
@SerializedName("bullet_tail")
var bulletTail: String,
@SerializedName("coin_type")
var coinType: String, // gold
@SerializedName("corner_background")
var cornerBackground: String,
@SerializedName("corner_mark")
var cornerMark: String, // 祈愿
@SerializedName("count_map")
var countMap: List<CountMap>,
@SerializedName("desc")
var desc: String, // 祥瑞御免,家宅平安。
@SerializedName("draw")
var draw: Int, // 0
@SerializedName("effect")
var effect: Int, // 0
@SerializedName("frame_animation")
var frameAnimation: String, // https://i0.hdslb.com/bfs/live/4e19f947d0bd346d38fe4838d7ab431d003f9d7f.png
@SerializedName("full_sc_horizontal")
var fullScHorizontal: String,
@SerializedName("full_sc_horizontal_svga")
var fullScHorizontalSvga: String,
@SerializedName("full_sc_vertical")
var fullScVertical: String,
@SerializedName("full_sc_vertical_svga")
var fullScVerticalSvga: String,
@SerializedName("full_sc_web")
var fullScWeb: String,
@SerializedName("gif")
var gif: String, // https://i0.hdslb.com/bfs/live/a90ff57209661b309f121116682bdee1f3937a98.gif
@SerializedName("gift_type")
var giftType: Int, // 1
@SerializedName("id")
var id: Int, // 30136
@SerializedName("img_basic")
var imgBasic: String, // https://s1.hdslb.com/bfs/live/a7c750335ed42ae4dfeb70570804326d3ecaf61c.png
@SerializedName("img_dynamic")
var imgDynamic: String, // https://i0.hdslb.com/bfs/live/f6f314227e7ed8065c4f7266ada913289971f806.png
@SerializedName("limit_interval")
var limitInterval: Int, // 0
@SerializedName("name")
var name: String, // 御守
@SerializedName("price")
var price: Int, // 1000
@SerializedName("privilege_required")
var privilegeRequired: Int, // 0
@SerializedName("rights")
var rights: String, // 当前主播亲密度+10 经验值+1000
@SerializedName("rule")
var rule: String, // 赠送御守即可参与“敬祈照准”活动。
@SerializedName("stay_time")
var stayTime: Int, // 3
@SerializedName("type")
var type: Int, // 0
@SerializedName("webp")
var webp: String // https://i0.hdslb.com/bfs/live/6d8a7907cf89556d074b8ce220e7dd56ccaf5160.webp
) {
data class CountMap(
@SerializedName("num")
var num: Int, // 2333
@SerializedName("text")
var text: String
)
}
}

View File

@@ -1,393 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class HomePage(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("activity_card")
var activityCard: List<JsonElement>,
@SerializedName("area_entrance")
var areaEntrance: List<AreaEntrance>,
@SerializedName("area_entrance_v2")
var areaEntranceV2: List<JsonElement>,
@SerializedName("banner")
var banner: List<Banner>,
@SerializedName("hour_rank")
var hourRank: List<HourRank>,
@SerializedName("interval")
var interval: Int, // 10
@SerializedName("is_sky_horse_gray")
var isSkyHorseGray: Int, // 0
@SerializedName("my_idol")
var myIdol: List<MyIdol>,
@SerializedName("my_tag")
var myTag: List<MyTag>,
@SerializedName("room_list")
var roomList: List<Room>,
@SerializedName("sea_patrol")
var seaPatrol: List<JsonElement>
) {
data class MyTag(
@SerializedName("extra_info")
var extraInfo: ExtraInfo,
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class X(
@SerializedName("area_v2_id")
var areaV2Id: Int, // 0
@SerializedName("area_v2_name")
var areaV2Name: String, // 全部标签
@SerializedName("area_v2_parent_id")
var areaV2ParentId: Int, // 0
@SerializedName("area_v2_parent_name")
var areaV2ParentName: String,
@SerializedName("is_advice")
var isAdvice: Int, // 1
@SerializedName("link")
var link: String, // http://live.bilibili.com/app/mytag/
@SerializedName("pic")
var pic: String // http://i0.hdslb.com/bfs/vc/ff03528785fc8c91491d79e440398484811d6d87.png
)
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 28
@SerializedName("link")
var link: String,
@SerializedName("pic")
var pic: String,
@SerializedName("sort")
var sort: Int, // 1
@SerializedName("title")
var title: String, // 常用标签
@SerializedName("type")
var type: Int // 12
)
data class ExtraInfo(
@SerializedName("is_gray")
var isGray: Int, // 0
@SerializedName("offline")
var offline: List<JsonElement>
)
}
data class Room(
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class X(
@SerializedName("accept_quality")
var acceptQuality: List<Int>,
@SerializedName("area_v2_id")
var areaV2Id: Int, // 96
@SerializedName("area_v2_name")
var areaV2Name: String, // 其他绘画
@SerializedName("area_v2_parent_id")
var areaV2ParentId: Int, // 4
@SerializedName("area_v2_parent_name")
var areaV2ParentName: String, // 绘画
@SerializedName("broadcast_type")
var broadcastType: Int, // 0
@SerializedName("click_callback")
var clickCallback: String,
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/live/room_cover/6ef5b522bdf4de9fe2535b6031ae9b32c405cea0.jpg
@SerializedName("current_quality")
var currentQuality: Int, // 4
@SerializedName("face")
var face: String, // http://i0.hdslb.com/bfs/face/c165088330c1bd7f671b427b610379603aa002ae.jpg
@SerializedName("group_id")
var groupId: Int, // 0
@SerializedName("link")
var link: String,
@SerializedName("online")
var online: Int, // 5584
@SerializedName("pendent_ru")
var pendentRu: String,
@SerializedName("pendent_ru_color")
var pendentRuColor: String,
@SerializedName("pendent_ru_pic")
var pendentRuPic: String,
@SerializedName("pk_id")
var pkId: Int, // 0
@SerializedName("play_url")
var playUrl: String, // http://ws.live-play.acgvideo.com/live-ws/403834/live_397298321_43558493.flv?wsSecret=9dc8725a77c8ef5c68545e436f53b917&wsTime=1552470101&trid=9179fec58e79438aab34a9bbe5087e33&sig=no
@SerializedName("play_url_h265")
var playUrlH265: String,
@SerializedName("rec_type")
var recType: Int, // 0
@SerializedName("roomid")
var roomid: Long, // 21218600
@SerializedName("session_id")
var sessionId: String, // 456BB3BF-16D6-4BD3-9B4E-4570C274CEE5
@SerializedName("show_callback")
var showCallback: String,
@SerializedName("title")
var title: String, // 学员答疑1300~2100
@SerializedName("uname")
var uname: String // 轻微课魔鬼绘画特训班
)
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 8
@SerializedName("link")
var link: String, // http://live.bilibili.com/app/area?parent_area_id=4&parent_area_name=绘画&area_id=&area_name=
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/live/7c54d7cc64e022845fccd63221de069b71eb6f67.png
@SerializedName("sort")
var sort: Int, // 25
@SerializedName("title")
var title: String, // 绘画
@SerializedName("type")
var type: Int // 9
)
}
data class AreaEntrance(
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 2
@SerializedName("link")
var link: String,
@SerializedName("pic")
var pic: String,
@SerializedName("sort")
var sort: Int, // 2
@SerializedName("title")
var title: String, // 分区入口
@SerializedName("type")
var type: Int // 2
)
data class X(
@SerializedName("content")
var content: String,
@SerializedName("id")
var id: Int, // 45
@SerializedName("link")
var link: String, // http://live.bilibili.com/app/area?parent_area_id=1&parent_area_name=娱乐&area_id=199&area_name=虚拟主播
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/vc/7725a45469b776ee91f2d42afca1e5711f84ac51.png
@SerializedName("title")
var title: String // 虚拟主播
)
}
data class Banner(
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class X(
@SerializedName("content")
var content: String,
@SerializedName("id")
var id: Int, // 1117
@SerializedName("link")
var link: String, // https://www.bilibili.com/blackboard/live/activity-flower-girl2-h5.html
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/vc/523a719b51a647eeb969956865a20781d7e6d994.jpg
@SerializedName("title")
var title: String // 花之初少女
)
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 1
@SerializedName("link")
var link: String,
@SerializedName("pic")
var pic: String,
@SerializedName("sort")
var sort: Int, // 0
@SerializedName("title")
var title: String, // banner位
@SerializedName("type")
var type: Int // 1
)
}
data class MyIdol(
@SerializedName("extra_info")
var extraInfo: ExtraInfo,
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class ExtraInfo(
@SerializedName("card_type")
var cardType: Int, // 1
@SerializedName("relation_page")
var relationPage: Int, // 1
@SerializedName("tags_desc")
var tagsDesc: String,
@SerializedName("time_desc")
var timeDesc: String,
@SerializedName("total_count")
var totalCount: Int, // 1
@SerializedName("uname_desc")
var unameDesc: String
)
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 13
@SerializedName("link")
var link: String, // http://live.bilibili.com/app/myfollow/
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/live/484abcd8940ee43ec8b4409cbfe0c1e52f09a338.png
@SerializedName("sort")
var sort: Int, // 4
@SerializedName("title")
var title: String, // 我的关注
@SerializedName("type")
var type: Int // 8
)
data class X(
@SerializedName("accept_quality")
var acceptQuality: List<Int>,
@SerializedName("area")
var area: Int, // 7
@SerializedName("area_name")
var areaName: String, // 放映厅
@SerializedName("area_v2_id")
var areaV2Id: Int, // 34
@SerializedName("area_v2_name")
var areaV2Name: String, // 音乐台
@SerializedName("area_v2_parent_id")
var areaV2ParentId: Int, // 1
@SerializedName("area_v2_parent_name")
var areaV2ParentName: String, // 娱乐
@SerializedName("broadcast_type")
var broadcastType: Int, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/live/6029764557e3cbe91475faae26e6e244de8c1d3c.jpg
@SerializedName("current_quality")
var currentQuality: Int, // 4
@SerializedName("face")
var face: String, // http://i0.hdslb.com/bfs/face/5d35da6e93fbfb1a77ad6d1f1004b08413913f9a.jpg
@SerializedName("link")
var link: String, // http://live.bilibili.com/23058?broadcast_type=0
@SerializedName("live_tag_name")
var liveTagName: String, // 音乐台
@SerializedName("live_time")
var liveTime: Int, // 1552406400
@SerializedName("official_verify")
var officialVerify: Int, // 1
@SerializedName("online")
var online: Int, // 9961
@SerializedName("pendent_ru")
var pendentRu: String,
@SerializedName("pendent_ru_color")
var pendentRuColor: String,
@SerializedName("pendent_ru_pic")
var pendentRuPic: String,
@SerializedName("pk_id")
var pkId: Int, // 0
@SerializedName("play_url")
var playUrl: String, // http://ws.live-play.acgvideo.com/live-ws/637609/live_11153765_9369560.flv?wsSecret=49e118106b827b5008e10b0c74fa1a5a&wsTime=1552470101&trid=fe3b3a0b017a439c86d792ab5dd6fcd5&sig=no
@SerializedName("play_url_h265")
var playUrlH265: String,
@SerializedName("roomid")
var roomid: Long, // 23058
@SerializedName("special_attention")
var specialAttention: Int, // 0
@SerializedName("title")
var title: String, // 哔哩哔哩音悦台
@SerializedName("uid")
var uid: Long, // 11153765
@SerializedName("uname")
var uname: String // 3号直播间
)
}
data class HourRank(
@SerializedName("extra_info")
var extraInfo: ExtraInfo,
@SerializedName("list")
var list: List<X>,
@SerializedName("module_info")
var moduleInfo: ModuleInfo
) {
data class ModuleInfo(
@SerializedName("count")
var count: Int, // 0
@SerializedName("id")
var id: Int, // 4
@SerializedName("link")
var link: String, // https://live.bilibili.com/p/html/live-app-rank/index.html?is_live_webview=1&nav=hour
@SerializedName("pic")
var pic: String, // http://i0.hdslb.com/bfs/live/39cd413f6bc72fb9da8c10ff2686b537477294ab.png
@SerializedName("sort")
var sort: Int, // 11
@SerializedName("title")
var title: String, // 小时榜
@SerializedName("type")
var type: Int // 5
)
data class X(
@SerializedName("area_v2_id")
var areaV2Id: Int, // 145
@SerializedName("area_v2_name")
var areaV2Name: String, // 视频聊天
@SerializedName("area_v2_parent_id")
var areaV2ParentId: Int, // 1
@SerializedName("area_v2_parent_name")
var areaV2ParentName: String, // 娱乐
@SerializedName("face")
var face: String, // http://i2.hdslb.com/bfs/face/cdc9866d09ed82e6fae610f5ba4b8706db509802.jpg
@SerializedName("live_status")
var liveStatus: Int, // 1
@SerializedName("rank")
var rank: Int, // 3
@SerializedName("roomid")
var roomid: Long, // 274926
@SerializedName("uid")
var uid: Long, // 24601383
@SerializedName("uname")
var uname: String // 蛋黄姬GAT-X105
)
data class ExtraInfo(
@SerializedName("sub_title")
var subTitle: String // 15:00-16:00 总榜排名
)
}
}
}

View File

@@ -1,58 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class MobileRoom(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // ok
@SerializedName("msg")
var msg: String // ok
) {
data class Data(
@SerializedName("encrypted")
var encrypted: Boolean, // false
@SerializedName("hidden_till")
var hiddenTill: Int, // 0
@SerializedName("is_hidden")
var isHidden: Boolean, // false
@SerializedName("is_locked")
var isLocked: Boolean, // false
@SerializedName("is_portrait")
var isPortrait: Boolean, // false
@SerializedName("is_sp")
var isSp: Int, // 0
@SerializedName("live_status")
var liveStatus: Int, // 0
@SerializedName("live_time")
var liveTime: Long, // -62170012800
@SerializedName("lock_till")
var lockTill: Int, // 0
@SerializedName("need_p2p")
var needP2p: Int, // 0
@SerializedName("pwd_verified")
var pwdVerified: Boolean, // true
/**
* 实际房间号
*/
@SerializedName("room_id")
var roomId: Long, // 1110317
/**
* 如果房间有短号则 roomShield 为 1
*/
@SerializedName("room_shield")
var roomShield: Int, // 0
/**
* 短房间号
*/
@SerializedName("short_id")
var shortId: Int, // 0
@SerializedName("special_type")
var specialType: Int, // 0
@SerializedName("uid")
var uid: Long // 20293030
)
}

View File

@@ -1,74 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class MobileRoomBanner(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // OK
@SerializedName("msg")
var msg: String // OK
) {
data class Data(
@SerializedName("bottom")
var bottom: List<JsonElement>,
@SerializedName("gift")
var gift: JsonElement?, // null
@SerializedName("gift_banner")
var giftBanner: JsonElement?, // null
@SerializedName("inputBanner")
var inputBanner: List<JsonElement>,
@SerializedName("lol_activity")
var lolActivity: LolActivity,
@SerializedName("superBanner")
var superBanner: JsonElement?, // null
@SerializedName("top")
var top: List<Top>
) {
data class Top(
@SerializedName("activity_title")
var activityTitle: String, // 周星
@SerializedName("color")
var color: String,
@SerializedName("cover")
var cover: String, // https://i0.hdslb.com/bfs/vc/5cfb2a7dc2a25db580f130a55f475f74e2bd3202.png
@SerializedName("expire_hour")
var expireHour: Int, // 24
@SerializedName("gift_img")
var giftImg: String, // https://s1.hdslb.com/bfs/vc/39aee4bf13b170f22f19ef1c278cebf3a6e40332.png
@SerializedName("id")
var id: Int, // 199
@SerializedName("is_close")
var isClose: Int, // 0
@SerializedName("jump_url")
var jumpUrl: String, // https://live.bilibili.com/p/html/live-app-weekstar/index.html?is_live_half_webview=1&hybrid_biz=live-app-weekStar&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,300e51,0,30,100;2,2,375,100p,300e51,0,30,100;3,3,100p,70p,300e51,0,30,100;4,2,375,100p,300e51,0,30,100;5,3,100p,70p,300e51,0,30,100;6,3,100p,70p,300e51,0,30,100;7,3,100p,70p,300e51,0,30,100&room_id=29434
@SerializedName("rank")
var rank: String, // 999+
@SerializedName("rank_name")
var rankName: String, // 打榜
@SerializedName("title")
var title: String, // 排名
@SerializedName("type")
var type: Int, // 1
@SerializedName("week_gift_color")
var weekGiftColor: String, // #ffffff
@SerializedName("week_rank_color")
var weekRankColor: String, // #ffffff
@SerializedName("week_text_color")
var weekTextColor: String // #ffffff
)
data class LolActivity(
@SerializedName("guess_cover")
var guessCover: String, // https://i0.hdslb.com/bfs/live/61d1c4bcce470080a5408d6c03b7b48e0a0fa8d7.png
@SerializedName("status")
var status: Int, // 0
@SerializedName("vote_cover")
var voteCover: String // https://i0.hdslb.com/bfs/live/6030cb2847f4d197caacb12fbe12f2656b999bcf.png
)
}
}

View File

@@ -1,36 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class MobileTab(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Tab>,
@SerializedName("message")
var message: String,
@SerializedName("msg")
var msg: String
) {
data class Tab(
/**
* 如果是非顶层 Tab 则 default 为 null
*/
@SerializedName("default")
var default: Int?, // 0
@SerializedName("default_sub_tab")
var defaultSubTab: String,
@SerializedName("desc")
var desc: String, // 友爱社
@SerializedName("order")
var order: Int, // 600
@SerializedName("status")
var status: Int, // 0
@SerializedName("sub_tab")
var subTab: List<Tab>,
@SerializedName("type")
var type: String, // love-club
@SerializedName("url")
var url: String
)
}

View File

@@ -1,105 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class RoomInfo(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // ok
@SerializedName("msg")
var msg: String // ok
) {
data class Data(
@SerializedName("allow_change_area_time")
var allowChangeAreaTime: Int, // 0
@SerializedName("allow_upload_cover_time")
var allowUploadCoverTime: Int, // 0
/**
* 没有 old 前缀的表示 v2 版本, 例如 area_v2_id. 下同
*/
@SerializedName("area_id")
var areaId: Int, // 107
@SerializedName("area_name")
var areaName: String, // 其他游戏
@SerializedName("area_pendants")
var areaPendants: String,
/**
* 粉丝数
*/
@SerializedName("attention")
var attention: Int, // 62
@SerializedName("background")
var background: String,
@SerializedName("description")
var description: String, // <p>立即安装Jetbrains, 享受精彩人生!</p>
@SerializedName("hot_words")
var hotWords: List<String>,
@SerializedName("hot_words_status")
var hotWordsStatus: Int, // 0
@SerializedName("is_anchor")
var isAnchor: Int, // 1
@SerializedName("is_portrait")
var isPortrait: Boolean, // false
@SerializedName("is_strict_room")
var isStrictRoom: Boolean, // false
@SerializedName("keyframe")
var keyframe: String, // https://i0.hdslb.com/bfs/live/1110317.jpg?03092037
@SerializedName("live_status")
var liveStatus: Int, // 0
@SerializedName("live_time")
var liveTime: String, // 0000-00-00 00:00:00
@SerializedName("new_pendants")
var newPendants: NewPendants,
@SerializedName("old_area_id")
var oldAreaId: Int, // 1
@SerializedName("online")
var online: Int, // 18
@SerializedName("parent_area_id")
var parentAreaId: Int, // 2
@SerializedName("parent_area_name")
var parentAreaName: String, // 网游
@SerializedName("pendants")
var pendants: String,
@SerializedName("pk_id")
var pkId: Int, // 0
@SerializedName("pk_status")
var pkStatus: Int, // 0
@SerializedName("room_id")
var roomId: Long, // 1110317
@SerializedName("room_silent_level")
var roomSilentLevel: Int, // 0
@SerializedName("room_silent_second")
var roomSilentSecond: Int, // 0
@SerializedName("room_silent_type")
var roomSilentType: String,
@SerializedName("short_id")
var shortId: Int, // 0
@SerializedName("tags")
var tags: String, // 编程
@SerializedName("title")
var title: String, // 太空程序员
@SerializedName("uid")
var uid: Long, // 20293030
@SerializedName("up_session")
var upSession: String,
@SerializedName("user_cover")
var userCover: String, // https://i0.hdslb.com/bfs/live/b4d4dbf35f7a30fb6b0a2ea4077514235262797e.jpg
@SerializedName("verify")
var verify: String
) {
data class NewPendants(
@SerializedName("badge")
var badge: JsonElement?, // null
@SerializedName("frame")
var frame: JsonElement?, // null
@SerializedName("mobile_badge")
var mobileBadge: JsonElement?, // null
@SerializedName("mobile_frame")
var mobileFrame: JsonElement? // null
)
}
}

View File

@@ -1,136 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class RoomList(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("banner")
var banner: List<Banner>,
@SerializedName("count")
var count: Int, // 5116
@SerializedName("list")
var list: List<X>,
@SerializedName("tags")
var tags: List<Tag>
) {
data class Banner(
@SerializedName("id")
var id: String, // 1117
@SerializedName("link")
var link: String, // https://www.bilibili.com/blackboard/live/activity-flower-girl2-h5.html
@SerializedName("pic")
var pic: String, // https://i0.hdslb.com/bfs/vc/523a719b51a647eeb969956865a20781d7e6d994.jpg
@SerializedName("position")
var position: String, // 5
@SerializedName("sort_num")
var sortNum: String, // 1
@SerializedName("title")
var title: String // 花之初少女
)
data class X(
@SerializedName("accept_quality")
var acceptQuality: String, // 4
@SerializedName("accept_quality_v2")
var acceptQualityV2: List<JsonElement>,
@SerializedName("area_id")
var areaId: Int, // 107
@SerializedName("area_name")
var areaName: String, // 其他游戏
@SerializedName("area_v2_id")
var areaV2Id: Int, // 107
@SerializedName("area_v2_name")
var areaV2Name: String, // 其他游戏
@SerializedName("area_v2_parent_id")
var areaV2ParentId: Int, // 2
@SerializedName("area_v2_parent_name")
var areaV2ParentName: String, // 网游
@SerializedName("broadcast_type")
var broadcastType: Int, // 0
@SerializedName("corner")
var corner: String,
@SerializedName("cover_size")
var coverSize: CoverSize,
@SerializedName("current_quality")
var currentQuality: Int, // 4
@SerializedName("face")
var face: String, // https://i0.hdslb.com/bfs/face/9c9ad7d21784e70dfa57cdae40cfdca1b58424c4.jpg
@SerializedName("game_live_num")
var gameLiveNum: Int, // 30
@SerializedName("group_id")
var groupId: Int, // 0
@SerializedName("is_tv")
var isTv: Int, // 0
@SerializedName("link")
var link: String, // /21142258
@SerializedName("online")
var online: Int, // 0
@SerializedName("parent_id")
var parentId: Int, // 2
@SerializedName("parent_name")
var parentName: String, // 网游
@SerializedName("pendent")
var pendent: String,
@SerializedName("pendent_ld")
var pendentLd: String,
@SerializedName("pendent_ld_color")
var pendentLdColor: String,
@SerializedName("pendent_ru")
var pendentRu: String,
@SerializedName("pendent_ru_color")
var pendentRuColor: String,
@SerializedName("pendent_ru_pic")
var pendentRuPic: String,
@SerializedName("pk_id")
var pkId: Int, // 0
@SerializedName("play_url")
var playUrl: String,
@SerializedName("roomid")
var roomid: Long, // 21142258
@SerializedName("session_id")
var sessionId: String, // E9168524-EA1A-9F86-7A88-E33B58B9C8A8
@SerializedName("show_cover")
var showCover: String,
@SerializedName("system_cover")
var systemCover: String, // https://i0.hdslb.com/bfs/live/21142258.jpg?03131614
@SerializedName("title")
var title: String, // DNF除了肝啥都没
@SerializedName("uid")
var uid: Long, // 278287794
@SerializedName("uname")
var uname: String, // wz85699909
@SerializedName("user_cover")
var userCover: String, // https://i0.hdslb.com/bfs/live/room_cover/dbfd1bbb5f936620a7bccac4b5e51b54342284d1.jpg
@SerializedName("user_cover_flag")
var userCoverFlag: Int, // 1
@SerializedName("web_pendent")
var webPendent: String
) {
data class CoverSize(
@SerializedName("height")
var height: Int, // 180
@SerializedName("width")
var width: Int // 320
)
}
data class Tag(
@SerializedName("id")
var id: Int, // -2
@SerializedName("name")
var name: String, // 最新
@SerializedName("sort_type")
var sortType: String // live_time
)
}
}

View File

@@ -1,116 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class RoomMessage(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String,
@SerializedName("msg")
var msg: String
) {
data class Data(
@SerializedName("admin")
var admin: List<Admin>,
@SerializedName("room")
var room: List<Message>
) {
data class Message(
@SerializedName("bubble")
var bubble: Int, // 0
@SerializedName("check_info")
var checkInfo: CheckInfo,
@SerializedName("guard_level")
var guardLevel: Int, // 0
@SerializedName("isadmin")
var isadmin: Int, // 0
@SerializedName("medal")
var medal: List<String>, //[ 17, "毛菇","猫菇椰汁",923614,16752445,""]
@SerializedName("nickname")
var nickname: String, // 柠檬味狗凉゜
@SerializedName("rank")
var rank: Int, // 10000
@SerializedName("rnd")
var rnd: String, // 1552452731
@SerializedName("svip")
var svip: Int, // 1
@SerializedName("teamid")
var teamid: Int, // 0
@SerializedName("text")
var text: String, // 当前有效总督房:14979272;146
@SerializedName("timeline")
var timeline: String, // 2019-03-13 12:52:09
@SerializedName("title")
var title: List<String>,
@SerializedName("uid")
var uid: Int, // 1615204
@SerializedName("uname_color")
var unameColor: String,
/**
* 有可能是 >50000 这样的东西, 下同
*/
@SerializedName("user_level")
var userLevel: List<String>,
@SerializedName("user_title")
var userTitle: String,
@SerializedName("vip")
var vip: Int // 1
) {
data class CheckInfo(
@SerializedName("ct")
var ct: String, // 4B9A2E41
@SerializedName("ts")
var ts: Int // 1552452729
)
}
data class Admin(
@SerializedName("bubble")
var bubble: Int, // 2
@SerializedName("check_info")
var checkInfo: CheckInfo,
@SerializedName("guard_level")
var guardLevel: Int, // 2
@SerializedName("isadmin")
var isadmin: Int, // 1
@SerializedName("medal")
var medal: List<String>,
@SerializedName("nickname")
var nickname: String, // 沧澜ベ
@SerializedName("rank")
var rank: Int, // 10000
@SerializedName("rnd")
var rnd: String, // 350761541
@SerializedName("svip")
var svip: Int, // 1
@SerializedName("teamid")
var teamid: Int, // 0
@SerializedName("text")
var text: String, // 下个号见
@SerializedName("timeline")
var timeline: String, // 2019-03-13 12:54:42
@SerializedName("title")
var title: List<String>,
@SerializedName("uid")
var uid: Long, // 11626554
@SerializedName("uname_color")
var unameColor: String, // #e91e63
@SerializedName("user_level")
var userLevel: List<String>,
@SerializedName("user_title")
var userTitle: String, // title-181-1
@SerializedName("vip")
var vip: Int // 1
) {
data class CheckInfo(
@SerializedName("ct")
var ct: String, // D1012BA7
@SerializedName("ts")
var ts: Long // 1552452882
)
}
}
}

View File

@@ -1,29 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class RoomRank(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // OK
@SerializedName("msg")
var msg: String // OK
) {
data class Data(
@SerializedName("color")
var color: String, // #FB7299
@SerializedName("h5_url")
var h5Url: String, // 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=2866663&rank_type=master_realtime_hour_room&area_hour=1&area_v2_id=145&area_v2_parent_id=1
@SerializedName("rank_desc")
var rankDesc: String, // 小时总榜
@SerializedName("roomid")
var roomid: Long, // 29434
@SerializedName("timestamp")
var timestamp: Long, // 1552451099
@SerializedName("web_url")
var webUrl: String // https://live.bilibili.com/blackboard/room-current-rank.html?rank_type=master_realtime_hour_room&area_hour=1&area_v2_id=145&area_v2_parent_id=1
)
}

View File

@@ -1,37 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.annotations.SerializedName
data class Title(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Title>,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Title(
@SerializedName("colorful")
var colorful: Int, // 0
@SerializedName("height")
var height: Int, // 20
@SerializedName("id")
var id: String, // cake-flour
@SerializedName("img")
var img: String, // https://s1.hdslb.com/bfs/static/blive/live-assets/mobile/titles/title/3/cake-flour.png?20180726173300
@SerializedName("is_lihui")
var isLihui: Int, // 0
@SerializedName("lihui_height")
var lihuiHeight: Int, // 0
@SerializedName("lihui_img")
var lihuiImg: String,
@SerializedName("lihui_width")
var lihuiWidth: Int, // 0
@SerializedName("title")
var title: String, // 2016 新春活动
@SerializedName("width")
var width: Int // 68
)
}

View File

@@ -1,55 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class User(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // OK
@SerializedName("msg")
var msg: String // OK
) {
data class Data(
@SerializedName("gold")
var gold: Int, // 0
@SerializedName("isSign")
var isSign: Int, // 0
@SerializedName("medal")
var medal: JsonElement?, // null
@SerializedName("new")
var new: Int, // 1
@SerializedName("room_id")
var roomId: Long, // 1110317
@SerializedName("silver")
var silver: Int, // 140258
@SerializedName("svip")
var svip: Int, // 0
@SerializedName("svip_time")
var svipTime: String, // 0000-00-00 00:00:00
@SerializedName("use_count")
var useCount: Int, // 0
@SerializedName("user_level")
var userLevel: Int, // 25
@SerializedName("user_level_color")
var userLevelColor: Int, // 5805790
@SerializedName("vip")
var vip: Int, // 0
@SerializedName("vip_time")
var vipTime: String, // 2018-05-15 12:00:50
@SerializedName("vip_view_status")
var vipViewStatus: Int, // 1
@SerializedName("wearTitle")
var wearTitle: WearTitle
) {
data class WearTitle(
@SerializedName("activity")
var activity: String, // 0
@SerializedName("title")
var title: String // 0
)
}
}

View File

@@ -1,181 +0,0 @@
package com.hiczp.bilibili.api.live.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class UserInfoInRoom(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // success
@SerializedName("msg")
var msg: String // success
) {
data class Data(
@SerializedName("entry_effect")
var entryEffect: EntryEffect,
@SerializedName("gift")
var gift: Gift,
@SerializedName("info")
var info: Info,
@SerializedName("level")
var level: Level,
@SerializedName("new")
var new: Int, // 1
@SerializedName("privilege")
var privilege: Privilege,
@SerializedName("role")
var role: Role,
@SerializedName("room_admin")
var roomAdmin: RoomAdmin,
@SerializedName("wallet")
var wallet: Wallet
) {
data class Wallet(
@SerializedName("gold")
var gold: String, // 0
@SerializedName("silver")
var silver: String // 140258
)
data class Info(
@SerializedName("bili_vip")
var biliVip: Int, // 0
@SerializedName("face")
var face: String, // https://i1.hdslb.com/bfs/face/4f65e79399ad5a1bf3f877851b2f819d5870b494.jpg
@SerializedName("gender")
var gender: Int, // 0
@SerializedName("identification")
var identification: Int, // 1
@SerializedName("mobile_verify")
var mobileVerify: Int, // 1
@SerializedName("mobile_virtual")
var mobileVirtual: Int, // 0
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("platform_user_level")
var platformUserLevel: Int, // 4
@SerializedName("rank")
var rank: String, // 10000
@SerializedName("uid")
var uid: Long, // 20293030
@SerializedName("uname")
var uname: String, // czp3009
@SerializedName("vip_type")
var vipType: Int // 0
) {
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("role")
var role: Int, // 0
@SerializedName("type")
var type: Int // -1
)
}
data class Level(
@SerializedName("color")
var color: Int, // 5805790
@SerializedName("cost")
var cost: Int, // 52187800
@SerializedName("is_show_vip_broadcast")
var isShowVipBroadcast: Int, // 0
@SerializedName("master_level")
var masterLevel: MasterLevel,
@SerializedName("rcost")
var rcost: Long, // 2737665
@SerializedName("svip")
var svip: Int, // 0
@SerializedName("svip_time")
var svipTime: String, // 0000-00-00 00:00:00
@SerializedName("uid")
var uid: Int, // 20293030
@SerializedName("update_time")
var updateTime: String, // 0000-00-00 00:00:00
@SerializedName("user_level")
var userLevel: Int, // 25
@SerializedName("user_level_rank")
var userLevelRank: String, // >50000
@SerializedName("user_score")
var userScore: String, // 0
@SerializedName("vip")
var vip: Int, // 0
@SerializedName("vip_time")
var vipTime: String // 2018-05-15 12:00:50
) {
data class MasterLevel(
@SerializedName("color")
var color: Int, // 5805790
@SerializedName("current")
var current: List<Int>,
@SerializedName("level")
var level: Int, // 11
@SerializedName("next")
var next: List<Int>
)
}
data class RoomAdmin(
@SerializedName("is_admin")
var isAdmin: Int // 0
)
data class Role(
@SerializedName("info")
var info: Info,
@SerializedName("role_id")
var roleId: Int, // 2
@SerializedName("role_name")
var roleName: String // 播主
) {
data class Info(
@SerializedName("roomid")
var roomid: Long // 1110317
)
}
data class EntryEffect(
@SerializedName("basemap_url")
var basemapUrl: String,
@SerializedName("copy_writing")
var copyWriting: String,
@SerializedName("effective_time")
var effectiveTime: Int, // 0
@SerializedName("face")
var face: String, // https://i1.hdslb.com/bfs/face/4f65e79399ad5a1bf3f877851b2f819d5870b494.jpg
@SerializedName("highlight_color")
var highlightColor: String,
@SerializedName("id")
var id: Int, // 0
@SerializedName("mock_effect")
var mockEffect: Int, // 0
@SerializedName("priority")
var priority: Int, // 0
@SerializedName("privilege_type")
var privilegeType: Int, // 0
@SerializedName("show_avatar")
var showAvatar: Int, // 0
@SerializedName("target_id")
var targetId: Long, // 2866663
@SerializedName("uid")
var uid: Long // 20293030
)
data class Privilege(
@SerializedName("broadcast")
var broadcast: JsonElement?, // null
@SerializedName("notice_status")
var noticeStatus: Int // 1
)
data class Gift(
@SerializedName("is_show")
var isShow: String, // 1
@SerializedName("uid")
var uid: Long // 20293030
)
}
}

View File

@@ -1,219 +0,0 @@
package com.hiczp.bilibili.api.live.websocket
import com.github.salomonbrys.kotson.obj
import com.google.gson.JsonObject
import com.hiczp.bilibili.api.BilibiliClient
import com.hiczp.bilibili.api.jsonParser
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.wss
import io.ktor.http.cio.websocket.CloseReason
import io.ktor.http.cio.websocket.close
import io.ktor.util.InternalAPI
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.decodeString
import io.ktor.util.error
import kotlinx.coroutines.*
import mu.KotlinLogging
private val logger = KotlinLogging.logger { }
/**
* 直播客户端
* 注意该类是有状态的
*
* @param maybeShortRoomId 可能为短房间号的房间号
* @param fetchRoomId 是否在连接前先获取房间号(长号)
* @param fetchDanmakuConfig 是否在连接前先获取弹幕推送服务器地址
* @param doEntryRoomAction 是否产生直播间观看历史记录
* @param sendUserOnlineHeart 是否发送 rest 心跳包, 这会增加观看直播的时长, 用于服务端统计(与弹幕推送无关)
* @param callback 回调
*/
@Suppress("CanBeParameter")
class LiveClient(
private val bilibiliClient: BilibiliClient,
maybeShortRoomId: Long,
private val fetchRoomId: Boolean = true,
private val fetchDanmakuConfig: Boolean = true,
private val doEntryRoomAction: Boolean = false,
private val sendUserOnlineHeart: Boolean = false,
callback: LiveClientCallbackDSL.() -> Unit
) {
private val callback = LiveClientCallbackDSL().apply { callback() }
private val liveAPI = bilibiliClient.liveAPI
var roomId = maybeShortRoomId
private set
/**
* 开启连接
*/
@UseExperimental(KtorExperimentalAPI::class, ObsoleteCoroutinesApi::class, InternalAPI::class)
fun launch() = GlobalScope.launch(CoroutineExceptionHandler { _, throwable ->
callback.onError?.invoke(this, throwable) ?: logger.error(throwable)
}) {
//得到原始房间号和主播的用户ID
var anchorUserId = 0L
if (fetchRoomId) {
liveAPI.mobileRoomInit(roomId).await().data.also {
roomId = it.roomId
anchorUserId = it.uid
}
}
//获得 wss 地址和端口(推荐服务器)
@Suppress("SpellCheckingInspection")
var host = "broadcastlv.chat.bilibili.com"
var port = 443
if (fetchDanmakuConfig) {
liveAPI.getDanmakuConfig(roomId).await().data.also { data ->
host = data.host
data.hostServerList.find { it.host == host }?.wssPort?.also {
port = it
}
}
}
//产生历史记录
@Suppress("DeferredResultUnused")
if (doEntryRoomAction && bilibiliClient.isLogin) liveAPI.roomEntryAction(roomId)
//开启 websocket
HttpClient(CIO).config { install(WebSockets) }.wss(host = host, port = port, path = "/sub") {
//发送进房数据包
send(PresetPacket.enterRoomPacket(anchorUserId, roomId))
val enterRoomResponsePacket = incoming.receive().toPackets()[0]
if (enterRoomResponsePacket.packetType == PacketType.ENTER_ROOM_RESPONSE) {
try {
callback.onConnect?.invoke(this@LiveClient)
} catch (e: Exception) {
logger.error(e)
}
} else {
//impossible
logger.error { "Receive unreadable server response: $enterRoomResponsePacket" }
close(CloseReason(CloseReason.Codes.NOT_CONSISTENT, ""))
return@wss
}
//发送 rest 心跳包
//五分钟一次
val restHeartBeatJob = if (sendUserOnlineHeart && bilibiliClient.isLogin) {
launch {
val scale = bilibiliClient.billingClientProperties.scale
while (true) {
liveAPI.userOnlineHeart(roomId, scale).invokeOnCompletion {
if (it != null) logger.error(it)
}
delay(300_000)
}
}
} else {
null
}
//发送 websocket 心跳包
//30 秒一次
val websocketHeartBeatJob = launch {
try {
while (true) {
send(PresetPacket.heartbeatPacket())
delay(30_000)
}
} catch (ignore: CancellationException) {
//ignore
} catch (e: Exception) {
logger.error(e)
}
}
try {
while (true) {
withTimeout(40_000) {
incoming.receive()
}.toPackets().forEach {
try {
@Suppress("NON_EXHAUSTIVE_WHEN")
when (it.packetType) {
PacketType.POPULARITY -> callback.onPopularityPacket?.invoke(
this@LiveClient,
it.content.int
)
PacketType.COMMAND -> callback.onCommandPacket?.invoke(
this@LiveClient,
jsonParser.parse(it.content.decodeString()).obj
)
}
} catch (e: Exception) {
logger.error(e)
}
}
}
} catch (e: TimeoutCancellationException) {
throw e
} catch (e: CancellationException) {
close()
} finally {
restHeartBeatJob?.cancel()
websocketHeartBeatJob.cancel()
launch {
val closeReason = closeReason.await()
try {
callback.onClose?.invoke(this@LiveClient, closeReason)
} catch (e: Exception) {
logger.error(e)
}
}
}
}
}
/**
* 发送弹幕
*/
fun sendMessage(message: String) =
liveAPI.sendMessage(cid = roomId, mid = bilibiliClient.userId ?: 0, message = message)
}
class LiveClientCallbackDSL {
/**
* 成功进入房间时触发
*/
var onConnect: ((LiveClient) -> Unit)? = null
/**
* 抛出异常时触发
*/
var onError: ((LiveClient, Throwable) -> Unit)? = null
/**
* 收到人气值数据包
*/
var onPopularityPacket: ((LiveClient, Int) -> Unit)? = null
/**
* 收到 command 数据包
*/
var onCommandPacket: ((LiveClient, JsonObject) -> Unit)? = null
/**
* 连接关闭时触发
*/
var onClose: ((LiveClient, CloseReason?) -> Unit)? = null
}
/**
* 打开一个直播客户端
*/
fun BilibiliClient.liveClient(
roomId: Long,
fetchRoomId: Boolean = true,
fetchDanmakuConfig: Boolean = true,
doEntryRoomAction: Boolean = false,
sendUserOnlineHeart: Boolean = false,
callback: LiveClientCallbackDSL.() -> Unit
) = LiveClient(
this, roomId, fetchRoomId, fetchDanmakuConfig, doEntryRoomAction, sendUserOnlineHeart,
callback
)

View File

@@ -1,66 +0,0 @@
package com.hiczp.bilibili.api.live.websocket
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.WebSocketSession
import java.nio.ByteBuffer
/**
* 数据包模型
* 由于 Android APP 并未全线换成 wss, 以下用的是移动版网页的协议
* 数据包头部结构 00 00 00 65 00 10 00 01 00 00 00 07 00 00 00 01
* |数据包总长度| |头长| |tag| |数据包类型 | | tag |
*
* @param shortTag 一种 tag, 如果是非 command 数据包则为 1, 否则为 0, short 类型
* @param packetType 数据包类型
* @param tag 同 tagShort, 但是为 int 类型
* @param content 正文内容
*/
@Suppress("MemberVisibilityCanBePrivate")
data class Packet(
val shortTag: Short = 1,
val packetType: PacketType,
val tag: Int = 1,
val content: ByteBuffer
) {
val totalLength
get() = headerLength + content.limit()
val headerLength: Short = 0x10
fun toFrame() = Frame.Binary(
true,
ByteBuffer.allocate(totalLength)
.putInt(totalLength)
.putShort(headerLength)
.putShort(shortTag)
.putInt(packetType.value)
.putInt(tag)
.put(content).apply {
flip()
}!!
)
}
/**
* 一个 Message 中可能包含多个数据包
*/
internal fun Frame.toPackets(): List<Packet> {
val bufferLength = buffer.limit()
val list = ArrayList<Packet>()
while (buffer.hasRemaining()) {
val startPosition = buffer.position()
val totalLength = buffer.int
buffer.position(buffer.position() + 2) //skip headerLength
val shortTag = buffer.short
val packetType = PacketType.getByValue(buffer.int)
val tag = buffer.int
buffer.limit(startPosition + totalLength)
val content = buffer.slice()
buffer.position(buffer.limit())
buffer.limit(bufferLength)
list.add(Packet(shortTag, packetType, tag, content))
}
return list
}
internal suspend inline fun WebSocketSession.send(packet: Packet) = send(packet.toFrame())

View File

@@ -1,373 +0,0 @@
package com.hiczp.bilibili.api.main
import com.hiczp.bilibili.api.main.model.*
import com.hiczp.bilibili.api.retrofit.CommonResponse
import com.hiczp.bilibili.api.retrofit.Header
import kotlinx.coroutines.Deferred
import retrofit2.http.*
/**
* 这也是总站 API
*/
@Suppress("DeferredIsResult")
interface MainAPI {
/**
* 获取一个视频下的评论
* 注意, 评论是倒序排序的, 即楼层大的楼排在前面, 所以返回值中的 next 会比 prev 小
* 返回值中的 rpid 为评论 id. parent 为父评论的 id, parent 为 0 的是顶级评论
*
* @param oid 就是 aid, 视频的唯一标识
* @param pageSize 分页大小, 最大值 50
* @param next 下一页的起始楼层(这一层不包含在返回值内), 注意, 翻页是越翻楼层越小的. 如果为 null 则从最后一楼(最新的评论)开始
*/
@GET("/x/v2/reply/main")
fun reply(
@Query("mode") mode: Int = 1,
@Query("next") next: Long? = null,
@Query("oid") oid: Long,
@Query("plat") plat: Int? = 2,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 1
): Deferred<Reply>
/**
* 获取一个视频下的评论的子评论
*
* @param minId 想要请求的子评论(复数)的第一个子评论的 id(子评论默认升序排序), 为 null 时从 0 楼开始
* @param oid aid
* @param root 根评论的 id
* @param size 分页大小
*/
@GET("/x/v2/reply/reply/cursor")
fun childReply(
@Query("min_id") minId: Long? = null,
@Query("oid") oid: Long,
@Query("plat") plat: Int? = 2,
@Query("root") root: Long,
@Query("size") size: Int = 20,
@Query("sort") sort: Int = 0,
@Query("type") type: Int = 1
): Deferred<ChildReply>
/**
* 查看 "对话列表"
* 当一个子评论中有多组人在互相 at 时, 旁边就会有一个按钮 "查看对话", 将启动一个 dialog 展示内容
* parentId 与 rootId 在请求子评论列表时取得
*
* @param dialog "查看对话" 按钮所在的评论所 at 的那条评论的 id, 即 parentId
* @param minFloor 最小楼层, 翻页参数
* @param oid aid
* @param root "查看对话" 按钮所在的根评论 id, 即 rootId
* @param size 分页大小
*
* @see childReply
*/
@GET("/x/v2/reply/dialog/cursor")
fun chatList(
@Query("dialog") dialog: Long,
@Query("min_floor") minFloor: Long? = null,
@Query("oid") oid: Long,
@Query("plat") plat: Int? = 2,
@Query("root") root: Long,
@Query("size") size: Int = 20,
@Query("type") type: Int = 1
): Deferred<ChatList>
/**
* 获得一个番剧的分季信息(生成番剧页面所需的信息), 包含当前选择的季的分集信息
* seasonId 或 episodeId 必须有一个, 如果用 episodeId 将跳转到对应的 season 的页面
* 返回值中, 每个 episode 都有 aid 和 cid
*
* @param seasonId 季的唯一标识
* @param episodeId 集的唯一标识
*/
@GET("/pgc/view/app/season")
fun season(
@Query("season_id") seasonId: Long? = null,
@Query("ep_id") episodeId: Long? = null,
@Query("track_path") trackPath: Int? = null
): Deferred<Season>
/**
* 番剧页面下方的推荐(对当前季进行推荐)
* 返回值中的 relates 是 "相关推荐"(广告), season 是 "更多推荐"(其他番, 目标为季)
*
* @param seasonId 季的唯一标识
*/
@GET("/pgc/season/app/related/recommend")
fun recommend(@Query("season_id") seasonId: Long): Deferred<Recommend>
/**
* 我的追番动态(追番页面上方的那一条 "我的追番")
* 首页 -> 追番 -> 我的追番
*/
@Suppress("SpellCheckingInspection")
@GET("/pgc/app/page/bangumi/mine")
fun myBangumiNews(
@Query("fnval") fnval: Int = 16,
@Query("fnver") fnver: Int = 0
): Deferred<MyBangumiNews>
/**
* 追番页面(客户端用这里面的数据来生成追番页面)
* 每个模块(module)的数据(item)全部超过三个.
* 每个板块下面的 换一换 按钮并不重新请求数据, 而是从每个模块的数据里选出另一批
* 首页 -> 追番
*
* @param pgcHomeTimelineABTest 与 A/B Test 有关, 不明确其值含义, 有可能使得返回内容不一样
*/
@Suppress("SpellCheckingInspection")
@GET("/pgc/app/page/bangumi")
fun bangumiPage(
@Query("fnval") fnval: Int = 16,
@Query("fnver") fnver: Int = 0,
@Query("pgc_home_timeline_abtest") pgcHomeTimelineABTest: Int? = 13
): Deferred<BangumiPage>
/**
* 获得更多 "编辑推荐"
* 首页 -> 追番 -> (下拉)
*
* @param cursor 表示时间(ms), 但是可能是科学计数法. 每次请求所用的 cursor 在上一次的返回值里的最后一个 item 里. 第一次请求所用的 cursor 在追番页面的返回值的最后.
* @param size 分页大小
* @param wid 不明确, 有可能是一些 padding, margin, 用于计算位置
*
* @see bangumiPage
*/
fun bangumiMore(
@Query("cursor") cursor: String,
@Query("size") size: Int = 10,
@Query("wid") wid: String? = "78,79,80,81,59"
): Deferred<BangumiMore>
/**
* 发送评论
* 如果发送根评论则 root 和 parent 为 null
* 如果发送子评论则 root 和 parent 均为根评论的 id
* 如果在子评论中 at 别人(即对子评论进行评论), 那么 root 为所属根评论的 id, parent 为所 at 的那个评论的 id
* at 别人时, 评论的内容必须符合以下格式 "回复 @$username :$message"
*
* @param message 发送的内容
* @param oid aid
* @param parent 父评论 id
* @param root 根评论 id
*/
@POST("/x/v2/reply/add")
@FormUrlEncoded
fun sendReply(
@Field("from") from: Int? = null,
@Field("message") message: String,
@Field("oid") oid: Long,
@Field("parent") parent: Long? = null,
@Field("plat") plat: Int = 2,
@Field("root") root: Long? = null,
@Field("type") type: Int = 1
): Deferred<SendReplyResponse>
/**
* 点赞(评论)
*
* @param action 为 1 时表示点赞, 0 表示取消点赞
* @param oid aid
* @param replyId 评论的 ID
*/
@Suppress("SpellCheckingInspection")
@POST("/x/v2/reply/action")
@FormUrlEncoded
fun likeReply(
@Field("action") action: Int,
@Field("oid") oid: Long,
@Field("rpid") replyId: Long,
@Field("type") type: Int = 1
): Deferred<CommonResponse>
/**
* 不喜欢(评论)
*
* @param action 为 1 时表示不喜欢, 为 0 时表示取消不喜欢
* @param oid aid
* @param replyId 评论的 ID
*/
@Suppress("SpellCheckingInspection")
@POST("/x/v2/reply/hate")
@FormUrlEncoded
fun dislikeReply(
@Field("action") action: Int,
@Field("oid") oid: Long,
@Field("rpid") replyId: Long,
@Field("type") type: Int = 1
): Deferred<CommonResponse>
/**
* 查看视频的删除日志
* 这个 API 看起来有翻页, 其实没有页
* 视频 -> 评论 -> 右上角三个点 ->查看删除日志
*
* @return 有$replyCount条评论因$reportCount次举报已被管理员移除
*/
@GET("/x/v2/reply/log")
fun deleteLog(
@Query("oid") oid: Long,
@Query("pn") pageNumber: Int = 1,
@Query("ps") pageSize: Int = 20,
@Query("type") type: Int = 1
): Deferred<DeleteLog>
/**
* 改变与某用户的关系(关注, 取消关注)
*
* @param action 动作类型(1: 关注;2: 取消关注)
* @param followId 操作对象的 ID(例如欲关注的那个用户的 ID)
* @param reSrc 不明确
*/
@POST("/x/relation/modify")
@FormUrlEncoded
fun modifyRelation(
@Field("act") action: Int,
@Field("fid") followId: Long,
@Field("re_src") reSrc: Int? = 32
): Deferred<CommonResponse>
/**
* 查看关注分组
* 默认分组永远是 0
*/
@GET("/x/relation/tag/m/tags")
fun relationTags(): Deferred<RelationTags>
/**
* 创建关注分组
*
* @param tag 不能包含绝大部分符号
*/
@POST("/x/relation/tag/create")
@FormUrlEncoded
fun createRelationTag(@Field("tag") tag: String): Deferred<CreateRelationTagResponse>
/**
* 设置分组(对某个用户的关注的分组)
* 用户 -> 关注 -> 设置分组
*
* @param followIds 关注的人(不明确能不能有多个)
* @param tagIds 分组 id. 可以有多个, 用逗号隔开, 例如 "-10,110641"
*/
@Suppress("SpellCheckingInspection")
@POST("/x/relation/tags/addUsers")
@FormUrlEncoded
fun relationAddUsers(
@Field("fids") followIds: String,
@Field("tagids") tagIds: String
): Deferred<CommonResponse>
/**
* 收藏文章
*
* @param id 文章的 id
*/
@POST("/x/article/favorites/add")
@FormUrlEncoded
fun addFavoriteArticle(@Field("id") id: Long): Deferred<CommonResponse>
/**
* 点赞(文章)
*
* @param id 文章的 id
* @param type 操作类型, 1 为点赞, 2 为取消点赞
*/
@POST("/x/article/like")
@FormUrlEncoded
fun articleLike(@Field("id") id: Long, @Field("type") type: Int): Deferred<CommonResponse>
/**
* 查看收藏夹分组
*
* @param aid 视频的唯一标识, 用于判断是否已经将当前视频加入收藏夹
* @param vmId 用户 id
*/
@Suppress("SpellCheckingInspection")
@GET("/x/v2/fav/folder")
fun favoriteFolder(
@Query("aid") aid: Long,
@Query("vmid") vmId: Long
): Deferred<FavoriteFolder>
/**
* 创建收藏夹
*
* @param name 收藏夹名
* @param public 是否公开, 0 为公开
*/
@POST("/x/v2/fav/folder/add")
@FormUrlEncoded
fun createFavoriteFolder(
@Field("name") name: String,
@Field("public") public: Int = 0
): Deferred<CreateFavoriteFolderResponse>
/**
* 收藏视频
*
* @param fid 收藏夹的 id, 可以有多个, 用逗号隔开. 例如 795158,3326376
*/
@POST("/x/v2/fav/video/add")
@FormUrlEncoded
fun addFavoriteVideo(
@Field("aid") aid: Long,
@Field("fid") fid: String,
@Field("from") from: Int? = null
): Deferred<CommonResponse>
/**
* 取消收藏视频
*
* @param fid 收藏夹的 id, 可以有多个, 同上
*
* @see addFavoriteVideo
*/
@POST("/x/v2/fav/video/del")
@FormUrlEncoded
fun deleteFavoriteVideo(
@Field("aid") aid: Long,
@Field("fid") fid: String
): Deferred<CommonResponse>
/**
* 发送弹幕(视频, 番剧)
*
* @param oid cid
* @param random 9 位的随机数字
* @param progress 播放器时间(ms)
*/
@POST("/x/v2/dm/post")
@FormUrlEncoded
@Headers(Header.FORCE_QUERY)
fun sendDanmaku(
@Query("aid") aid: Long,
@Query("oid") oid: Long,
@Field("pool") pool: Int = 0,
@Field("rnd") random: Int = (100000000..999999999).random(),
@Field("oid") oidInBody: Long,
@Field("fontsize") fontSize: Int = 25,
@Field("msg") message: String,
@Field("mode") mode: Int = 1,
@Field("progress") progress: Long,
@Field("color") color: Int = 16777215,
@Field("plat") plat: Int = 2,
@Field("screen_state") screenState: Int = 0,
@Field("from") from: Int? = null,
@Field("type") type: Int = 1
): Deferred<SendDanmakuResponse>
/**
* 发送弹幕的快捷方式
*/
@JvmDefault
fun sendDanmaku(aid: Long, cid: Long, progress: Long, message: String) =
sendDanmaku(aid = aid, oid = cid, oidInBody = cid, progress = progress, message = message)
/**
* 获取文章分类列表
*/
@GET("/x/article/categories")
fun articleCategories(): Deferred<ArticleCategories>
}

View File

@@ -1,25 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class ArticleCategories(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Category>,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Category(
@SerializedName("children")
var children: List<Category>,
@SerializedName("id")
var id: Int, // 17
@SerializedName("name")
var name: String, // 科技
@SerializedName("parent_id")
var parentId: Int // 0
)
}

View File

@@ -1,43 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class BangumiMore(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String, // success
@SerializedName("result")
var result: List<Result>
) {
data class Result(
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/5bac9515a50c880e55a772c194241ff9943e0004.png
/**
* cursor 的值有可能是科学计数法, 例如 1.550210400638E12
* 如果不是最后一个 item 将没有这个字段
*/
@SerializedName("cursor")
var cursor: String?, // 1548172800112.0
@SerializedName("desc")
var desc: String, // 正在就读白凰女学院3年级的加藤茉莉香是个拥有“私掠船免状”的合法宇宙海贼。她不仅是学生、宇宙艇部的部长、咖啡馆的服务员还是宇宙海贼船·弁天丸的船长每天都过着繁忙而充实的生活。某天正在豪华客船上开展工作的茉莉香在乘客名单中发现了拥有银河通行证的少年·无限彼方的名字……。少年与海贼的亚空冒险就此展开
@SerializedName("id")
var id: Int, // 33409
@SerializedName("is_new")
var isNew: Int, // 0
@SerializedName("link")
var link: String, // https://www.bilibili.com/read/cv1831506
@SerializedName("link_type")
var linkType: Int, // 4
@SerializedName("link_value")
var linkValue: Int, // 0
@SerializedName("pub_time")
var pubTime: String, // 2019-01-23 00:00:00
@SerializedName("simg")
var simg: String,
@SerializedName("title")
var title: String, // 化身为刃,除魔四方——《多罗罗》
@SerializedName("wid")
var wid: Int // 81
)
}

View File

@@ -1,80 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class BangumiPage(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String, // success
@SerializedName("result")
var result: Result
) {
data class Result(
@SerializedName("modules")
var modules: List<Module>,
@SerializedName("regions")
var regions: List<Region>
) {
data class Module(
@SerializedName("attr")
var attr: Attr,
@SerializedName("headers")
var headers: List<JsonElement>,
@SerializedName("items")
var items: List<Item>,
@SerializedName("module_id")
var moduleId: Int, // 6
@SerializedName("size")
var size: Int, // 10
@SerializedName("style")
var style: String, // fall
@SerializedName("title")
var title: String, // 编辑推荐
@SerializedName("wid")
var wid: List<Int>
) {
data class Item(
@SerializedName("badge")
var badge: String, // NEW
@SerializedName("badge_type")
var badgeType: Int, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/57e00f9995459ab0cf40800358ee7f3b392b38a4.jpg
@SerializedName("cursor")
var cursor: String, // 1.55021040036E12
@SerializedName("desc")
var desc: String, // 明明是最差劲的相遇,但雏却不知何时开始无法停止心动。雏被初中时的学长·恋雪所吸引,决定和他进入同所高中而拼命学习。并且,和青梅竹马的虎太朗一同进入了樱丘高中。曾经单调乏味的恋雪,为了自己单相思的对象,而在假日结束后改换了形象,变得受欢迎起来。在这种状况下,雏决定要「告白」,但是——!?
@SerializedName("is_new")
var isNew: Int, // 0
@SerializedName("item_id")
var itemid: Long, // 34265
@SerializedName("link")
var link: String, // https://www.bilibili.com/blackboard/topic/activity-dm4qK4-BI.html
@SerializedName("title")
var title: String, // 【资讯档】2019年第7周
@SerializedName("wid")
var wid: Int // 78
)
data class Attr(
@SerializedName("follow")
var follow: Int, // 0
@SerializedName("header")
var header: Int, // 1
@SerializedName("random")
var random: Int // 0
)
}
data class Region(
@SerializedName("icon")
var icon: String, // http://i0.hdslb.com/bfs/bangumi/3b66adc7339e62d469ea5b89a45c74e14e3ae831.png
@SerializedName("title")
var title: String, // 点评
@SerializedName("url")
var url: String // bilibili://pgc/review/index
)
}
}

View File

@@ -1,227 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class ChatList(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("config")
var config: Config,
@SerializedName("cursor")
var cursor: Cursor,
@SerializedName("dialog")
var dialog: Dialog,
@SerializedName("replies")
var replies: List<Reply>
) {
data class Reply(
@SerializedName("action")
var action: Int, // 0
@SerializedName("assist")
var assist: Int, // 0
@SerializedName("attr")
var attr: Int, // 8
@SerializedName("content")
var content: Content,
@SerializedName("count")
var count: Int, // 0
@SerializedName("ctime")
var ctime: Int, // 1541824116
@SerializedName("dialog")
var dialog: Long, // 1136351035
@SerializedName("dialog_str")
var dialogStr: String,
@SerializedName("fansgrade")
var fansgrade: Int, // 0
@SerializedName("floor")
var floor: Int, // 172
@SerializedName("folder")
var folder: Folder,
@SerializedName("like")
var like: Int, // 0
@SerializedName("member")
var member: Member,
@SerializedName("mid")
var mid: Long, // 161745277
@SerializedName("oid")
var oid: Long, // 34175504
@SerializedName("parent")
var parent: Int, // 1136656601
@SerializedName("parent_str")
var parentStr: String, // 1136656601
@SerializedName("rcount")
var rcount: Int, // 0
@SerializedName("replies")
var replies: JsonElement?, // null
@SerializedName("root")
var root: Long, // 1136310360
@SerializedName("root_str")
var rootStr: String, // 1136310360
@SerializedName("rpid")
var rpid: Long, // 1175989845
@SerializedName("rpid_str")
var rpidStr: String, // 1175989845
@SerializedName("state")
var state: Int, // 2
@SerializedName("type")
var type: Int, // 1
@SerializedName("up_action")
var upAction: UpAction
) {
data class UpAction(
@SerializedName("like")
var like: Boolean, // false
@SerializedName("reply")
var reply: Boolean // false
)
data class Member(
@SerializedName("DisplayRank")
var displayRank: String, // 0
@SerializedName("avatar")
var avatar: String, // http://static.hdslb.com/images/member/noface.gif
@SerializedName("fans_detail")
var fansDetail: JsonElement?, // null
@SerializedName("following")
var following: Int, // 0
@SerializedName("level_info")
var levelInfo: LevelInfo,
@SerializedName("mid")
var mid: String, // 161745277
@SerializedName("nameplate")
var nameplate: Nameplate,
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("pendant")
var pendant: Pendant,
@SerializedName("rank")
var rank: String, // 10000
@SerializedName("sex")
var sex: String, // 保密
@SerializedName("sign")
var sign: String,
@SerializedName("uname")
var uname: String, // vo6869
@SerializedName("vip")
var vip: Vip
) {
data class Pendant(
@SerializedName("expire")
var expire: Long, // 0
@SerializedName("image")
var image: String,
@SerializedName("name")
var name: String,
@SerializedName("pid")
var pid: Int // 0
)
data class Nameplate(
@SerializedName("condition")
var condition: String,
@SerializedName("image")
var image: String,
@SerializedName("image_small")
var imageSmall: String,
@SerializedName("level")
var level: String,
@SerializedName("name")
var name: String,
@SerializedName("nid")
var nid: Int // 0
)
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 1544371200000
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 1
)
data class LevelInfo(
@SerializedName("current_exp")
var currentExp: Int, // 0
@SerializedName("current_level")
var currentLevel: Int, // 2
@SerializedName("current_min")
var currentMin: Int, // 0
@SerializedName("next_exp")
var nextExp: Int // 0
)
}
data class Content(
@SerializedName("device")
var device: String, // phone
@SerializedName("members")
var members: List<JsonElement>,
@SerializedName("message")
var message: String, // 回复 @***全副武装 :耶酥是佛教徒
@SerializedName("plat")
var plat: Int // 3
)
data class Folder(
@SerializedName("has_folded")
var hasFolded: Boolean, // false
@SerializedName("is_folded")
var isFolded: Boolean, // false
@SerializedName("rule")
var rule: String
)
}
data class Cursor(
@SerializedName("max_floor")
var maxFloor: Int, // 172
@SerializedName("min_floor")
var minFloor: Int, // 8
@SerializedName("size")
var size: Int // 11
)
data class Config(
@SerializedName("show_up_flag")
var showUpFlag: Boolean, // true
@SerializedName("showadmin")
var showadmin: Int, // 0
@SerializedName("showentry")
var showentry: Int, // 0
@SerializedName("showfloor")
var showfloor: Int, // 1
@SerializedName("showtopic")
var showtopic: Int // 1
)
data class Dialog(
@SerializedName("max_floor")
var maxFloor: Int, // 172
@SerializedName("min_floor")
var minFloor: Int // 8
)
}
}

View File

@@ -1,403 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class ChildReply(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("assist")
var assist: Int, // 0
@SerializedName("blacklist")
var blacklist: Int, // 0
@SerializedName("config")
var config: Config,
@SerializedName("cursor")
var cursor: Cursor,
@SerializedName("root")
var root: Root,
@SerializedName("upper")
var upper: Upper
) {
data class Config(
@SerializedName("show_up_flag")
var showUpFlag: Boolean, // true
@SerializedName("showadmin")
var showadmin: Int, // 0
@SerializedName("showentry")
var showentry: Int, // 0
@SerializedName("showfloor")
var showfloor: Int, // 1
@SerializedName("showtopic")
var showtopic: Int // 1
)
data class Upper(
@SerializedName("mid")
var mid: Long // 7584632
)
data class Cursor(
@SerializedName("all_count")
var allCount: Int, // 2
@SerializedName("max_id")
var maxId: Int, // 2
@SerializedName("min_id")
var minId: Int, // 1
@SerializedName("size")
var size: Int // 2
)
data class Root(
@SerializedName("action")
var action: Int, // 0
@SerializedName("assist")
var assist: Int, // 0
@SerializedName("attr")
var attr: Int, // 0
@SerializedName("content")
var content: Content,
@SerializedName("count")
var count: Int, // 2
@SerializedName("ctime")
var ctime: Int, // 1550681500
@SerializedName("dialog")
var dialog: Long, // 0
@SerializedName("dialog_str")
var dialogStr: String,
@SerializedName("fansgrade")
var fansgrade: Int, // 0
@SerializedName("floor")
var floor: Int, // 1348
@SerializedName("folder")
var folder: Folder,
@SerializedName("like")
var like: Int, // 1
@SerializedName("member")
var member: Member,
@SerializedName("mid")
var mid: Long, // 14363383
@SerializedName("oid")
var oid: Long, // 16622855
@SerializedName("parent")
var parent: Long, // 0
@SerializedName("parent_str")
var parentStr: String, // 0
@SerializedName("rcount")
var rcount: Int, // 2
@SerializedName("replies")
var replies: List<Reply>,
@SerializedName("root")
var root: Long, // 0
@SerializedName("root_str")
var rootStr: String, // 0
@SerializedName("rpid")
var rpid: Long, // 1405602348
@SerializedName("rpid_str")
var rpidStr: String, // 1405602348
@SerializedName("state")
var state: Int, // 0
@SerializedName("type")
var type: Int, // 1
@SerializedName("up_action")
var upAction: UpAction
) {
data class Folder(
@SerializedName("has_folded")
var hasFolded: Boolean, // false
@SerializedName("is_folded")
var isFolded: Boolean, // false
@SerializedName("rule")
var rule: String // https://www.bilibili.com/blackboard/foldingreply.html
)
data class Reply(
@SerializedName("action")
var action: Int, // 0
@SerializedName("assist")
var assist: Int, // 0
@SerializedName("attr")
var attr: Int, // 0
@SerializedName("content")
var content: Content,
@SerializedName("count")
var count: Int, // 0
@SerializedName("ctime")
var ctime: Int, // 1550682402
@SerializedName("dialog")
var dialog: Long, // 1405625526
@SerializedName("dialog_str")
var dialogStr: String,
@SerializedName("fansgrade")
var fansgrade: Int, // 0
@SerializedName("floor")
var floor: Int, // 2
@SerializedName("folder")
var folder: Folder,
@SerializedName("like")
var like: Int, // 1
@SerializedName("member")
var member: Member,
@SerializedName("mid")
var mid: Long, // 14363383
@SerializedName("oid")
var oid: Long, // 16622855
@SerializedName("parent")
var parent: Long, // 1405602348
@SerializedName("parent_str")
var parentStr: String, // 1405602348
@SerializedName("rcount")
var rcount: Int, // 0
@SerializedName("replies")
var replies: List<JsonElement>, // []
@SerializedName("root")
var root: Long, // 1405602348
@SerializedName("root_str")
var rootStr: String, // 1405602348
@SerializedName("rpid")
var rpid: Long, // 1405625526
@SerializedName("rpid_str")
var rpidStr: String, // 1405625526
@SerializedName("state")
var state: Int, // 0
@SerializedName("type")
var type: Int, // 1
@SerializedName("up_action")
var upAction: UpAction
) {
data class Content(
@SerializedName("device")
var device: String,
@SerializedName("members")
var members: List<JsonElement>,
@SerializedName("message")
var message: String, // 导演:你认为是否有人了解你?像你自己一样了解你?老佛爷:这个问题我很难回答,别人对我的想法已根深蒂固,所以我认为几乎是不可能,我想是如此,即使是我深爱的人。我不想在别人生活中显得真实,我想成为幽灵,现身,然后消失,我也不想面对任何人的真实,因为我不想面对真实的自己,那是我的秘密。别跟我说那些关于孤独的陈词滥调,之于我这种人,孤独是一种胜利,这是场人生战役。像我一样从事创意工作的人,必须独处,让自己重新充电,整日生活在聚光灯前是无法创作的。我还要做许多事,例如阅读,身边有人就无法去做。平时几乎已没时间,但我随时都会想阅读,所以我赞成每人都要该独立生活。将别人当成依靠,对于我这样的人来说很危险,我必须时时刻刻如履薄冰,并在它破裂之前跨出下一步。
@SerializedName("plat")
var plat: Int // 2
)
data class UpAction(
@SerializedName("like")
var like: Boolean, // false
@SerializedName("reply")
var reply: Boolean // false
)
data class Member(
@SerializedName("DisplayRank")
var displayRank: String, // 0
@SerializedName("avatar")
var avatar: String, // http://i2.hdslb.com/bfs/face/63f5da7bda813e470cefd465767035efccff747d.jpg
@SerializedName("fans_detail")
var fansDetail: JsonElement?, // null
@SerializedName("following")
var following: Int, // 0
@SerializedName("level_info")
var levelInfo: LevelInfo,
@SerializedName("mid")
var mid: String, // 14363383
@SerializedName("nameplate")
var nameplate: Nameplate,
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("pendant")
var pendant: Pendant,
@SerializedName("rank")
var rank: String, // 10000
@SerializedName("sex")
var sex: String, // 保密
@SerializedName("sign")
var sign: String, // - 故事何必听的真切,自在之人掀雨踏天阙。
@SerializedName("uname")
var uname: String, // 浮生不思量
@SerializedName("vip")
var vip: Vip
) {
data class Pendant(
@SerializedName("expire")
var expire: Long, // 0
@SerializedName("image")
var image: String,
@SerializedName("name")
var name: String,
@SerializedName("pid")
var pid: Int // 0
)
data class Nameplate(
@SerializedName("condition")
var condition: String,
@SerializedName("image")
var image: String,
@SerializedName("image_small")
var imageSmall: String,
@SerializedName("level")
var level: String,
@SerializedName("name")
var name: String,
@SerializedName("nid")
var nid: Int // 0
)
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 1515686400000
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 1
)
data class LevelInfo(
@SerializedName("current_exp")
var currentExp: Int, // 0
@SerializedName("current_level")
var currentLevel: Int, // 5
@SerializedName("current_min")
var currentMin: Int, // 0
@SerializedName("next_exp")
var nextExp: Int // 0
)
}
data class Folder(
@SerializedName("has_folded")
var hasFolded: Boolean, // false
@SerializedName("is_folded")
var isFolded: Boolean, // false
@SerializedName("rule")
var rule: String
)
}
data class Content(
@SerializedName("device")
var device: String,
@SerializedName("members")
var members: List<JsonElement>,
@SerializedName("message")
var message: String, // 唉有点不敢相信…R.I.P……走好走好
@SerializedName("plat")
var plat: Int // 2
)
data class UpAction(
@SerializedName("like")
var like: Boolean, // false
@SerializedName("reply")
var reply: Boolean // false
)
data class Member(
@SerializedName("DisplayRank")
var displayRank: String, // 0
@SerializedName("avatar")
var avatar: String, // http://i2.hdslb.com/bfs/face/63f5da7bda813e470cefd465767035efccff747d.jpg
@SerializedName("fans_detail")
var fansDetail: JsonElement?, // null
@SerializedName("following")
var following: Int, // 0
@SerializedName("level_info")
var levelInfo: LevelInfo,
@SerializedName("mid")
var mid: String, // 14363383
@SerializedName("nameplate")
var nameplate: Nameplate,
@SerializedName("official_verify")
var officialVerify: OfficialVerify,
@SerializedName("pendant")
var pendant: Pendant,
@SerializedName("rank")
var rank: String, // 10000
@SerializedName("sex")
var sex: String, // 保密
@SerializedName("sign")
var sign: String, // - 故事何必听的真切,自在之人掀雨踏天阙。
@SerializedName("uname")
var uname: String, // 浮生不思量
@SerializedName("vip")
var vip: Vip
) {
data class Pendant(
@SerializedName("expire")
var expire: Long, // 0
@SerializedName("image")
var image: String,
@SerializedName("name")
var name: String,
@SerializedName("pid")
var pid: Int // 0
)
data class Nameplate(
@SerializedName("condition")
var condition: String,
@SerializedName("image")
var image: String,
@SerializedName("image_small")
var imageSmall: String,
@SerializedName("level")
var level: String,
@SerializedName("name")
var name: String,
@SerializedName("nid")
var nid: Int // 0
)
data class OfficialVerify(
@SerializedName("desc")
var desc: String,
@SerializedName("type")
var type: Int // -1
)
data class Vip(
@SerializedName("accessStatus")
var accessStatus: Int, // 0
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Long, // 1515686400000
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
var vipStatusWarn: String,
@SerializedName("vipType")
var vipType: Int // 1
)
data class LevelInfo(
@SerializedName("current_exp")
var currentExp: Int, // 0
@SerializedName("current_level")
var currentLevel: Int, // 5
@SerializedName("current_min")
var currentMin: Int, // 0
@SerializedName("next_exp")
var nextExp: Int // 0
)
}
}
}
}

View File

@@ -1,19 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class CreateFavoriteFolderResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("fid")
var fid: Long // 3326376
)
}

View File

@@ -1,20 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class CreateRelationTagResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
@Suppress("SpellCheckingInspection")
data class Data(
@SerializedName("tagid")
var tagid: Int // 110641
)
}

View File

@@ -1,37 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class DeleteLog(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("logs")
var logs: JsonElement?, // null
@SerializedName("page")
var page: Page,
@SerializedName("reply_count")
var replyCount: Int, // 11
@SerializedName("report_count")
var reportCount: Int // 28
) {
data class Page(
@SerializedName("num")
var num: Int, // 1
@SerializedName("pages")
var pages: Int, // 0
@SerializedName("size")
var size: Int, // 20
@SerializedName("total")
var total: Int // 0
)
}
}

View File

@@ -1,50 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class FavoriteFolder(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: List<Data>,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("atten_count")
var attenCount: Int, // 0
@SerializedName("cover")
var cover: List<Cover>,
@SerializedName("ctime")
var ctime: Long, // 1451133174
@SerializedName("cur_count")
var curCount: Int, // 1
@SerializedName("favoured")
var favoured: Int, // 0
@SerializedName("fid")
var fid: Long, // 795158
@SerializedName("max_count")
var maxCount: Int, // 50000
@SerializedName("media_id")
var mediaId: Long, // 79515830
@SerializedName("mid")
var mid: Long, // 20293030
@SerializedName("mtime")
var mtime: Long, // 1544629663
@SerializedName("name")
var name: String, // 默认收藏夹
@SerializedName("state")
var state: Int // 0
) {
data class Cover(
@SerializedName("aid")
var aid: Long, // 9498716
@SerializedName("pic")
var pic: String, // http://i2.hdslb.com/bfs/archive/3536b8de71da4dd7bf01200db1e6c710b5f4aa0e.png
@SerializedName("type")
var type: Int // 2
)
}
}

View File

@@ -1,74 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class MyBangumiNews(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String, // success
@SerializedName("result")
var result: Result
) {
data class Result(
@SerializedName("delay")
var delay: List<JsonElement>,
@SerializedName("follow")
var follow: Int, // 34
@SerializedName("follows")
var follows: List<Follow>,
@SerializedName("follows_type")
var followsType: Int, // 1
@SerializedName("update")
var update: Int // 1
) {
data class Follow(
@SerializedName("badge")
var badge: String, // 会员抢先
@SerializedName("badge_type")
var badgeType: Int, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/f34ff3975c39913af936c133ae60a5891babba08.png
@SerializedName("is_finish")
var isFinish: Int, // 0
@SerializedName("is_started")
var isStarted: Int, // 1
@SerializedName("new_ep")
var newEp: NewEp,
/**
* 如果 progress 为 null 说明尚未观看
*/
@SerializedName("progress")
var progress: Progress?,
@SerializedName("season_id")
var seasonId: Int, // 25681
@SerializedName("title")
var title: String, // JOJO的奇妙冒险 黄金之风
@SerializedName("total_count")
var totalCount: Int, // 39
@SerializedName("url")
var url: String // https://www.bilibili.com/bangumi/play/ss25681
) {
data class NewEp(
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/archive/c3af18bf85040dfacb081db46e033f056318a8f0.jpg
@SerializedName("id")
var id: Int, // 250631
@SerializedName("index_show")
var indexShow: String // 更新至第20话
)
data class Progress(
@SerializedName("last_ep_desc")
var lastEpDesc: String, // 看到第2话
@SerializedName("last_ep_id")
var lastEpId: Int, // 250837
@SerializedName("last_ep_index")
var lastEpIndex: String, // 2
@SerializedName("last_time")
var lastTime: Int // 1377
)
}
}
}

View File

@@ -1,89 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class Recommend(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String, // success
@SerializedName("result")
var result: Result
) {
data class Result(
@SerializedName("card")
var card: JsonElement, // []
@SerializedName("relates")
var relates: List<Relate>,
@SerializedName("season")
var season: List<Season>
) {
data class Season(
@SerializedName("badge")
var badge: String, // 会员抢先
@SerializedName("badge_type")
var badgeType: Int, // 0
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/3fc16a667502cbff226e585eb660a96a20c7458c.png
@SerializedName("from")
var from: Int, // 0
@SerializedName("new_ep")
var newEp: NewEp,
@SerializedName("rating")
var rating: Rating,
@SerializedName("season_id")
var seasonId: Int, // 26146
@SerializedName("season_type")
var seasonType: Int, // 1
@SerializedName("stat")
var stat: Stat,
@SerializedName("title")
var title: String, // 多罗罗
@SerializedName("url")
var url: String // http://www.bilibili.com/bangumi/play/ss26146
) {
data class NewEp(
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/archive/7dec9d820b82ee57ebde0ba5c186b63e1e728abd.jpg
@SerializedName("index_show")
var indexShow: String // 更新至第7话
)
data class Rating(
@SerializedName("count")
var count: Long, // 22916
@SerializedName("score")
var score: Double // 9.8
)
data class Stat(
@SerializedName("danmaku")
var danmaku: Int, // 435439
@SerializedName("follow")
var follow: Int, // 2073877
@SerializedName("view")
var view: Int // 24884016
)
}
data class Relate(
@SerializedName("desc1")
var desc1: String, // 【萌羽Moeyu】魔法禁书目录御坂美琴易拉罐保温杯
@SerializedName("desc2")
var desc2: String, // 295
@SerializedName("item_id")
var itemid: Long, // 10005816
@SerializedName("pic")
var pic: String, // https://i0.hdslb.com/bfs/mall/mall/c3/f0/c3f029d8221c6ecc96bd1ab321034bc2.jpg
@SerializedName("title")
var title: String, // 【现货即发】魔法禁书目录正版授权Moeyu出品。
@SerializedName("type")
var type: Int, // 1
@SerializedName("type_name")
var typeName: String, // 商品
@SerializedName("url")
var url: String // bilibili://mall/web?url=https%3A%2F%2Fmall.bilibili.com%2Fdetail.html%3FitemsId%3D10005816%26msource%3Dfanju_25617_10005816%26noTitleBar%3D1%26loadingShow%3D1
)
}
}

View File

@@ -1,38 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class RelationTags(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
/**
* all 不是指全部, 而是指 公开关注
*/
@SerializedName("all")
var all: List<Tag>,
@SerializedName("default")
var default: List<Tag>,
@SerializedName("list")
var list: List<Tag>,
@SerializedName("special")
var special: List<Tag>
) {
@Suppress("SpellCheckingInspection")
data class Tag(
@SerializedName("count")
var count: Int, // 28
@SerializedName("name")
var name: String, // 公开关注
@SerializedName("tagid")
var tagid: Int // -1
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,283 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class Season(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String, // success
@SerializedName("result")
var result: Result
) {
data class Result(
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/bangumi/a92892921f3209f7784a954c37467c9869a1d4c1.png
@SerializedName("episodes")
var episodes: List<Episode>,
@SerializedName("evaluate")
var evaluate: String, // 位于东京西部的巨大“学园都市”实施着超能力开发的特殊课程。学生们的能力被给予从“无能力Level 0”到“超能力Level 5”的六阶段评价。高中生上条当麻由于寄宿在右手中的力量——只要是异能之力...
@SerializedName("link")
var link: String, // http://www.bilibili.com/bangumi/media/md134912/
@SerializedName("media_id")
var mimediaId: Long, // 134912
@SerializedName("mode")
var mode: Int, // 2
@SerializedName("new_ep")
var newEp: NewEp,
@SerializedName("paster")
var paster: Paster,
@SerializedName("payment")
var payment: Payment,
@SerializedName("publish")
var publish: Publish,
@SerializedName("rating")
var rating: Rating,
@SerializedName("record")
var record: String,
@SerializedName("rights")
var rights: Rights,
@SerializedName("season_id")
var seasonId: Long, // 25617
@SerializedName("season_title")
var seasonTitle: String, // 魔法禁书目录 第三季
@SerializedName("seasons")
var seasons: List<Season>,
@SerializedName("section")
var section: List<JsonElement>,
@SerializedName("series")
var series: Series,
@SerializedName("share_url")
var shareUrl: String, // http://m.bilibili.com/bangumi/play/ss25617
@SerializedName("square_cover")
var squareCover: String, // http://i0.hdslb.com/bfs/bangumi/91b29251445f9b808e9c30f34019d3ba4f128d6d.jpg
@SerializedName("stat")
var stat: Stat,
@SerializedName("status")
var status: Int, // 13
@SerializedName("title")
var title: String, // 魔法禁书目录 第三季
@SerializedName("total")
var total: Int, // 0
@SerializedName("type")
var type: Int, // 1
@SerializedName("user_status")
var userStatus: UserStatus
) {
data class Series(
@SerializedName("series_id")
var seriesId: Int, // 621
@SerializedName("series_title")
var seriesTitle: String // 魔法禁书目录
)
data class UserStatus(
@SerializedName("follow")
var follow: Int, // 1
@SerializedName("pay")
var pay: Int, // 0
@SerializedName("progress")
var progress: Progress,
@SerializedName("review")
var review: Review,
@SerializedName("sponsor")
var sponsor: Int, // 0
@SerializedName("vip")
var vip: Int, // 0
@SerializedName("vip_frozen")
var vipFrozen: Int // 0
) {
data class Progress(
@SerializedName("last_ep_id")
var lastEpId: Int, // 250436
@SerializedName("last_ep_index")
var lastEpIndex: String, // 3
@SerializedName("last_time")
var lastTime: Int // 1405
)
data class Review(
@SerializedName("is_open")
var isOpen: Int // 0
)
}
data class Season(
@SerializedName("is_new")
var isNew: Int, // 1
@SerializedName("season_id")
var seasonId: Long, // 25617
@SerializedName("season_title")
var seasonTitle: String // 第三季
)
data class Episode(
@SerializedName("aid")
var aid: Int, // 44389470
@SerializedName("badge")
var badge: String, // 会员
@SerializedName("badge_type")
var badgeType: Int, // 0
@SerializedName("cid")
var cid: Int, // 77725026
@SerializedName("cover")
var cover: String, // http://i0.hdslb.com/bfs/archive/c83ef2a961d8b53d6a30f03e8fb631ea4248fede.jpg
@SerializedName("dimension")
var dimension: Dimension,
@SerializedName("from")
var from: String, // bangumi
@SerializedName("id")
var id: Int, // 250453
@SerializedName("long_title")
var longTitle: String, // 守护的理由
@SerializedName("share_url")
var shareUrl: String, // https://m.bilibili.com/bangumi/play/ep250453
@SerializedName("status")
var status: Int, // 13
@SerializedName("title")
var title: String, // 20
@SerializedName("vid")
var vid: String
) {
data class Dimension(
@SerializedName("height")
var height: Int, // 1080
@SerializedName("rotate")
var rotate: Int, // 0
@SerializedName("width")
var width: Int // 1920
)
}
data class Rating(
@SerializedName("count")
var count: Int, // 32318
@SerializedName("score")
var score: Double // 7.8
)
data class Rights(
@SerializedName("allow_bp")
var allowBp: Int, // 0
@SerializedName("allow_download")
var allowDownload: Int, // 0
@SerializedName("allow_review")
var allowReview: Int, // 1
@SerializedName("area_limit")
var areaLimit: Int, // 0
@SerializedName("ban_area_show")
var banAreaShow: Int, // 1
@SerializedName("copyright")
var copyright: String, // bilibili
@SerializedName("is_preview")
var isPreview: Int, // 1
@SerializedName("watch_platform")
var watchPlatform: Int // 0
)
data class NewEp(
@SerializedName("desc")
var desc: String, // 连载中, 每周五22:30更新
@SerializedName("id")
var id: Int, // 250453
@SerializedName("is_new")
var isNew: Int, // 1
@SerializedName("title")
var title: String // 20
)
data class Payment(
@SerializedName("dialog")
var dialog: Dialog,
@SerializedName("pay_tip")
var payTip: PayTip,
@SerializedName("pay_type")
var payType: PayType,
@SerializedName("price")
var price: String, // 0.0
@SerializedName("vip_promotion")
var vipPromotion: String
) {
data class Dialog(
@SerializedName("btn_right")
var btnRight: BtnRight,
@SerializedName("desc")
var desc: String,
@SerializedName("title")
var title: String // 开通大会员抢先看
) {
data class BtnRight(
@SerializedName("title")
var title: String, // 成为大会员
@SerializedName("type")
var type: String // vip
)
}
data class PayType(
@SerializedName("allow_ticket")
var allowTicket: Int // 0
)
data class PayTip(
@SerializedName("primary")
var primary: Primary
) {
data class Primary(
@SerializedName("sub_title")
var subTitle: String,
@SerializedName("title")
var title: String, // 开通大会员抢先看
@SerializedName("type")
var type: Int, // 1
@SerializedName("url")
var url: String
)
}
}
data class Stat(
@SerializedName("coins")
var coins: Long, // 333230
@SerializedName("danmakus")
var danmakus: Int, // 729836
@SerializedName("favorites")
var favorites: Int, // 2300833
@SerializedName("reply")
var reply: Int, // 324057
@SerializedName("share")
var share: Int, // 18362
@SerializedName("views")
var views: Int // 41392306
)
data class Publish(
@SerializedName("is_finish")
var isFinish: Int, // 0
@SerializedName("is_started")
var isStarted: Int, // 1
@SerializedName("pub_time")
var pubTime: String, // 2018-10-05 22:30:00
@SerializedName("pub_time_show")
var pubTimeShow: String, // 10月05日22:30
@SerializedName("weekday")
var weekday: Int // 0
)
data class Paster(
@SerializedName("aid")
var aid: Long, // 0
@SerializedName("allow_jump")
var allowJump: Int, // 0
@SerializedName("cid")
var cid: Long, // 0
@SerializedName("duration")
var duration: Int, // 0
@SerializedName("type")
var type: Int, // 0
@SerializedName("url")
var url: String
)
}
}

View File

@@ -1,20 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class SendDanmakuResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
@Suppress("SpellCheckingInspection")
data class Data(
@SerializedName("dmid")
var dmid: Long // 12699467350278148
)
}

View File

@@ -1,33 +0,0 @@
package com.hiczp.bilibili.api.main.model
import com.google.gson.annotations.SerializedName
data class SendReplyResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("dialog")
var dialog: Long, // 0
@SerializedName("dialog_str")
var dialogStr: String, // 0
@SerializedName("parent")
var parent: Long, // 0
@SerializedName("parent_str")
var parentStr: String, // 0
@SerializedName("root")
var root: Long, // 0
@SerializedName("root_str")
var rootStr: String, // 0
@SerializedName("rpid")
var rpid: Long, // 1422858564
@SerializedName("rpid_str")
var rpidStr: String // 1422858564
)
}

View File

@@ -1,18 +0,0 @@
package com.hiczp.bilibili.api.member
import com.hiczp.bilibili.api.member.model.Pre
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
/**
* 创作中心
*/
@Suppress("DeferredIsResult")
interface MemberAPI {
/**
* 刚登陆时会访问该 API, 使用返回的 url 来创建一个指向一系列 H5 页面的 ListView
* 侧拉抽屉 -> 创作中心 -> 更多功能
*/
@GET("/x/app/pre")
fun pre(): Deferred<Pre>
}

View File

@@ -1,78 +0,0 @@
package com.hiczp.bilibili.api.member.model
import com.google.gson.annotations.SerializedName
data class Pre(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("academy")
var academy: Academy,
@SerializedName("act")
var act: Act,
@SerializedName("creative")
var creative: Creative,
@SerializedName("entrance")
var entrance: Entrance
) {
data class Entrance(
@SerializedName("guidance")
var guidance: String, // 投稿
@SerializedName("show")
var show: Int // 1
)
data class Creative(
@SerializedName("portal_list")
var portalList: List<Portal>,
@SerializedName("show")
var show: Int // 1
) {
data class Portal(
@SerializedName("icon")
var icon: String, // http://i0.hdslb.com/bfs/archive/31f485451e415bbbc59407f1fddce8f317db6287.png
@SerializedName("id")
var id: Int, // 0
@SerializedName("more")
var more: Int, // 0
@SerializedName("mtime")
var mtime: Int, // 1547466050
@SerializedName("new")
var new: Int, // 1
@SerializedName("position")
var position: Int, // 8
@SerializedName("subtitle")
var subtitle: String,
@SerializedName("title")
var title: String, // 更多功能
@SerializedName("url")
var url: String // activity://uper/user_center/more_portal
)
}
data class Act(
@SerializedName("show")
var show: Int, // 1
@SerializedName("title")
var title: String, // 热门活动
@SerializedName("url")
var url: String // https://www.bilibili.com/blackboard/x/activity-tougao-h5/all
)
data class Academy(
@SerializedName("show")
var show: Int, // 0
@SerializedName("title")
var title: String,
@SerializedName("url")
var url: String
)
}
}

View File

@@ -1,26 +0,0 @@
package com.hiczp.bilibili.api.message
import com.hiczp.bilibili.api.message.model.NotifyCount
import com.hiczp.bilibili.api.message.model.UplmList
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
/**
* 消息推送有关的接口
*/
@Suppress("DeferredIsResult")
interface MessageAPI {
/**
* 获取消息数量
* 首页 -> 右上角 talk 图标
*/
@GET("/api/notify/query.notify.count.do")
fun queryNotifyCount(): Deferred<NotifyCount>
/**
* 荣誉周报
*/
@Suppress("SpellCheckingInspection")
@GET("/api/notify/get.uplm.list.do")
fun getUplmList(): Deferred<UplmList>
}

View File

@@ -1,38 +0,0 @@
package com.hiczp.bilibili.api.message.model
import com.google.gson.annotations.SerializedName
data class NotifyCount(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String,
@SerializedName("msg")
var msg: String
) {
data class Data(
@SerializedName("_gt_")
var gt: Int, // 0
/**
* \@我
*/
@SerializedName("at_me")
var atMe: Int, // 0
@SerializedName("notify_me")
var notifyMe: Int, // 9
/**
* 收到的赞
*/
@SerializedName("praise_me")
var praiseMe: Int, // 0
/**
* 回复我的
*/
@SerializedName("reply_me")
var replyMe: Int, // 0
@SerializedName("up")
var up: Int // 0
)
}

View File

@@ -1,27 +0,0 @@
package com.hiczp.bilibili.api.message.model
import com.google.gson.annotations.SerializedName
data class UplmList(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String,
@SerializedName("msg")
var msg: String
) {
data class Data(
@SerializedName("_gt_")
var gt: Int, // 0
@SerializedName("id")
var id: Int, // 2429173
@SerializedName("time")
var time: String, // 2018-11-25 19:29:22
@SerializedName("title")
var title: String, // 叮!你有一份荣誉周报待查收!
@SerializedName("unread")
var unread: Int // 0
)
}

View File

@@ -1,78 +0,0 @@
package com.hiczp.bilibili.api.passport
import com.hiczp.bilibili.api.passport.model.GetKeyResponse
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.passport.model.OAuth2Info
import com.hiczp.bilibili.api.retrofit.CommonResponse
import kotlinx.coroutines.Deferred
import retrofit2.http.*
import java.util.*
/**
* 用户鉴权相关的接口
*/
@Suppress("DeferredIsResult")
interface PassportAPI {
@POST("/api/oauth2/getKey")
fun getKey(): Deferred<GetKeyResponse>
/**
* 多次错误的登陆尝试后, 服务器将返回 {"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}
*/
@Suppress("SpellCheckingInspection")
@POST("/api/v3/oauth2/login")
@FormUrlEncoded
fun login(
@Field("username") username: String, @Field("password") password: String,
//以下为极验所需字段
@Field("challenge") challenge: String? = null,
@Field("seccode") secCode: String? = null,
@Field("validate") validate: String? = null
): Deferred<LoginResponse>
/**
* 除了 accessToken, 其他全部都是 cookie 的值
*/
@Suppress("SpellCheckingInspection")
@POST("/api/v2/oauth2/revoke")
@FormUrlEncoded
fun revoke(
@Field("DedeUserID") dedeUserId: String? = null,
@Field("DedeUserID__ckMd5") ckMd5: String? = null,
@Field("SESSDATA") sessData: String? = null,
@Field("access_token") accessToken: String,
@Field("bili_jct") biliJct: String? = null,
@Field("sid") sid: String? = null
): Deferred<CommonResponse>
/**
* 将所有 cookie 以 Map 形式传入
*/
@POST("/api/v2/oauth2/revoke")
@FormUrlEncoded
fun revoke(
@FieldMap cookieMap: Map<String, String> = Collections.emptyMap(),
@Field("access_token") accessToken: String
): Deferred<CommonResponse>
/**
* 获取 OAuth2 信息
* 如果未登录会返回 {"message":"user not login","ts":1552319204,"code":-101}
*/
@Suppress("SpellCheckingInspection")
@GET("/api/v2/oauth2/info")
fun info(
@Query("DedeUserID") dedeUserId: String? = null,
@Query("DedeUserID__ckMd5") ckMd5: String? = null,
@Query("SESSDATA") sessData: String? = null,
@Query("access_token") accessToken: String,
@Query("bili_jct") biliJct: String? = null,
@Query("sid") sid: String? = null
): Deferred<OAuth2Info>
@GET("/api/v2/oauth2/info")
fun info(
@QueryMap cookieMap: Map<String, String> = Collections.emptyMap(),
@Query("access_token") accessToken: String
): Deferred<OAuth2Info>
}

View File

@@ -1,21 +0,0 @@
package com.hiczp.bilibili.api.passport.model
import com.google.gson.annotations.SerializedName
data class GetKeyResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String?,
@SerializedName("data")
var `data`: Data,
@SerializedName("ts")
var ts: Long // 1550219688
) {
data class Data(
@SerializedName("hash")
var hash: String, // 93ac6f60b4789952
@SerializedName("key")
var key: String // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdScM09sZJqFPX7bvmB2y6i08JbHsa0v4THafPbJN9NoaZ9Djz1LmeLkVlmWx1DwgHVW+K7LVWT5FV3johacVRuV9837+RNntEK6SE82MPcl7fA++dmW2cLlAjsIIkrX+aIvvSGCuUfcWpWFy3YVDqhuHrNDjdNcaefJIQHMW+sQIDAQAB-----END PUBLIC KEY-----
)
}

View File

@@ -1,59 +0,0 @@
package com.hiczp.bilibili.api.passport.model
import com.google.gson.annotations.SerializedName
import java.io.Serializable
data class LoginResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("message")
var message: String?,
@SerializedName("data")
var `data`: Data,
@SerializedName("ts")
var ts: Long // 1550219689
) : Serializable {
data class Data(
@SerializedName("cookie_info")
var cookieInfo: CookieInfo,
@SerializedName("sso")
var sso: List<String>,
@SerializedName("status")
var status: Int, // 0
@SerializedName("token_info")
var tokenInfo: TokenInfo
) : Serializable {
data class CookieInfo(
@SerializedName("cookies")
var cookies: List<Cookie>,
@SerializedName("domains")
var domains: List<String>
) : Serializable {
data class Cookie(
@SerializedName("expires")
var expires: Long, // 1552811689
@SerializedName("http_only")
var httpOnly: Int, // 1
@SerializedName("name")
var name: String, // SESSDATA
@SerializedName("value")
var value: String // 5ff9ba24%2C1552811689%2C04ae9421
) : Serializable
}
data class TokenInfo(
@SerializedName("access_token")
var accessToken: String, // fd0303ff75a6ec6b452c28f4d8621021
@SerializedName("expires_in")
var expiresIn: Long, // 2592000
@SerializedName("mid")
var mid: Long, // 20293030
@SerializedName("refresh_token")
var refreshToken: String // 6a333ebded3c3dbdde65d136b3190d21
) : Serializable
}
//快捷方式
val userId get() = data.tokenInfo.mid
val token get() = data.tokenInfo.accessToken
}

View File

@@ -1,21 +0,0 @@
package com.hiczp.bilibili.api.passport.model
import com.google.gson.annotations.SerializedName
data class OAuth2Info(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("ts")
var ts: Long // 1551865482
) {
data class Data(
@SerializedName("access_token")
var accessToken: String, // 813b20ea7f229795eba7bd31608e3621
@SerializedName("expires_in")
var expiresIn: Long, // 1797151
@SerializedName("mid")
var mid: Long // 20293030
)
}

View File

@@ -1,69 +0,0 @@
package com.hiczp.bilibili.api.player
import com.hiczp.bilibili.api.md5
import com.hiczp.bilibili.api.player.model.BangumiPlayUrl
import com.hiczp.bilibili.api.player.model.VideoPlayUrl
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
import retrofit2.http.Query
import java.lang.management.ManagementFactory
/**
* 这里是播放器会访问的 API
* 返回内容中会有多个视频下载地址, 他们代表不同的视频质量(音频同理)
* 音频和视频是分开的
* 下载视频得到的是一个 m4s 文件, 但是实际上是完整的视频(例如整个番剧而非片段)
* 下载音频得到的也是一个 m4s 文件, 也是完整的
* 将视频和音频合在一起, 就可以播放了
*/
@Suppress("DeferredIsResult", "SpellCheckingInspection")
interface PlayerAPI {
/**
* 获得视频的播放地址
* 这个 API 需要使用特别的 appKey
*
* @param cid 在获取视频详情页面的接口的返回值里
* @param aid 视频的唯一标识
*
* @see com.hiczp.bilibili.api.app.AppAPI.view
*/
@GET(videoPlayUrl)
fun videoPlayUrl(
@Query("force_host") forceHost: Int = 0,
@Query("fnval") fnVal: Int = 16,
@Query("qn") qn: Int = 32,
@Query("npcybs") npcybs: Int = 0,
@Query("cid") cid: Long,
@Query("fnver") fnVer: Int = 0,
@Query("aid") aid: Long
): Deferred<VideoPlayUrl>
/**
* 获得番剧的播放地址
*
* @param aid 番剧的唯一标识
* @param cid 在番剧详情页的返回值里
* @param seasonType 分季类型, 不明确, 似乎总为 1
* @param session 其值为 系统已运行时间(ms)的MD5值, 此处的默认值为 JVM 已启动时间, 在 Android 上请使用 SystemClock
* @param trackPath 不明确
*
* @see com.hiczp.bilibili.api.main.MainAPI.season
*/
@GET("https://api.bilibili.com/pgc/player/api/playurl")
fun bangumiPlayUrl(
@Query("aid") aid: Long,
@Query("cid") cid: Long,
@Query("fnval") fnVal: Int = 16,
@Query("fnver") fnVer: Int = 0,
@Query("module") module: String = "bangumi",
@Query("npcybs") npcybs: Int = 0,
@Query("qn") qn: Int = 32,
@Query("season_type") seasonType: Int = 1,
@Query("session") session: String = (System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().startTime).toString().md5(),
@Query("track_path") trackPath: Int? = null
): Deferred<BangumiPlayUrl>
companion object {
const val videoPlayUrl = "https://app.bilibili.com/x/playurl"
}
}

View File

@@ -1,84 +0,0 @@
package com.hiczp.bilibili.api.player
import com.hiczp.bilibili.api.BilibiliClientProperties
import com.hiczp.bilibili.api.calculateSign
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.retrofit.Charsets.UTF_8
import com.hiczp.bilibili.api.retrofit.Header
import com.hiczp.bilibili.api.retrofit.Param
import okhttp3.Interceptor
import okhttp3.Response
import java.net.URLEncoder
import java.time.Instant
/**
* PlayerAPI 专用的拦截器
*
* @see PlayerAPI
*/
class PlayerInterceptor(
private val bilibiliClientProperties: BilibiliClientProperties,
private val loginResponseExpression: () -> LoginResponse?
) : Interceptor {
@Suppress("SpellCheckingInspection")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
//添加 header
val header = request.headers().newBuilder().apply {
add(Header.ACCEPT, "*/*")
add(Header.USER_AGENT, "Bilibili Freedoooooom/MarkII")
add(Header.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8")
}.build()
//添加 Query Params
val oldUrl = request.url()
//如果是视频播放地址这个 API, 要用特殊的 appKey
val isVideo = oldUrl.toString().startsWith(PlayerAPI.videoPlayUrl)
val url = StringBuilder(oldUrl.encodedQuery() ?: "").apply {
//appKey
addParamEncode(Param.APP_KEY, if (isVideo) bilibiliClientProperties.videoAppKey else bilibiliClientProperties.appKey)
//凭证有关
val loginRespons = loginResponseExpression()
if (loginRespons != null) {
//expire 的值为 token过期时间+2s
addParamEncode(Param.EXPIRE, (loginRespons.ts + loginRespons.data.tokenInfo.expiresIn + 2).toString())
addParamEncode(Param.ACCESS_KEY, loginRespons.token)
addParamEncode(Param.MID, loginRespons.userId.toString())
} else {
addParamEncode(Param.EXPIRE, "0")
addParamEncode(Param.MID, "0")
}
//公共参数
addParamEncode(Param.DEVICE, bilibiliClientProperties.platform)
addParamEncode(Param.MOBILE_APP, bilibiliClientProperties.platform)
addParamEncode(Param.PLATFORM, bilibiliClientProperties.platform)
addParamEncode("otype", "json")
addParamEncode(Param.TIMESTAMP, Instant.now().epochSecond.toString())
addParamEncode(Param.BUILD, bilibiliClientProperties.build)
addParamEncode(Param.BUILD_VERSION_ID, bilibiliClientProperties.buildVersionId)
}.toString().let {
//排序
val sortedEncodedQuery = it.split('&').sorted().joinToString(separator = "&")
//添加 sign
val sign = calculateSign(sortedEncodedQuery, if (isVideo) bilibiliClientProperties.videoAppSecret else bilibiliClientProperties.appSecret)
"$sortedEncodedQuery&${Param.SIGN}=$sign"
}.let {
oldUrl.newBuilder().encodedQuery(it).build()
}
return chain.proceed(
request.newBuilder()
.headers(header)
.url(url)
.build()
)
}
}
private fun StringBuilder.addParamEncode(name: String, value: String) {
if (length != 0) append('&')
append(name)
append('=')
append(URLEncoder.encode(value, UTF_8))
}

View File

@@ -1,89 +0,0 @@
package com.hiczp.bilibili.api.player.model
import com.google.gson.annotations.SerializedName
data class BangumiPlayUrl(
@SerializedName("accept_description")
var acceptDescription: List<String>,
@SerializedName("accept_format")
var acceptFormat: String, // hdflv2,flv,flv720,flv480,mp4
@SerializedName("accept_quality")
var acceptQuality: List<Int>,
@SerializedName("bp")
var bp: Int, // 0
@SerializedName("code")
var code: Int, // 0
@SerializedName("dash")
var dash: Dash,
@SerializedName("fnval")
var fnval: Int, // 16
@SerializedName("fnver")
var fnver: Int, // 0
@SerializedName("format")
var format: String, // flv480
@SerializedName("from")
var from: String, // local
@SerializedName("has_paid")
var hasPaid: Boolean, // false
@SerializedName("is_preview")
var isPreview: Int, // 0
@SerializedName("quality")
var quality: Int, // 32
@SerializedName("result")
var result: String, // suee
@SerializedName("seek_param")
var seekParam: String, // start
@SerializedName("seek_type")
var seekType: String, // offset
@SerializedName("status")
var status: Int, // 2
@SerializedName("timelength")
var timelength: Long, // 1420201
@SerializedName("video_codecid")
var videoCodecid: Int, // 7
@SerializedName("video_project")
var videoProject: Boolean, // true
@SerializedName("vip_status")
var vipStatus: Int, // 0
@SerializedName("vip_type")
var vipType: Int // 0
) {
data class Dash(
@SerializedName("audio")
var audio: List<Audio>,
@SerializedName("video")
var video: List<Video>
) {
data class Video(
@SerializedName("backupUrl")
var backupUrl: List<String>,
@SerializedName("backup_url")
var backup_url: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 379067
@SerializedName("baseUrl")
var baseUrl: String, // http://60.12.119.70/upgcxcode/28/12/74921228/74921228-1-30016.m4s?expires=1550754300&platform=android&ssig=rJUT9lWneFYshCT4p_3YuA&oi=1699214834&trid=83334c4981ed460ea2444f6aab79d1b6&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("base_url")
var base_url: String, // http://60.12.119.70/upgcxcode/28/12/74921228/74921228-1-30016.m4s?expires=1550754300&platform=android&ssig=rJUT9lWneFYshCT4p_3YuA&oi=1699214834&trid=83334c4981ed460ea2444f6aab79d1b6&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("codecid")
var codecid: Int, // 7
@SerializedName("id")
var id: Int // 16
)
data class Audio(
@SerializedName("backupUrl")
var backupUrl: List<String>,
@SerializedName("backup_url")
var backup_url: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 193680
@SerializedName("baseUrl")
var baseUrl: String, // http://60.12.119.68/upgcxcode/28/12/74921228/74921228-1-30280.m4s?expires=1550754300&platform=android&ssig=1-JiBSZvopajZNgVZ3HRdA&oi=1699214834&trid=83334c4981ed460ea2444f6aab79d1b6&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("base_url")
var base_url: String, // http://60.12.119.68/upgcxcode/28/12/74921228/74921228-1-30280.m4s?expires=1550754300&platform=android&ssig=1-JiBSZvopajZNgVZ3HRdA&oi=1699214834&trid=83334c4981ed460ea2444f6aab79d1b6&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("id")
var id: Int // 30280
)
}
}

View File

@@ -1,80 +0,0 @@
package com.hiczp.bilibili.api.player.model
import com.google.gson.annotations.SerializedName
data class VideoPlayUrl(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("accept_description")
var acceptDescription: List<String>,
@SerializedName("accept_format")
var acceptFormat: String, // flv_p60,flv720_p60,flv,flv720,flv480,flv360
@SerializedName("accept_quality")
var acceptQuality: List<Int>,
@SerializedName("dash")
var dash: Dash,
@SerializedName("fnval")
var fnval: Int, // 16
@SerializedName("fnver")
var fnver: Int, // 0
@SerializedName("format")
var format: String, // flv480
@SerializedName("from")
var from: String, // local
@SerializedName("quality")
var quality: Int, // 32
@SerializedName("result")
var result: String, // suee
@SerializedName("seek_param")
var seekParam: String, // start
@SerializedName("seek_type")
var seekType: String, // offset
@SerializedName("timelength")
var timelength: Long, // 196367
@SerializedName("video_codecid")
var videoCodecid: Int, // 7
@SerializedName("video_project")
var videoProject: Boolean // true
) {
data class Dash(
@SerializedName("audio")
var audio: List<Audio>,
@SerializedName("video")
var video: List<Video>
) {
data class Audio(
@SerializedName("backup_url")
var backupUrl: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 191246
@SerializedName("base_url")
var baseUrl: String, // http://101.75.242.10/upgcxcode/41/36/72913641/72913641-1-30280.m4s?expires=1550754000&platform=android&ssig=2eirz02lIhKUw--w26lpqQ&oi=1699214834&trid=e0d3ad6245d8432887eb12b71f29bb3e&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("codecid")
var codecid: Int, // 0
@SerializedName("id")
var id: Int // 30280
)
data class Video(
@SerializedName("backup_url")
var backupUrl: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 288340
@SerializedName("base_url")
var baseUrl: String, // http://60.12.119.68/upgcxcode/41/36/72913641/72913641-1-30011.m4s?expires=1550754000&platform=android&ssig=Ven-c2XaxfkQIoMkzuq7MQ&oi=1699214834&trid=e0d3ad6245d8432887eb12b71f29bb3e&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("codecid")
var codecid: Int, // 12
@SerializedName("id")
var id: Int // 16
)
}
}
}

View File

@@ -1,36 +0,0 @@
package com.hiczp.bilibili.api.retrofit
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
/**
* 通用实体, 可表示无 data 的响应 或 错误响应
* code 为 0 表示正常响应, 此时 message 为 null
* code 不为 0 表示错误响应, 此时 data 可能是各种类型
* 一些 API 同时有 msg 和 message
*/
data class CommonResponse(
@SerializedName("code")
var code: Int, // 0
@SerializedName("msg")
var msg: String?,
@SerializedName("message")
var message: String?,
@SerializedName("ts")
var timestamp: Long, // 1550546539
/**
* data 可能是各种类型, 例如 array, object, string
*/
@SerializedName("data")
var data: JsonElement?,
/**
* ttl, 不明确含义, 如果存在则值总为 1
*/
@SerializedName("ttl")
var ttl: Int?
)

View File

@@ -1,62 +0,0 @@
package com.hiczp.bilibili.api.retrofit
//该文件用于防止拼写错误
object Method {
const val GET = "GET"
const val POST = "POST"
const val PATCH = "PATCH"
const val PUT = "PUT"
const val DELETE = "DELETE"
const val OPTION = "OPTION"
}
object Header {
const val DISPLAY_ID = "Display-ID"
@Suppress("SpellCheckingInspection")
const val BUILD_VERSION_ID = "Buvid"
const val DEVICE_ID = "Device-ID"
const val USER_AGENT = "User-Agent"
const val ACCEPT = "Accept"
const val ACCEPT_LANGUAGE = "Accept-Language"
const val ACCEPT_ENCODING = "Accept-Encoding"
//强制公共参数添加位置
const val FORCE_PARAM = "Retrofit-Force-Param"
const val FORCE_PARAM_QUERY = "query"
@Suppress("MemberVisibilityCanBePrivate")
const val FORCE_PARAM_FORM_BODY = "formBody"
const val FORCE_QUERY = "$FORCE_PARAM: $FORCE_PARAM_QUERY"
const val FORCE_FORM_BODY = "$FORCE_PARAM: $FORCE_PARAM_FORM_BODY"
}
object Param {
const val ACCESS_KEY = "access_key"
@Suppress("SpellCheckingInspection")
const val APP_KEY = "appkey"
const val ACTION_KEY = "actionKey"
const val BUILD = "build"
@Suppress("SpellCheckingInspection")
const val BUILD_VERSION_ID = "buvid"
const val CHANNEL = "channel"
@Suppress("ObjectPropertyName")
const val _DEVICE = "_device"
const val DEVICE = "device"
@Suppress("ObjectPropertyName", "SpellCheckingInspection")
const val _HARDWARE_ID = "_hwid"
const val SOURCE = "src"
const val TRACE_ID = "trace_id"
const val USER_ID = "uid"
const val VERSION = "version"
@Suppress("SpellCheckingInspection")
const val MOBILE_APP = "mobi_app"
const val PLATFORM = "platform"
const val TIMESTAMP = "ts"
const val EXPIRE = "expire"
const val MID = "mid"
const val SIGN = "sign"
}
internal object Charsets {
const val UTF_8 = "UTF-8"
}

View File

@@ -1,54 +0,0 @@
package com.hiczp.bilibili.api.retrofit
import okhttp3.FormBody
inline fun FormBody.forEach(block: (String, String) -> Unit) {
repeat(size()) {
block(encodedName(it), encodedValue(it))
}
}
fun FormBody.raw() =
StringBuilder().apply {
repeat(size()) {
if (it != 0) append('&')
append(encodedName(it))
append('=')
append(encodedValue(it))
}
}.toString()
fun FormBody.sortedRaw(): String {
val nameAndValue = ArrayList<String>()
repeat(size()) {
nameAndValue.add("${encodedName(it)}=${encodedValue(it)}")
}
return nameAndValue.sorted().joinToString(separator = "&")
}
fun FormBody.containsEncodedName(name: String): Boolean {
repeat(size()) {
if (encodedName(it) == name) return true
}
return false
}
fun FormBody.Builder.addAllEncoded(formBody: FormBody): FormBody.Builder {
with(formBody) {
repeat(size()) {
addEncoded(encodedName(it), encodedValue(it))
}
}
return this
}
internal typealias ParamExpression = Pair<String, () -> String?>
internal inline fun Array<out ParamExpression>.forEachNonNull(action: (String, String) -> Unit) {
forEach { (name, valueExpression) ->
val value = valueExpression()
if (value != null) {
action(name, value)
}
}
}

View File

@@ -1,11 +0,0 @@
package com.hiczp.bilibili.api.retrofit.exception
import com.hiczp.bilibili.api.retrofit.CommonResponse
import java.io.IOException
/**
* 当服务器返回的 code 不等于 0 时抛出
*/
class BilibiliApiException(
commonResponse: CommonResponse
) : IOException(commonResponse.message?.takeIf { it.isNotEmpty() } ?: commonResponse.msg)

View File

@@ -1,22 +0,0 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.hiczp.bilibili.api.retrofit.ParamExpression
import com.hiczp.bilibili.api.retrofit.forEachNonNull
import okhttp3.Interceptor
import okhttp3.Response
/**
* 为请求添加公共 Header
*
* @param additionHeaders HeaderName to HeaderValueExpression
*/
class CommonHeaderInterceptor(private vararg val additionHeaders: ParamExpression) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder().apply {
additionHeaders.forEachNonNull { name, value ->
addHeader(name, value)
}
}.build()
return chain.proceed(request)
}
}

View File

@@ -1,76 +0,0 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.hiczp.bilibili.api.retrofit.*
import mu.KotlinLogging
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
private val logger = KotlinLogging.logger {}
/**
* 为请求添加公共参数
* 如果请求为 GET 方式则添加到 Query, 如果是其他其他方式则尝试添加到 BODY.
*
* @param additionParams ParamName to ParamValueExpression
*/
class CommonParamInterceptor(private vararg val additionParams: ParamExpression) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var headers = request.headers()
var httpUrl = request.url()
var body = request.body()
//是否强制加到 Query(暂不存在强制加到 FormBody 的情况)
var forceQuery = false
val forceParam = headers[Header.FORCE_PARAM]
if (forceParam != null) {
if (forceParam == Header.FORCE_PARAM_QUERY) forceQuery = true
headers = headers.newBuilder().removeAll(Header.FORCE_PARAM).build()
}
when {
//如果是 GET 则添加到 Query
request.method() == Method.GET || forceQuery -> {
httpUrl = request.url().newBuilder().apply {
additionParams.forEachNonNull { name, value ->
addQueryParameter(name, value)
}
}.build()
}
//如果 Body 不存在或者为空则创建一个 FormBody
body == null || body.contentLength() == 0L -> {
body = FormBody.Builder().apply {
additionParams.forEachNonNull { name, value ->
add(name, value)
}
}.build()
}
//如果 Body 为 FormBody 则里面可能已经存在内容
body is FormBody -> {
body = FormBody.Builder().addAllEncoded(body).apply {
additionParams.forEachNonNull { name, value ->
add(name, value)
}
}.build()
}
//如果方式不为 GET 且 Body 不为空或者为 FormBody 则无法添加公共参数
else -> {
logger.error {
"Cannot add params to request: ${request.method()} ${request.url()} ${body.javaClass.simpleName}"
}
}
}
return chain.proceed(
request.newBuilder()
.headers(headers)
.url(httpUrl)
.method(request.method(), body)
.build()
)
}
}

View File

@@ -1,52 +0,0 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.obj
import com.hiczp.bilibili.api.gson
import com.hiczp.bilibili.api.jsonParser
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
import okhttp3.Interceptor
import okhttp3.Response
/**
* 如果服务器返回的 code 不为 0 则抛出异常
*/
object FailureResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val body = response.body()
if (!response.isSuccessful || body == null || body.contentLength() == 0L) return response
//获取字符集
val contentType = body.contentType()
val charset = if (contentType == null) {
Charsets.UTF_8
} else {
contentType.charset(Charsets.UTF_8)!!
}
//拷贝流
val inputStreamReader = body.source().also {
it.request(Long.MAX_VALUE)
}.buffer.clone().inputStream().reader(charset)
//读取其内容
val jsonObject = try {
jsonParser.parse(inputStreamReader).obj
} catch (exception: Exception) {
//如果返回内容解析失败, 说明它不是一个合法的 json
//如果在拦截器抛出 MalformedJsonException 会导致 Retrofit 的异步请求一直卡着直到超时
return response
} finally {
inputStreamReader.close()
}
//判断 code 是否为 0
if (jsonObject["code"].int != 0) {
throw BilibiliApiException(gson.fromJson(jsonObject))
}
return response
}
}

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