This commit is contained in:
czp3009 2019-02-22 18:29:14 +08:00
parent 1bc0bf61c2
commit 6ed9109b05
13 changed files with 361 additions and 129 deletions

View File

@ -6,6 +6,8 @@ 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.Param
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
@ -86,7 +88,6 @@ class BilibiliClient(
)
private val defaultCommonQueryParamInterceptor = CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray)
private val defaultSortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
/**
* 用户鉴权相关的接口
@ -102,8 +103,7 @@ class BilibiliClient(
"mobi_app" to { billingClientProperties.platform },
"platform" to { billingClientProperties.platform },
"ts" to { Instant.now().epochSecond.toString() }
),
defaultSortAndSignInterceptor
)
)
}
@ -117,8 +117,7 @@ class BilibiliClient(
CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray,
"actionKey" to { "appkey" },
"has_up" to { "1" }
),
defaultSortAndSignInterceptor
)
)
}
@ -129,8 +128,7 @@ class BilibiliClient(
val appAPI by lazy {
createAPI<AppAPI>(BaseUrl.app,
defaultCommonHeaderInterceptor,
defaultCommonQueryParamInterceptor,
defaultSortAndSignInterceptor
defaultCommonQueryParamInterceptor
)
}
@ -147,8 +145,7 @@ class BilibiliClient(
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
),
defaultCommonQueryParamInterceptor,
defaultSortAndSignInterceptor
defaultCommonQueryParamInterceptor
)
}
@ -166,8 +163,7 @@ class BilibiliClient(
"trace_id" to { generateTraceId() },
"uid" to { userId?.toString() },
"version" to { billingClientProperties.version }
),
defaultSortAndSignInterceptor
)
)
}
@ -177,11 +173,31 @@ class BilibiliClient(
val memberAPI by lazy {
createAPI<MemberAPI>(BaseUrl.member,
defaultCommonHeaderInterceptor,
defaultCommonQueryParamInterceptor,
defaultSortAndSignInterceptor
defaultCommonQueryParamInterceptor
)
}
/**
* 播放器所需的 API, 用于获取视频播放地址
*/
val playerAPI: PlayerAPI by lazy {
Retrofit.Builder()
.baseUrl("https://bilibili.com") //这里的 baseUrl 是没用的
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
//TODO functional
addInterceptor(PlayerInterceptor(billingClientProperties, loginResponse))
addInterceptor(FailureResponseInterceptor)
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
}
}.build())
.build()
.create(PlayerAPI::class.java)
}
/**
* 登陆
* v3 登陆接口会同时返回 cookies token
@ -237,6 +253,7 @@ class BilibiliClient(
loginResponse = null
}
private val sortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
private inline fun <reified T : Any> createAPI(
baseUrl: String,
vararg interceptors: Interceptor
@ -248,6 +265,7 @@ class BilibiliClient(
interceptors.forEach {
addInterceptor(it)
}
addInterceptor(sortAndSignInterceptor)
addInterceptor(FailureResponseInterceptor)
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {

View File

@ -99,30 +99,4 @@ interface AppAPI {
@Query("qn") qn: Int = 32,
@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>
}

View File

@ -1,78 +0,0 @@
package com.hiczp.bilibili.api.app.model
//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

@ -31,7 +31,7 @@ data class LoginResponse(
) : Serializable {
data class Cookie(
@SerializedName("expires")
var expires: Int, // 1552811689
var expires: Long, // 1552811689
@SerializedName("http_only")
var httpOnly: Int, // 1
@SerializedName("name")
@ -45,9 +45,9 @@ data class LoginResponse(
@SerializedName("access_token")
var accessToken: String, // fd0303ff75a6ec6b452c28f4d8621021
@SerializedName("expires_in")
var expiresIn: Int, // 2592000
var expiresIn: Long, // 2592000
@SerializedName("mid")
var mid: Int, // 20293030
var mid: Long, // 20293030
@SerializedName("refresh_token")
var refreshToken: String // 6a333ebded3c3dbdde65d136b3190d21
) : Serializable

View File

@ -0,0 +1,67 @@
package com.hiczp.bilibili.api.player
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.util.*
/**
* 这里是播放器会访问的 API
*/
@Suppress("DeferredIsResult", "SpellCheckingInspection")
interface PlayerAPI {
/**
* 获得视频的播放地址
* 这个 API 需要使用特别的 appKey
*
* @param expire 默认为下个月的这一天的时间戳
* @param cid 在获取视频详情页面的接口的返回值里
* @param aid 视频的唯一标识
*
* @see com.hiczp.bilibili.api.app.AppAPI.view
*/
@GET(videoPlayUrl)
fun videoPlayUrl(
@Query("expire") expire: Long = nextMonthTimestamp(),
@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("otype") otype: String = "json",
@Query("fnver") fnVer: Int = 0,
@Query("aid") aid: Long
): Deferred<VideoPlayUrl>
/**
* 获得番剧的播放地址
*
* @param aid 番剧的唯一标识
* @param cid 在番剧详情页的返回值里
* @param seasonType 番剧分季(第一季, 第二季)( 1 开始)
* @param session 不明确其含义
* @param trackPath 不明确
*/
@GET("https://api.bilibili.com/pgc/player/api/playurl")
fun bangumiPlayUrl(
@Query("aid") aid: Long,
@Query("cid") cid: Long,
@Query("expire") expire: Long = nextMonthTimestamp(),
@Query("fnval") fnVal: Int = 16,
@Query("fnver") fnVer: Int = 0,
@Query("module") module: String = "bangumi",
@Query("npcybs") npcybs: Int = 0,
@Query("otype") otype: String = "json",
@Query("qn") qn: Int = 32,
@Query("season_type") seasonType: Int,
@Query("session") session: String? = null,
@Query("track_path") trackPath: Int? = null
): Deferred<BangumiPlayUrl>
companion object {
const val videoPlayUrl = "https://app.bilibili.com/x/playurl"
private fun nextMonthTimestamp() = Calendar.getInstance().apply { add(Calendar.MONTH, 1) }.toInstant().epochSecond
}
}

