mirror of
https://github.com/czp3009/bilibili-api.git
synced 2025-02-19 20:50:28 +08:00
读取评论的接口
This commit is contained in:
parent
075d880f53
commit
fa906093a6
123
README.md
123
README.md
@ -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>=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
|
||||
|
@ -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"
|
||||
|
||||
/**
|
||||
* 小视频
|
||||
*/
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 小视频相关接口
|
||||
*/
|
||||
|
@ -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"
|
||||
|
||||
/**
|
||||
* 客户端平台
|
||||
*/
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
29
src/main/kotlin/com/hiczp/bilibili/api/app/model/Tab.kt
Normal file
29
src/main/kotlin/com/hiczp/bilibili/api/app/model/Tab.kt
Normal 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
|
||||
)
|
||||
}
|
@ -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(
|
||||
|
51
src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt
Normal file
51
src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt
Normal 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>
|
||||
}
|
402
src/main/kotlin/com/hiczp/bilibili/api/main/model/ChildReply.kt
Normal file
402
src/main/kotlin/com/hiczp/bilibili/api/main/model/ChildReply.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1230
src/main/kotlin/com/hiczp/bilibili/api/main/model/Reply.kt
Normal file
1230
src/main/kotlin/com/hiczp/bilibili/api/main/model/Reply.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user