读取评论的接口

This commit is contained in:
czp3009 2019-02-21 18:59:53 +08:00
parent 075d880f53
commit fa906093a6
14 changed files with 1970 additions and 169 deletions

123
README.md
View File

@ -1,83 +1,108 @@
# Bilibili API JVM 调用库
该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
# 技术说明
`BilibiliClient` 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.
所有调用从这个类开始, 包括登陆以及访问其他各种 API.
使用协程来实现异步, 由于 [kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 为编译器实现, 因此并非所有 JVM 语言都能正确调用 `suspend` 方法.
本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.
`BilibiliClient` 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 `BilibiliClient` 实例, 而应该保存其引用.
一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:
```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` 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.
# 登录和登出
https://passport.bilibili.com
(Bilibili oauth2 v3)
登陆和登出均为异步方法, 需要在协程上下文中执行.
runBlocking {
BilibiliClient().run {
login(username, password)
logout()
}
```kotlin
runBlocking {
BilibiliClient().run {
login(username, password)
logout()
}
}
```
`login` 方法返回一个 `LoginResponse` 实例, 下次可以直接赋值到没有登陆的 `BilibiliClient` 实例中来恢复登陆状态.
BilibiliClient().apply {
this.loginResponse = loginResponse
}
```kotlin
BilibiliClient().apply {
this.loginResponse = loginResponse
}
```
`LoginResponse` 继承 `Serializable`, 可被序列化.
`LoginResponse` 继承 `Serializable`, 可被序列化(JVM 序列化).
如果登录操作失败, 将抛出 `BilibiliApiException`(只要服务器返回的 code 不为 0 都将抛出异常), 通过以下代码获取服务器返回的 `code`
可能的错误返回有两种:
val code = bilibiliApiException.commonResponse.code
-629 用户名与密码不匹配
-105 验证码错误
在登陆操作中, 如果服务器返回 `-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` 将打开一个极验弹窗, 通过验证码后再次调用登陆接口:
自行访问 `commonResponse.data.obj.url.string` 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:
login(username, password, challenge, secCode, validate)
```kotlin
login(username, password, challenge, secCode, validate)
```
`challenge` 为本次极验的唯一标识
`challenge` 为本次极验的唯一标识(在一开始给出的 url 中)
`validate` 为极验返回值
`secCode``"$validate|jordan"`
(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误.
登陆后, 可以访问全部 API.
# message
https://message.bilibili.com
# 访问 API
通常的 API 访问是这样的
BilibiliClient().messageAPI
```kotlin
val myInfo = bilibiliClient.appAPI.myInfo().await()
```
消息通知有关的接口.
# app
https://app.bilibili.com
BilibiliClient().appAPI
总站 API. 获取个人信息的完整示例如下:
runBlocking {
val bilibiliClient = BilibiliClient().apply {
login(username, password)
}
val myInfo = bilibiliClient.appAPI.myInfo().await()
println(myInfo)
}
# av
https://api.vc.bilibili.com
BilibiliClient().vcAPI
小视频.
# member
https://member.bilibili.com
BilibiliClient().memberAPI
创作中心.
不要问文档, 用自动补全(心)来感受.
# License
GPL V3

View File

@ -15,10 +15,15 @@ object BaseUrl {
const val message = "https://message.bilibili.com"
/**
* 提供通用功能, 例如获取用户信息
* 主站
*/
const val app = "https://app.bilibili.com"
/**
* 这也是主站
*/
const val main = "https://api.bilibili.com"
/**
* 小视频
*/

View File

@ -1,6 +1,7 @@
package com.hiczp.bilibili.api
import com.hiczp.bilibili.api.app.AppAPI
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
@ -31,13 +32,11 @@ import javax.crypto.Cipher
* 不能严格保证线程安全.
*
* @param billingClientProperties 客户端的固有属性, 是一种常量
* @param autoRefreshToken Token 过期时是否自动重新登录
* @param logLevel 日志打印等级
*/
class BilibiliClient(
@Suppress("MemberVisibilityCanBePrivate")
val billingClientProperties: BilibiliClientProperties = BilibiliClientProperties(),
private val autoRefreshToken: Boolean = true, //TODO 自动 refreshToken
private val logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE
) {
/**
@ -134,6 +133,24 @@ class BilibiliClient(
)
}
/**
* 这也是总站 API
*/
@Suppress("SpellCheckingInspection")
val mainAPI by lazy {
createAPI<MainAPI>(BaseUrl.main,
CommonHeaderInterceptor(
//如果未登陆则没有 Display-ID
"Display-ID" to { userId?.let { "$it-$initTime" } },
"Buvid" to { billingClientProperties.buildVersionId },
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
),
defaultCommonQueryParamInterceptor,
defaultQuerySignInterceptor
)
}
/**
* 小视频相关接口
*/

View File

@ -6,7 +6,7 @@ package com.hiczp.bilibili.api
*/
class BilibiliClientProperties {
/**
* Android 平台的 appKey
* Android 平台的 appKey(该默认值为普通版客户端, 非概念版)
*/
var appKey = "1d8b6e7d45233436"
@ -16,6 +16,18 @@ class BilibiliClientProperties {
@Suppress("SpellCheckingInspection")
var appSecret = "560c52ccd288fed045859ed18bffd973"
/**
* 获取视频播放地址使用的 appKey, 与访问其他 RestFulAPI 所用的 appKey 是不一样的
*/
@Suppress("SpellCheckingInspection")
var videoAppKey = "iVGUTjsxvpLeuDCf"
/**
* 获取视频播放地址所用的 appSecret
*/
@Suppress("SpellCheckingInspection")
var videoAppSecret = "aHRmhWMLkdeMuILqORnYZocwMBpMEOdt"
/**
* 客户端平台
*/

View File

@ -5,13 +5,19 @@ import kotlinx.coroutines.Deferred
import retrofit2.http.GET
import retrofit2.http.Query
import java.time.Instant
import java.util.*
/**
* 总站 API
*/
@Suppress("DeferredIsResult")
interface AppAPI {
/**
* 打开 APP 时将访问此接口来获得 UI 排布顺序
* 包括下方 tab(首页, 频道, 动态, 会员购), 首页的上方 tab(直播, 推荐, 热门, 追番) 以及右上角的 游戏中心, 离线下载, 消息
*/
@GET("/x/resource/show/tab")
fun tab(): Deferred<Tab>
/**
* 登陆完成后将请求一次此接口以获得个人资料
*/
@ -73,8 +79,9 @@ interface AppAPI {
): Deferred<PopularPage>
/**
* 视频页面
* 视频页面(普通视频, 非番剧)
* 包含视频基本信息, 推荐和广告
* 从这个接口得到视频的 cid
*
* @param aid 视频的唯一标识
*/
@ -93,29 +100,29 @@ interface AppAPI {
@Query("trackid") trackId: String? = null //all_10.shylf-ai-recsys-120.1550674524909.237
): Deferred<View>
//TODO 这里的 appkey 变为 iVGUTjsxvpLeuDCf
/**
* 获得视频的播放地址
*
* @param expire 默认为下个月的这一天的时间戳
* @param mid 当前用户 ID
* @param cid view() 接口的返回值里
* @param aid 视频的唯一标识
*/
@Suppress("SpellCheckingInspection")
@GET("/x/playurl")
fun playUrl(
@Query("device") device: String = "android",
@Query("expire") expire: Long = Calendar.getInstance().apply { add(Calendar.MONTH, 1) }.toInstant().epochSecond,
@Query("force_host") forceHost: Int = 0,
@Query("mid") mid: Long? = null,
@Query("fnval") fnVal: Int = 16,
@Query("qn") qn: Int = 32,
@Query("npcybs") npcybs: Int = 0,
@Query("cid") cid: Long? = null,
@Query("otype") otype: String = "json",
@Query("fnver") fnVer: Int = 0,
@Query("buvid") buildVersionId: String? = null,
@Query("aid") aid: Long
): Deferred<PlayUrl>
// //TODO 这里的 appkey 变为 iVGUTjsxvpLeuDCf
// /**
// * 获得视频的播放地址
// *
// * @param expire 默认为下个月的这一天的时间戳
// * @param mid 当前用户 ID
// * @param cid 在 view() 接口的返回值里
// * @param aid 视频的唯一标识
// */
// @Suppress("SpellCheckingInspection")
// @GET("/x/playurl")
// fun playUrl(
// @Query("device") device: String = "android",
// @Query("expire") expire: Long = Calendar.getInstance().apply { add(Calendar.MONTH, 1) }.toInstant().epochSecond,
// @Query("force_host") forceHost: Int = 0,
// @Query("mid") mid: Long? = null,
// @Query("fnval") fnVal: Int = 16,
// @Query("qn") qn: Int = 32,
// @Query("npcybs") npcybs: Int = 0,
// @Query("cid") cid: Long? = null,
// @Query("otype") otype: String = "json",
// @Query("fnver") fnVer: Int = 0,
// @Query("buvid") buildVersionId: String? = null,
// @Query("aid") aid: Long
// ): Deferred<PlayUrl>
}

View File

@ -1,5 +1,6 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class HomePage(
@ -172,9 +173,9 @@ data class HomePage(
@SerializedName("card")
var card: Card,
@SerializedName("click_urls")
var clickUrls: List<Any>,
var clickUrls: List<JsonElement>,
@SerializedName("download_whitelist")
var downloadWhitelist: List<Any>,
var downloadWhitelist: List<JsonElement>,
@SerializedName("open_whitelist")
var openWhitelist: List<String>,
@SerializedName("preload_landingpage")
@ -184,7 +185,7 @@ data class HomePage(
@SerializedName("sales_type")
var salesType: Int, // 12
@SerializedName("show_urls")
var showUrls: List<Any>,
var showUrls: List<JsonElement>,
@SerializedName("special_industry")
var specialIndustry: Boolean, // false
@SerializedName("special_industry_tips")
@ -288,7 +289,7 @@ data class HomePage(
@SerializedName("sales_type")
var salesType: Int, // 31
@SerializedName("show_urls")
var showUrls: List<Any>,
var showUrls: List<JsonElement>,
@SerializedName("special_industry")
var specialIndustry: Boolean, // false
@SerializedName("special_industry_tips")
@ -327,7 +328,7 @@ data class HomePage(
@SerializedName("jump_url")
var jumpUrl: String, // bilibili://game_center/detail?id=80&sourceFrom=782&sourceType=adPut
@SerializedName("report_urls")
var reportUrls: List<Any>,
var reportUrls: List<JsonElement>,
@SerializedName("text")
var text: String,
@SerializedName("type")

View File

@ -55,7 +55,7 @@ data class MyInfo(
data class Vip(
@SerializedName("due_date")
var dueDate: Int, // 0
var dueDate: Long, // 0
@SerializedName("status")
var status: Int, // 0
@SerializedName("type")

View File

@ -1,80 +1,78 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class PlayUrl(
@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, // 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: Int, // 443737
@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 Video(
@SerializedName("backup_url")
var backupUrl: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 980114
@SerializedName("base_url")
var baseUrl: String, // http://112.13.92.195/upgcxcode/86/69/77356986/77356986-1-30064.m4s?expires=1550682900&platform=android&ssig=vLwE2fl303BrUu1wF1grNQ&oi=3670888805&trid=cf1bde09d63149168c0a0a997a3757d8&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("codecid")
var codecid: Int, // 7
@SerializedName("id")
var id: Int // 64
)
data class Audio(
@SerializedName("backup_url")
var backupUrl: List<String>,
@SerializedName("bandwidth")
var bandwidth: Int, // 67125
@SerializedName("base_url")
var baseUrl: String, // http://117.148.189.5/upgcxcode/86/69/77356986/77356986-1-30216.m4s?expires=1550682900&platform=android&ssig=LlSJk_i74xGEjSOwmjUYzA&oi=3670888805&trid=cf1bde09d63149168c0a0a997a3757d8&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
@SerializedName("codecid")
var codecid: Int, // 0
@SerializedName("id")
var id: Int // 30216
)
}
}
}
//data class PlayUrl(
// @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, // 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: Int, // 443737
// @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 Video(
// @SerializedName("backup_url")
// var backupUrl: List<String>,
// @SerializedName("bandwidth")
// var bandwidth: Int, // 980114
// @SerializedName("base_url")
// var baseUrl: String, // http://112.13.92.195/upgcxcode/86/69/77356986/77356986-1-30064.m4s?expires=1550682900&platform=android&ssig=vLwE2fl303BrUu1wF1grNQ&oi=3670888805&trid=cf1bde09d63149168c0a0a997a3757d8&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
// @SerializedName("codecid")
// var codecid: Int, // 7
// @SerializedName("id")
// var id: Int // 64
// )
//
// data class Audio(
// @SerializedName("backup_url")
// var backupUrl: List<String>,
// @SerializedName("bandwidth")
// var bandwidth: Int, // 67125
// @SerializedName("base_url")
// var baseUrl: String, // http://117.148.189.5/upgcxcode/86/69/77356986/77356986-1-30216.m4s?expires=1550682900&platform=android&ssig=LlSJk_i74xGEjSOwmjUYzA&oi=3670888805&trid=cf1bde09d63149168c0a0a997a3757d8&nfb=maPYqpoel5MI3qOUX6YpRA==&nfc=1
// @SerializedName("codecid")
// var codecid: Int, // 0
// @SerializedName("id")
// var id: Int // 30216
// )
// }
// }
//}

View File

@ -0,0 +1,29 @@
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

@ -105,7 +105,7 @@ data class View(
@SerializedName("elec_set")
var elecSet: ElecSet,
@SerializedName("list")
var list: List<Any>,
var list: List<JsonElement>,
@SerializedName("show")
var show: Boolean // true
) {
@ -176,7 +176,7 @@ data class View(
data class OwnerExt(
@SerializedName("assists")
var assists: Any?, // null
var assists: JsonElement?, // null
@SerializedName("fans")
var fans: Int, // 275
@SerializedName("official_verify")
@ -197,7 +197,7 @@ data class View(
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Int, // 0
var vipDueDate: Long, // 0
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
@ -257,7 +257,7 @@ data class View(
@SerializedName("dueRemark")
var dueRemark: String,
@SerializedName("vipDueDate")
var vipDueDate: Int, // 0
var vipDueDate: Long, // 0
@SerializedName("vipStatus")
var vipStatus: Int, // 0
@SerializedName("vipStatusWarn")
@ -379,7 +379,7 @@ data class View(
@SerializedName("real_name")
var realName: Boolean, // false
@SerializedName("subtitles")
var subtitles: Any? // null
var subtitles: JsonElement? // null
)
data class Dimension(

View File

@ -0,0 +1,51 @@
package com.hiczp.bilibili.api.main
import com.hiczp.bilibili.api.main.model.ChildReply
import com.hiczp.bilibili.api.main.model.Reply
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
import retrofit2.http.Query
/**
* 这也是总站 API
*/
@Suppress("DeferredIsResult")
interface MainAPI {
/**
* 获取一个视频下的评论
* 注意, 评论是倒序排序的, 即楼层大的楼排在前面, 所以返回值中的 next 会比 prev
* 返回值中的 rpid 为评论 id. parent 为父评论的 id, parent 0 的是顶级评论
*
* @param oid 就是 aid, 视频的唯一标识
* @param pageSize 分页大小
* @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>
}

View File

@ -0,0 +1,402 @@
package com.hiczp.bilibili.api.main.model
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: Int, // 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: Int, // 14363383
@SerializedName("oid")
var oid: Int, // 16622855
@SerializedName("parent")
var parent: Int, // 0
@SerializedName("parent_str")
var parentStr: String, // 0
@SerializedName("rcount")
var rcount: Int, // 2
@SerializedName("replies")
var replies: List<Reply>,
@SerializedName("root")
var root: Int, // 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: Int, // 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: Int, // 14363383
@SerializedName("oid")
var oid: Int, // 16622855
@SerializedName("parent")
var parent: Int, // 1405602348
@SerializedName("parent_str")
var parentStr: String, // 1405602348
@SerializedName("rcount")
var rcount: Int, // 0
@SerializedName("replies")
var replies: List<Any>,
@SerializedName("root")
var root: Int, // 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<Any>,
@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: Any?, // 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: 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 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<Any>,
@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: Any?, // 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: 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 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
)
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
package com.hiczp.bilibili.api.test
import com.hiczp.bilibili.api.BilibiliClient
import kotlinx.coroutines.runBlocking
import okhttp3.logging.HttpLoggingInterceptor
import org.junit.jupiter.api.Test
class FetchReplyTest {
@Test
fun fetchReply() {
runBlocking {
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
.mainAPI.reply(oid = 44154463).await()
}
}
@Test
fun fetchChildReply() {
runBlocking {
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
}
}
}