View File

@ -0,0 +1,57 @@
package com.hiczp.bilibili.api.player
import com.hiczp.bilibili.api.BilibiliClientProperties
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.retrofit.Header
import com.hiczp.bilibili.api.retrofit.Param
import okhttp3.Interceptor
import okhttp3.Response
import java.time.Instant
/**
* PlayerAPI 专用的拦截器
*
* @see PlayerAPI
*/
class PlayerInterceptor(
private val bilibiliClientProperties: BilibiliClientProperties,
private val loginResponse: LoginResponse?
) : Interceptor {
@Suppress("SpellCheckingInspection")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val header = request.headers().newBuilder().apply {
add("Accept", "*/*")
add("User-Agent", "Bilibili Freedoooooom/MarkII")
add("Accept-Language", Header.ZH_CN)
}.build()
val url = request.url().newBuilder().apply {
//视频播放地址(非番剧)这个接口要用 videoAppKey
if (request.url().toString().startsWith(PlayerAPI.videoPlayUrl)) {
addQueryParameter(Param.APP_KEY, bilibiliClientProperties.videoAppKey)
} else {
addQueryParameter(Param.APP_KEY, bilibiliClientProperties.appKey)
}
//公共参数
addQueryParameter("device", bilibiliClientProperties.platform)
addQueryParameter("mobi_app", bilibiliClientProperties.platform)
if (loginResponse != null) {
addQueryParameter("mid", loginResponse.userId.toString())
addQueryParameter(Param.ACCESS_KEY, loginResponse.token)
}
addQueryParameter("platform", bilibiliClientProperties.platform)
addQueryParameter("ts", Instant.now().epochSecond.toString())
addQueryParameter("build", bilibiliClientProperties.build)
addQueryParameter("buvid", bilibiliClientProperties.buildVersionId)
}.build()
return chain.proceed(
request.newBuilder()
.headers(header)
.url(url)
.build()
)
}
}

View File

@ -0,0 +1,89 @@
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: Int, // 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

@ -0,0 +1,80 @@
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: Int, // 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

@ -11,9 +11,10 @@ object Method {
const val OPTION = "OPTION"
}
object ContentType {
object Header {
const val JSON = "application/json"
const val FORM_URLENCODED = "application/x-www-form-urlencoded; charset=utf-8"
const val ZH_CN = "zh-CN,zh;q=0.8"
}
object Param {

View File

@ -19,9 +19,13 @@ object Config {
val password by config.byString
val presetBilibiliClient by lazy {
//登陆过的实例
val bilibiliClient by lazy {
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY).apply {
loginResponse = config["loginResponse"]?.let { gson.fromJson(it) }
}
}
//未登录的实例
val noLoginBilibiliClient = BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
}

View File

@ -1,24 +1,20 @@
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()
Config.noLoginBilibiliClient.mainAPI.reply(oid = 44154463).await()
}
}
@Test
fun fetchChildReply() {
runBlocking {
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
Config.noLoginBilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
}
}
}

View File

@ -0,0 +1,24 @@
package com.hiczp.bilibili.api.test
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
class PlayUrlTest {
@Test
fun videoPlayUrl() {
runBlocking {
Config.noLoginBilibiliClient.playerAPI.run {
videoPlayUrl(aid = 41517911, cid = 72913641).await()
}
}
}
@Test
fun bangumiPlayUrl() {
runBlocking {
Config.noLoginBilibiliClient.playerAPI.run {
bangumiPlayUrl(aid = 42714241, cid = 74921228, seasonType = 1).await()
}
}
}
}

View File

@ -7,7 +7,7 @@ class UserInfoTest {
@Test
fun info() {
runBlocking {
Config.presetBilibiliClient.appAPI.myInfo().await()
Config.bilibiliClient.appAPI.myInfo().await()
}
}
}