
com.hiczp.bilibili.api.BilibiliClient.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bilibili-api Show documentation
Show all versions of bilibili-api Show documentation
Bilibili Android client API library for Kotlin
The newest version!
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(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(BaseUrl.message,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(*defaultCommonParamArray,
Param.ACTION_KEY to { Param.APP_KEY },
"has_up" to { "1" }
)
)
}
/**
* 总站 API
*/
@Suppress("SpellCheckingInspection")
val appAPI by lazy {
createAPI(BaseUrl.app,
defaultCommonHeaderInterceptor,
defaultCommonParamInterceptor
)
}
/**
* 这也是总站 API
*/
@Suppress("SpellCheckingInspection")
val mainAPI by lazy {
createAPI(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(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(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(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 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())
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy