mirror of
https://github.com/czp3009/bilibili-api.git
synced 2025-02-19 20:50:28 +08:00
完成获取视频和番剧播放地址的 API
This commit is contained in:
parent
6ed9109b05
commit
47706c10de
@ -47,6 +47,10 @@ val code = bilibiliApiException.commonResponse.code
|
||||
|
||||
登陆和登出均为异步方法, 需要在协程上下文中执行.
|
||||
|
||||
如果所使用的语言无法正确调用 `suspend` 方法, 可以使用 `loginFuture` 方法来替代, 它会返回一个 Java8 `CompletableFuture`.
|
||||
|
||||
`logoutFuture` 同理.
|
||||
|
||||
```kotlin
|
||||
runBlocking {
|
||||
BilibiliClient().run {
|
||||
|
@ -1,6 +1,7 @@
|
||||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.3.21'
|
||||
kotlin_coroutines_version= '1.1.1'
|
||||
jvm_target = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
@ -28,7 +29,9 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8
|
||||
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
|
||||
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.1.1'
|
||||
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: kotlin_coroutines_version
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-jdk8
|
||||
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: kotlin_coroutines_version
|
||||
}
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
|
@ -17,6 +17,8 @@ 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 kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.future.future
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -186,8 +188,7 @@ class BilibiliClient(
|
||||
.addConverterFactory(gsonConverterFactory)
|
||||
.addCallAdapterFactory(coroutineCallAdapterFactory)
|
||||
.client(OkHttpClient.Builder().apply {
|
||||
//TODO functional
|
||||
addInterceptor(PlayerInterceptor(billingClientProperties, loginResponse))
|
||||
addInterceptor(PlayerInterceptor(billingClientProperties) { loginResponse })
|
||||
addInterceptor(FailureResponseInterceptor)
|
||||
//log
|
||||
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
|
||||
@ -239,6 +240,15 @@ class BilibiliClient(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Future 类型的 login 接口, 用于兼容 Java, 下同
|
||||
*/
|
||||
fun loginFuture(username: String, password: String,
|
||||
challenge: String?,
|
||||
secCode: String?,
|
||||
validate: String?
|
||||
) = GlobalScope.future { login(username, password, challenge, secCode, validate) }
|
||||
|
||||
/**
|
||||
* 登出
|
||||
* 这个方法不一定是线程安全的, 登出的同时如果进行登陆操作可能引发错误
|
||||
@ -253,6 +263,11 @@ class BilibiliClient(
|
||||
loginResponse = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Future 类型的 logout 接口
|
||||
*/
|
||||
fun logoutFuture() = GlobalScope.future { logout() }
|
||||
|
||||
private val sortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
|
||||
private inline fun <reified T : Any> createAPI(
|
||||
baseUrl: String,
|
||||
|
@ -47,6 +47,7 @@ class BilibiliClientProperties {
|
||||
|
||||
/**
|
||||
* 屏幕尺寸, 大屏手机(已经没有小屏手机了)统一为 xxhdpi
|
||||
* 此参数在新版客户端已经较少使用
|
||||
*/
|
||||
var scale = "xxhdpi"
|
||||
|
||||
|
23
src/main/kotlin/com/hiczp/bilibili/api/CipherExtension.kt
Normal file
23
src/main/kotlin/com/hiczp/bilibili/api/CipherExtension.kt
Normal file
@ -0,0 +1,23 @@
|
||||
package com.hiczp.bilibili.api
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
//MD5
|
||||
private val md5Instance = MessageDigest.getInstance("MD5")
|
||||
|
||||
internal fun String.md5() =
|
||||
StringBuilder(32).apply {
|
||||
//优化过的 md5 字符串生成算法
|
||||
md5Instance.digest(toByteArray()).forEach {
|
||||
val value = it.toInt() and 0xFF
|
||||
val high = value / 16
|
||||
val low = value - high * 16
|
||||
append(if (high <= 9) '0' + high else 'a' - 10 + high)
|
||||
append(if (low <= 9) '0' + low else 'a' - 10 + low)
|
||||
}
|
||||
}.toString()
|
||||
|
||||
/**
|
||||
* 签名算法为 "$排序后的参数字符串$appSecret".md5()
|
||||
*/
|
||||
fun calculateSign(sortedQuery: String, appSecret: String) = (sortedQuery + appSecret).md5()
|
@ -1,7 +1,9 @@
|
||||
package com.hiczp.bilibili.api.main
|
||||
|
||||
import com.hiczp.bilibili.api.main.model.ChildReply
|
||||
import com.hiczp.bilibili.api.main.model.Recommend
|
||||
import com.hiczp.bilibili.api.main.model.Reply
|
||||
import com.hiczp.bilibili.api.main.model.Season
|
||||
import kotlinx.coroutines.Deferred
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
@ -48,4 +50,28 @@ interface MainAPI {
|
||||
@Query("sort") sort: Int = 0,
|
||||
@Query("type") type: Int = 1
|
||||
): Deferred<ChildReply>
|
||||
|
||||
/**
|
||||
* 获得一个番剧的分季信息, 包含默认季(通常是最新的一季)的分集信息
|
||||
* seasonId 或 episodeId 必须有一个, 返回的结果是一样的
|
||||
* 返回值中, 每个 episode 都有 aid 和 cid
|
||||
*
|
||||
* @param seasonId 季的唯一标识
|
||||
* @param episodeId 集的唯一标识
|
||||
*/
|
||||
@GET("/pgc/view/app/season")
|
||||
fun season(
|
||||
@Query("season_id") seasonId: Long? = null,
|
||||
@Query("ep_id") episodeId: Long? = null,
|
||||
@Query("track_path") trackPath: Int? = null
|
||||
): Deferred<Season>
|
||||
|
||||
/**
|
||||
* 番剧页面下方的推荐(对当前季进行推荐)
|
||||
* 返回值中的 relates 是 "相关推荐"(广告), season 是 "更多推荐"(其他番, 目标为季)
|
||||
*
|
||||
* @param seasonId 季的唯一标识
|
||||
*/
|
||||
@GET("/pgc/season/app/related/recommend")
|
||||
fun recommend(@Query("season_id") seasonId: Long): Deferred<Recommend>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.hiczp.bilibili.api.main.model
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ChildReply(
|
||||
@ -156,7 +157,7 @@ data class ChildReply(
|
||||
@SerializedName("rcount")
|
||||
var rcount: Int, // 0
|
||||
@SerializedName("replies")
|
||||
var replies: List<Any>,
|
||||
var replies: List<JsonElement>,
|
||||
@SerializedName("root")
|
||||
var root: Int, // 1405602348
|
||||
@SerializedName("root_str")
|
||||
@ -176,7 +177,7 @@ data class ChildReply(
|
||||
@SerializedName("device")
|
||||
var device: String,
|
||||
@SerializedName("members")
|
||||
var members: List<Any>,
|
||||
var members: List<JsonElement>,
|
||||
@SerializedName("message")
|
||||
var message: String, // 导演:你认为是否有人了解你?像你自己一样了解你?老佛爷:这个问题我很难回答,别人对我的想法已根深蒂固,所以我认为几乎是不可能,我想是如此,即使是我深爱的人。我不想在别人生活中显得真实,我想成为幽灵,现身,然后消失,我也不想面对任何人的真实,因为我不想面对真实的自己,那是我的秘密。别跟我说那些关于孤独的陈词滥调,之于我这种人,孤独是一种胜利,这是场人生战役。像我一样从事创意工作的人,必须独处,让自己重新充电,整日生活在聚光灯前是无法创作的。我还要做许多事,例如阅读,身边有人就无法去做。平时几乎已没时间,但我随时都会想阅读,所以我赞成每人都要该独立生活。将别人当成依靠,对于我这样的人来说很危险,我必须时时刻刻如履薄冰,并在它破裂之前跨出下一步。
|
||||
@SerializedName("plat")
|
||||
@ -196,7 +197,7 @@ data class ChildReply(
|
||||
@SerializedName("avatar")
|
||||
var avatar: String, // http://i2.hdslb.com/bfs/face/63f5da7bda813e470cefd465767035efccff747d.jpg
|
||||
@SerializedName("fans_detail")
|
||||
var fansDetail: Any?, // null
|
||||
var fansDetail: JsonElement?, // null
|
||||
@SerializedName("following")
|
||||
var following: Int, // 0
|
||||
@SerializedName("level_info")
|
||||
@ -294,7 +295,7 @@ data class ChildReply(
|
||||
@SerializedName("device")
|
||||
var device: String,
|
||||
@SerializedName("members")
|
||||
var members: List<Any>,
|
||||
var members: List<JsonElement>,
|
||||
@SerializedName("message")
|
||||
var message: String, // 唉有点不敢相信…R.I.P……走好走好
|
||||
@SerializedName("plat")
|
||||
@ -314,7 +315,7 @@ data class ChildReply(
|
||||
@SerializedName("avatar")
|
||||
var avatar: String, // http://i2.hdslb.com/bfs/face/63f5da7bda813e470cefd465767035efccff747d.jpg
|
||||
@SerializedName("fans_detail")
|
||||
var fansDetail: Any?, // null
|
||||
var fansDetail: JsonElement?, // null
|
||||
@SerializedName("following")
|
||||
var following: Int, // 0
|
||||
@SerializedName("level_info")
|
||||
|
@ -0,0 +1,89 @@
|
||||
package com.hiczp.bilibili.api.main.model
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Recommend(
|
||||
@SerializedName("code")
|
||||
var code: Int, // 0
|
||||
@SerializedName("message")
|
||||
var message: String, // success
|
||||
@SerializedName("result")
|
||||
var result: Result
|
||||
) {
|
||||
data class Result(
|
||||
@SerializedName("card")
|
||||
var card: JsonElement, // []
|
||||
@SerializedName("relates")
|
||||
var relates: List<Relate>,
|
||||
@SerializedName("season")
|
||||
var season: List<Season>
|
||||
) {
|
||||
data class Season(
|
||||
@SerializedName("badge")
|
||||
var badge: String, // 会员抢先
|
||||
@SerializedName("badge_type")
|
||||
var badgeType: Int, // 0
|
||||
@SerializedName("cover")
|
||||
var cover: String, // http://i0.hdslb.com/bfs/bangumi/3fc16a667502cbff226e585eb660a96a20c7458c.png
|
||||
@SerializedName("from")
|
||||
var from: Int, // 0
|
||||
@SerializedName("new_ep")
|
||||
var newEp: NewEp,
|
||||
@SerializedName("rating")
|
||||
var rating: Rating,
|
||||
@SerializedName("season_id")
|
||||
var seasonId: Int, // 26146
|
||||
@SerializedName("season_type")
|
||||
var seasonType: Int, // 1
|
||||
@SerializedName("stat")
|
||||
var stat: Stat,
|
||||
@SerializedName("title")
|
||||
var title: String, // 多罗罗
|
||||
@SerializedName("url")
|
||||
var url: String // http://www.bilibili.com/bangumi/play/ss26146
|
||||
) {
|
||||
data class NewEp(
|
||||
@SerializedName("cover")
|
||||
var cover: String, // http://i0.hdslb.com/bfs/archive/7dec9d820b82ee57ebde0ba5c186b63e1e728abd.jpg
|
||||
@SerializedName("index_show")
|
||||
var indexShow: String // 更新至第7话
|
||||
)
|
||||
|
||||
data class Rating(
|
||||
@SerializedName("count")
|
||||
var count: Long, // 22916
|
||||
@SerializedName("score")
|
||||
var score: Double // 9.8
|
||||
)
|
||||
|
||||
data class Stat(
|
||||
@SerializedName("danmaku")
|
||||
var danmaku: Int, // 435439
|
||||
@SerializedName("follow")
|
||||
var follow: Int, // 2073877
|
||||
@SerializedName("view")
|
||||
var view: Int // 24884016
|
||||
)
|
||||
}
|
||||
|
||||
data class Relate(
|
||||
@SerializedName("desc1")
|
||||
var desc1: String, // 【萌羽Moeyu】魔法禁书目录御坂美琴易拉罐保温杯
|
||||
@SerializedName("desc2")
|
||||
var desc2: String, // 295
|
||||
@SerializedName("item_id")
|
||||
var itemId: Int, // 10005816
|
||||
@SerializedName("pic")
|
||||
var pic: String, // https://i0.hdslb.com/bfs/mall/mall/c3/f0/c3f029d8221c6ecc96bd1ab321034bc2.jpg
|
||||
@SerializedName("title")
|
||||
var title: String, // 【现货即发】魔法禁书目录正版授权,Moeyu出品。
|
||||
@SerializedName("type")
|
||||
var type: Int, // 1
|
||||
@SerializedName("type_name")
|
||||
var typeName: String, // 商品
|
||||
@SerializedName("url")
|
||||
var url: String // bilibili://mall/web?url=https%3A%2F%2Fmall.bilibili.com%2Fdetail.html%3FitemsId%3D10005816%26msource%3Dfanju_25617_10005816%26noTitleBar%3D1%26loadingShow%3D1
|
||||
)
|
||||
}
|
||||
}
|
283
src/main/kotlin/com/hiczp/bilibili/api/main/model/Season.kt
Normal file
283
src/main/kotlin/com/hiczp/bilibili/api/main/model/Season.kt
Normal file
@ -0,0 +1,283 @@
|
||||
package com.hiczp.bilibili.api.main.model
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Season(
|
||||
@SerializedName("code")
|
||||
var code: Int, // 0
|
||||
@SerializedName("message")
|
||||
var message: String, // success
|
||||
@SerializedName("result")
|
||||
var result: Result
|
||||
) {
|
||||
data class Result(
|
||||
@SerializedName("cover")
|
||||
var cover: String, // http://i0.hdslb.com/bfs/bangumi/a92892921f3209f7784a954c37467c9869a1d4c1.png
|
||||
@SerializedName("episodes")
|
||||
var episodes: List<Episode>,
|
||||
@SerializedName("evaluate")
|
||||
var evaluate: String, // 位于东京西部的巨大“学园都市”,实施着超能力开发的特殊课程。学生们的能力被给予从“无能力Level 0”到“超能力Level 5”的六阶段评价。高中生上条当麻,由于寄宿在右手中的力量——只要是异能之力...
|
||||
@SerializedName("link")
|
||||
var link: String, // http://www.bilibili.com/bangumi/media/md134912/
|
||||
@SerializedName("media_id")
|
||||
var mediaId: Int, // 134912
|
||||
@SerializedName("mode")
|
||||
var mode: Int, // 2
|
||||
@SerializedName("new_ep")
|
||||
var newEp: NewEp,
|
||||
@SerializedName("paster")
|
||||
var paster: Paster,
|
||||
@SerializedName("payment")
|
||||
var payment: Payment,
|
||||
@SerializedName("publish")
|
||||
var publish: Publish,
|
||||
@SerializedName("rating")
|
||||
var rating: Rating,
|
||||
@SerializedName("record")
|
||||
var record: String,
|
||||
@SerializedName("rights")
|
||||
var rights: Rights,
|
||||
@SerializedName("season_id")
|
||||
var seasonId: Long, // 25617
|
||||
@SerializedName("season_title")
|
||||
var seasonTitle: String, // 魔法禁书目录 第三季
|
||||
@SerializedName("seasons")
|
||||
var seasons: List<Season>,
|
||||
@SerializedName("section")
|
||||
var section: List<JsonElement>,
|
||||
@SerializedName("series")
|
||||
var series: Series,
|
||||
@SerializedName("share_url")
|
||||
var shareUrl: String, // http://m.bilibili.com/bangumi/play/ss25617
|
||||
@SerializedName("square_cover")
|
||||
var squareCover: String, // http://i0.hdslb.com/bfs/bangumi/91b29251445f9b808e9c30f34019d3ba4f128d6d.jpg
|
||||
@SerializedName("stat")
|
||||
var stat: Stat,
|
||||
@SerializedName("status")
|
||||
var status: Int, // 13
|
||||
@SerializedName("title")
|
||||
var title: String, // 魔法禁书目录 第三季
|
||||
@SerializedName("total")
|
||||
var total: Int, // 0
|
||||
@SerializedName("type")
|
||||
var type: Int, // 1
|
||||
@SerializedName("user_status")
|
||||
var userStatus: UserStatus
|
||||
) {
|
||||
data class Series(
|
||||
@SerializedName("series_id")
|
||||
var seriesId: Int, // 621
|
||||
@SerializedName("series_title")
|
||||
var seriesTitle: String // 魔法禁书目录
|
||||
)
|
||||
|
||||
data class UserStatus(
|
||||
@SerializedName("follow")
|
||||
var follow: Int, // 1
|
||||
@SerializedName("pay")
|
||||
var pay: Int, // 0
|
||||
@SerializedName("progress")
|
||||
var progress: Progress,
|
||||
@SerializedName("review")
|
||||
var review: Review,
|
||||
@SerializedName("sponsor")
|
||||
var sponsor: Int, // 0
|
||||
@SerializedName("vip")
|
||||
var vip: Int, // 0
|
||||
@SerializedName("vip_frozen")
|
||||
var vipFrozen: Int // 0
|
||||
) {
|
||||
data class Progress(
|
||||
@SerializedName("last_ep_id")
|
||||
var lastEpId: Int, // 250436
|
||||
@SerializedName("last_ep_index")
|
||||
var lastEpIndex: String, // 3
|
||||
@SerializedName("last_time")
|
||||
var lastTime: Int // 1405
|
||||
)
|
||||
|
||||
data class Review(
|
||||
@SerializedName("is_open")
|
||||
var isOpen: Int // 0
|
||||
)
|
||||
}
|
||||
|
||||
data class Season(
|
||||
@SerializedName("is_new")
|
||||
var isNew: Int, // 1
|
||||
@SerializedName("season_id")
|
||||
var seasonId: Long, // 25617
|
||||
@SerializedName("season_title")
|
||||
var seasonTitle: String // 第三季
|
||||
)
|
||||
|
||||
data class Episode(
|
||||
@SerializedName("aid")
|
||||
var aid: Int, // 44389470
|
||||
@SerializedName("badge")
|
||||
var badge: String, // 会员
|
||||
@SerializedName("badge_type")
|
||||
var badgeType: Int, // 0
|
||||
@SerializedName("cid")
|
||||
var cid: Int, // 77725026
|
||||
@SerializedName("cover")
|
||||
var cover: String, // http://i0.hdslb.com/bfs/archive/c83ef2a961d8b53d6a30f03e8fb631ea4248fede.jpg
|
||||
@SerializedName("dimension")
|
||||
var dimension: Dimension,
|
||||
@SerializedName("from")
|
||||
var from: String, // bangumi
|
||||
@SerializedName("id")
|
||||
var id: Int, // 250453
|
||||
@SerializedName("long_title")
|
||||
var longTitle: String, // 守护的理由
|
||||
@SerializedName("share_url")
|
||||
var shareUrl: String, // https://m.bilibili.com/bangumi/play/ep250453
|
||||
@SerializedName("status")
|
||||
var status: Int, // 13
|
||||
@SerializedName("title")
|
||||
var title: String, // 20
|
||||
@SerializedName("vid")
|
||||
var vid: String
|
||||
) {
|
||||
data class Dimension(
|
||||
@SerializedName("height")
|
||||
var height: Int, // 1080
|
||||
@SerializedName("rotate")
|
||||
var rotate: Int, // 0
|
||||
@SerializedName("width")
|
||||
var width: Int // 1920
|
||||
)
|
||||
}
|
||||
|
||||
data class Rating(
|
||||
@SerializedName("count")
|
||||
var count: Int, // 32318
|
||||
@SerializedName("score")
|
||||
var score: Double // 7.8
|
||||
)
|
||||
|
||||
data class Rights(
|
||||
@SerializedName("allow_bp")
|
||||
var allowBp: Int, // 0
|
||||
@SerializedName("allow_download")
|
||||
var allowDownload: Int, // 0
|
||||
@SerializedName("allow_review")
|
||||
var allowReview: Int, // 1
|
||||
@SerializedName("area_limit")
|
||||
var areaLimit: Int, // 0
|
||||
@SerializedName("ban_area_show")
|
||||
var banAreaShow: Int, // 1
|
||||
@SerializedName("copyright")
|
||||
var copyright: String, // bilibili
|
||||
@SerializedName("is_preview")
|
||||
var isPreview: Int, // 1
|
||||
@SerializedName("watch_platform")
|
||||
var watchPlatform: Int // 0
|
||||
)
|
||||
|
||||
data class NewEp(
|
||||
@SerializedName("desc")
|
||||
var desc: String, // 连载中, 每周五22:30更新
|
||||
@SerializedName("id")
|
||||
var id: Int, // 250453
|
||||
@SerializedName("is_new")
|
||||
var isNew: Int, // 1
|
||||
@SerializedName("title")
|
||||
var title: String // 20
|
||||
)
|
||||
|
||||
data class Payment(
|
||||
@SerializedName("dialog")
|
||||
var dialog: Dialog,
|
||||
@SerializedName("pay_tip")
|
||||
var payTip: PayTip,
|
||||
@SerializedName("pay_type")
|
||||
var payType: PayType,
|
||||
@SerializedName("price")
|
||||
var price: String, // 0.0
|
||||
@SerializedName("vip_promotion")
|
||||
var vipPromotion: String
|
||||
) {
|
||||
data class Dialog(
|
||||
@SerializedName("btn_right")
|
||||
var btnRight: BtnRight,
|
||||
@SerializedName("desc")
|
||||
var desc: String,
|
||||
@SerializedName("title")
|
||||
var title: String // 开通大会员抢先看
|
||||
) {
|
||||
data class BtnRight(
|
||||
@SerializedName("title")
|
||||
var title: String, // 成为大会员
|
||||
@SerializedName("type")
|
||||
var type: String // vip
|
||||
)
|
||||
}
|
||||
|
||||
data class PayType(
|
||||
@SerializedName("allow_ticket")
|
||||
var allowTicket: Int // 0
|
||||
)
|
||||
|
||||
data class PayTip(
|
||||
@SerializedName("primary")
|
||||
var primary: Primary
|
||||
) {
|
||||
data class Primary(
|
||||
@SerializedName("sub_title")
|
||||
var subTitle: String,
|
||||
@SerializedName("title")
|
||||
var title: String, // 开通大会员抢先看
|
||||
@SerializedName("type")
|
||||
var type: Int, // 1
|
||||
@SerializedName("url")
|
||||
var url: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Stat(
|
||||
@SerializedName("coins")
|
||||
var coins: Long, // 333230
|
||||
@SerializedName("danmakus")
|
||||
var danmakus: Int, // 729836
|
||||
@SerializedName("favorites")
|
||||
var favorites: Int, // 2300833
|
||||
@SerializedName("reply")
|
||||
var reply: Int, // 324057
|
||||
@SerializedName("share")
|
||||
var share: Int, // 18362
|
||||
@SerializedName("views")
|
||||
var views: Int // 41392306
|
||||
)
|
||||
|
||||
data class Publish(
|
||||
@SerializedName("is_finish")
|
||||
var isFinish: Int, // 0
|
||||
@SerializedName("is_started")
|
||||
var isStarted: Int, // 1
|
||||
@SerializedName("pub_time")
|
||||
var pubTime: String, // 2018-10-05 22:30:00
|
||||
@SerializedName("pub_time_show")
|
||||
var pubTimeShow: String, // 10月05日22:30
|
||||
@SerializedName("weekday")
|
||||
var weekday: Int // 0
|
||||
)
|
||||
|
||||
data class Paster(
|
||||
@SerializedName("aid")
|
||||
var aid: Long, // 0
|
||||
@SerializedName("allow_jump")
|
||||
var allowJump: Int, // 0
|
||||
@SerializedName("cid")
|
||||
var cid: Long, // 0
|
||||
@SerializedName("duration")
|
||||
var duration: Int, // 0
|
||||
@SerializedName("type")
|
||||
var type: Int, // 0
|
||||
@SerializedName("url")
|
||||
var url: String
|
||||
)
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
package com.hiczp.bilibili.api.player
|
||||
|
||||
import com.hiczp.bilibili.api.md5
|
||||
import com.hiczp.bilibili.api.player.model.BangumiPlayUrl
|
||||
import com.hiczp.bilibili.api.player.model.VideoPlayUrl
|
||||
import kotlinx.coroutines.Deferred
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import java.util.*
|
||||
import java.lang.management.ManagementFactory
|
||||
|
||||
/**
|
||||
* 这里是播放器会访问的 API
|
||||
@ -16,7 +17,6 @@ interface PlayerAPI {
|
||||
* 获得视频的播放地址
|
||||
* 这个 API 需要使用特别的 appKey
|
||||
*
|
||||
* @param expire 默认为下个月的这一天的时间戳
|
||||
* @param cid 在获取视频详情页面的接口的返回值里
|
||||
* @param aid 视频的唯一标识
|
||||
*
|
||||
@ -24,13 +24,11 @@ interface PlayerAPI {
|
||||
*/
|
||||
@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>
|
||||
@ -40,28 +38,27 @@ interface PlayerAPI {
|
||||
*
|
||||
* @param aid 番剧的唯一标识
|
||||
* @param cid 在番剧详情页的返回值里
|
||||
* @param seasonType 番剧分季(第一季, 第二季)(从 1 开始)
|
||||
* @param session 不明确其含义
|
||||
* @param seasonType 分级类型, 不明确, 似乎总为 1
|
||||
* @param session 其值为 系统已运行时间(ms)的MD5值, 此处的默认值为 JVM 已启动时间, 在 Android 上请使用 SystemClock
|
||||
* @param trackPath 不明确
|
||||
*
|
||||
* @see com.hiczp.bilibili.api.main.MainAPI.season
|
||||
*/
|
||||
@GET("https://api.bilibili.com/pgc/player/api/playurl")
|
||||
fun bangumiPlayUrl(
|
||||
@Query("aid") aid: Long,
|
||||
@Query("cid") cid: Long,
|
||||
@Query("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("season_type") seasonType: Int = 1,
|
||||
@Query("session") session: String = (System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().startTime).toString().md5(),
|
||||
@Query("track_path") trackPath: Int? = null
|
||||
): Deferred<BangumiPlayUrl>
|
||||
|
||||
companion object {
|
||||
const val videoPlayUrl = "https://app.bilibili.com/x/playurl"
|
||||
private fun nextMonthTimestamp() = Calendar.getInstance().apply { add(Calendar.MONTH, 1) }.toInstant().epochSecond
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
package com.hiczp.bilibili.api.player
|
||||
|
||||
import com.hiczp.bilibili.api.BilibiliClientProperties
|
||||
import com.hiczp.bilibili.api.calculateSign
|
||||
import com.hiczp.bilibili.api.passport.model.LoginResponse
|
||||
import com.hiczp.bilibili.api.retrofit.Charsets.UTF_8
|
||||
import com.hiczp.bilibili.api.retrofit.Header
|
||||
import com.hiczp.bilibili.api.retrofit.Param
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.net.URLEncoder
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@ -15,37 +18,54 @@ import java.time.Instant
|
||||
*/
|
||||
class PlayerInterceptor(
|
||||
private val bilibiliClientProperties: BilibiliClientProperties,
|
||||
private val loginResponse: LoginResponse?
|
||||
private val loginResponseExpression: () -> LoginResponse?
|
||||
) : Interceptor {
|
||||
@Suppress("SpellCheckingInspection")
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
//添加 header
|
||||
val header = request.headers().newBuilder().apply {
|
||||
add("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)
|
||||
//添加 Query Params
|
||||
val oldUrl = request.url()
|
||||
//如果是视频播放地址这个 API, 要用特殊的 appKey
|
||||
val isVideo = oldUrl.toString().startsWith(PlayerAPI.videoPlayUrl)
|
||||
val url = StringBuilder(oldUrl.encodedQuery() ?: "").apply {
|
||||
//appKey
|
||||
addParamEncode(Param.APP_KEY, if (isVideo) bilibiliClientProperties.videoAppKey else bilibiliClientProperties.appKey)
|
||||
//凭证有关
|
||||
val loginRespons = loginResponseExpression()
|
||||
if (loginRespons != null) {
|
||||
//expire 的值为 token过期时间+2s
|
||||
addParamEncode(Param.EXPIRE, (loginRespons.ts + loginRespons.data.tokenInfo.expiresIn + 2).toString())
|
||||
addParamEncode(Param.ACCESS_KEY, loginRespons.token)
|
||||
addParamEncode(Param.MID, loginRespons.userId.toString())
|
||||
} else {
|
||||
addQueryParameter(Param.APP_KEY, bilibiliClientProperties.appKey)
|
||||
addParamEncode(Param.EXPIRE, "0")
|
||||
addParamEncode(Param.MID, "0")
|
||||
}
|
||||
//公共参数
|
||||
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()
|
||||
addParamEncode("device", bilibiliClientProperties.platform)
|
||||
addParamEncode("mobi_app", bilibiliClientProperties.platform)
|
||||
addParamEncode("platform", bilibiliClientProperties.platform)
|
||||
addParamEncode("otype", "json")
|
||||
addParamEncode("ts", Instant.now().epochSecond.toString())
|
||||
addParamEncode("build", bilibiliClientProperties.build)
|
||||
addParamEncode("buvid", bilibiliClientProperties.buildVersionId)
|
||||
}.toString().let {
|
||||
//排序
|
||||
val sortedEncodedQuery = it.split('&').sorted().joinToString(separator = "&")
|
||||
//添加 sign
|
||||
val sign = calculateSign(sortedEncodedQuery, if (isVideo) bilibiliClientProperties.videoAppSecret else bilibiliClientProperties.appSecret)
|
||||
"$sortedEncodedQuery&${Param.SIGN}=$sign"
|
||||
}.let {
|
||||
oldUrl.newBuilder().encodedQuery(it).build()
|
||||
}
|
||||
|
||||
return chain.proceed(
|
||||
request.newBuilder()
|
||||
@ -55,3 +75,10 @@ class PlayerInterceptor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.addParamEncode(name: String, value: String) {
|
||||
if (length != 0) append('&')
|
||||
append(name)
|
||||
append('=')
|
||||
append(URLEncoder.encode(value, UTF_8))
|
||||
}
|
||||
|
@ -21,5 +21,11 @@ object Param {
|
||||
const val ACCESS_KEY = "access_key"
|
||||
@Suppress("SpellCheckingInspection")
|
||||
const val APP_KEY = "appkey"
|
||||
const val EXPIRE = "expire"
|
||||
const val MID = "mid"
|
||||
const val SIGN = "sign"
|
||||
}
|
||||
|
||||
internal object Charsets {
|
||||
const val UTF_8 = "UTF-8"
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.hiczp.bilibili.api.retrofit.interceptor
|
||||
|
||||
import com.hiczp.bilibili.api.calculateSign
|
||||
import com.hiczp.bilibili.api.retrofit.Param
|
||||
import com.hiczp.bilibili.api.retrofit.containsEncodedName
|
||||
import com.hiczp.bilibili.api.retrofit.sortedRaw
|
||||
@ -7,7 +8,6 @@ import mu.KotlinLogging
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@ -27,14 +27,10 @@ class SortAndSignInterceptor(private val appSecret: String) : Interceptor {
|
||||
request = when {
|
||||
//判断 appKey 是否在 Query 里
|
||||
url.queryParameter(Param.APP_KEY) != null -> {
|
||||
val sortedEncodedQuery = url.encodedQuery()!!
|
||||
.split('&')
|
||||
.sorted()
|
||||
.joinToString(separator = "&")
|
||||
val sign = calculateSign(sortedEncodedQuery, appSecret)
|
||||
val sortedEncodedQuery = url.encodedQuery()!!.split('&').sorted().joinToString(separator = "&")
|
||||
request.newBuilder()
|
||||
.url(url.newBuilder()
|
||||
.encodedQuery("$sortedEncodedQuery&${Param.SIGN}=$sign")
|
||||
.encodedQuery("$sortedEncodedQuery&${Param.SIGN}=${calculateSign(sortedEncodedQuery, appSecret)}")
|
||||
.build()
|
||||
).build()
|
||||
}
|
||||
@ -42,13 +38,12 @@ class SortAndSignInterceptor(private val appSecret: String) : Interceptor {
|
||||
//在 FormBody 里
|
||||
body is FormBody && body.containsEncodedName(Param.APP_KEY) -> {
|
||||
val sortedRaw = body.sortedRaw()
|
||||
val sign = calculateSign(sortedRaw, appSecret)
|
||||
val formBody = FormBody.Builder().apply {
|
||||
sortedRaw.split('&').forEach {
|
||||
val (name, value) = it.split('=')
|
||||
addEncoded(name, value)
|
||||
}
|
||||
addEncoded(Param.SIGN, sign)
|
||||
addEncoded(Param.SIGN, calculateSign(sortedRaw, appSecret))
|
||||
}.build()
|
||||
request.newBuilder()
|
||||
.method(request.method(), formBody)
|
||||
@ -64,22 +59,4 @@ class SortAndSignInterceptor(private val appSecret: String) : Interceptor {
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val md5Instance = MessageDigest.getInstance("MD5")
|
||||
/**
|
||||
* 签名算法为 "$排序后的参数字符串$appSecret".md5()
|
||||
*/
|
||||
private fun calculateSign(string: String, appSecret: String) =
|
||||
StringBuilder(32).apply {
|
||||
//优化过的 md5 字符串生成算法
|
||||
md5Instance.digest((string + appSecret).toByteArray()).forEach {
|
||||
val value = it.toInt() and 0xFF
|
||||
val high = value / 16
|
||||
val low = value - high * 16
|
||||
append(if (high <= 9) '0' + high else 'a' - 10 + high)
|
||||
append(if (low <= 9) '0' + low else 'a' - 10 + low)
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
@ -7,25 +7,26 @@ import com.google.gson.JsonObject
|
||||
import com.hiczp.bilibili.api.BilibiliClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
//配置文件
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val gson = Gson()
|
||||
|
||||
private val config = gson.fromJson<JsonObject>(
|
||||
Config::class.java.getResourceAsStream("/config.json").reader()
|
||||
)
|
||||
|
||||
//未登录的实例
|
||||
val noLoginBilibiliClient = BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
|
||||
|
||||
//登陆过的实例
|
||||
val bilibiliClient by lazy {
|
||||
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY).apply {
|
||||
loginResponse = config["loginResponse"]?.let { gson.fromJson(it) }
|
||||
}
|
||||
}
|
||||
|
||||
object Config {
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val gson = Gson()
|
||||
|
||||
private val config = gson.fromJson<JsonObject>(
|
||||
Config::class.java.getResourceAsStream("/config.json").reader()
|
||||
)
|
||||
|
||||
val username by config.byString
|
||||
|
||||
val password by config.byString
|
||||
|
||||
//登陆过的实例
|
||||
val bilibiliClient by lazy {
|
||||
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY).apply {
|
||||
loginResponse = config["loginResponse"]?.let { gson.fromJson(it) }
|
||||
}
|
||||
}
|
||||
|
||||
//未登录的实例
|
||||
val noLoginBilibiliClient = BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY)
|
||||
}
|
||||
|
@ -7,14 +7,14 @@ class FetchReplyTest {
|
||||
@Test
|
||||
fun fetchReply() {
|
||||
runBlocking {
|
||||
Config.noLoginBilibiliClient.mainAPI.reply(oid = 44154463).await()
|
||||
noLoginBilibiliClient.mainAPI.reply(oid = 44154463).await()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchChildReply() {
|
||||
runBlocking {
|
||||
Config.noLoginBilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
|
||||
noLoginBilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ class PlayUrlTest {
|
||||
@Test
|
||||
fun videoPlayUrl() {
|
||||
runBlocking {
|
||||
Config.noLoginBilibiliClient.playerAPI.run {
|
||||
bilibiliClient.playerAPI.run {
|
||||
videoPlayUrl(aid = 41517911, cid = 72913641).await()
|
||||
}
|
||||
}
|
||||
@ -16,8 +16,8 @@ class PlayUrlTest {
|
||||
@Test
|
||||
fun bangumiPlayUrl() {
|
||||
runBlocking {
|
||||
Config.noLoginBilibiliClient.playerAPI.run {
|
||||
bangumiPlayUrl(aid = 42714241, cid = 74921228, seasonType = 1).await()
|
||||
bilibiliClient.playerAPI.run {
|
||||
bangumiPlayUrl(aid = 42714241, cid = 74921228).await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
src/test/kotlin/com/hiczp/bilibili/api/test/SeasonTest.kt
Normal file
13
src/test/kotlin/com/hiczp/bilibili/api/test/SeasonTest.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package com.hiczp.bilibili.api.test
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SeasonTest {
|
||||
@Test
|
||||
fun season() {
|
||||
runBlocking {
|
||||
noLoginBilibiliClient.mainAPI.season(episodeId = 250536).await()
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ class UserInfoTest {
|
||||
@Test
|
||||
fun info() {
|
||||
runBlocking {
|
||||
Config.bilibiliClient.appAPI.myInfo().await()
|
||||
bilibiliClient.appAPI.myInfo().await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user