MENU

一个基于 Kotlin 的简易 Bilibili 接口客户端

March 9, 2024 • Read: 53 • 后端,开发日常

一个基于 Kotlin 的简易 Bilibili 接口客户端

壹;简介

基于 Kotlin 开发封装的 Bilibili 接口客户端, 支持 WebAPI 鉴权。

贰;相关代码

封装类,支持异步调用,同步执行可直接实例化 HttpBilibiliClient
class HttpBilibiliClient(private val apiUrl: String, private val parameters: String = "") {
    private var wbi: Boolean = false
    private val cookies: MutableList<String> = mutableListOf()
    private val headers: MutableMap<String, List<String>> = mutableMapOf()

    fun request(): JsonObject {
        var appendParameters = parameters
        // 当启用 WBI 签名
        if (wbi) {
            val rawWBIKey: String = "替换成 img_key" + "替换成 sub_key"
            val mixinKeyEncTab = intArrayOf(
                46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
                33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
                61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
                36, 20, 34, 44, 52
            )
            val key = StringBuilder()
            for (i in 0..31) {
                key.append(rawWBIKey[mixinKeyEncTab[i]])
            }
            val mixinKey = key.toString()
            val wts = System.currentTimeMillis() / 1000
            val tempParamenters = "${this.getUrlParameters("$appendParameters&wts=$wts")}$mixinKey"
            appendParameters = "$appendParameters&w_rid=${DigestUtil.calculateMD5(tempParamenters)}&wts=$wts"
        }
        val client = URL("$apiUrl${if (appendParameters.isEmpty()) "" else "?$appendParameters"}")
        with(client.openConnection() as HttpURLConnection) {
            val response = StringBuilder()
            setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76")
            setRequestProperty("Accept", "*/*")
            setRequestProperty("Connection", "keep-alive")
            if (cookies.isNotEmpty()) {
                val strJoiner = StringJoiner("; ")
                cookies.forEach { strJoiner.add(it) }
                setRequestProperty("Cookie", strJoiner.toString())
            }
            requestMethod = "GET"
            inputStream.bufferedReader().use {
                it.lines().forEach { line ->
                    response.append(line)
                }
                it.close()
                headers.putAll(headerFields)
                return JsonUtil.strToObject(response.toString())
            }
        }
    }

    fun setCookie(key: String, value: String): HttpBilibiliClient {
        this.cookies.add("$key=$value")
        return this
    }

    fun enableWBI(wbi: Boolean = false): HttpBilibiliClient {
        this.wbi = wbi
        return this
    }

    fun getHeaders(): Map<String, List<String>> {
        return headers
    }

    private fun getUrlParameters(query: String): String {
        if (query.isEmpty()) {
            return query
        }
        val params = mutableMapOf<String, String>()
        query.split("&").forEach { param ->
            val (key, value) = param.split("=")
            params[key] = value
        }
        val sortedParams = params.toSortedMap()
        return sortedParams.entries.joinToString("&") { (key, value) -> "$key=${URLEncoder.encode(value, "utf8")}" }
    }
}

object BilibiliClient {

    fun openClient(apiUrl: String, parameters: String = "", wbi: Boolean = false, consumer: Consumer<JsonObject>) {
        CompletableFuture.supplyAsync {
            HttpBilibiliClient(apiUrl, parameters).enableWBI(wbi).request()
        }.thenApply { it }.thenAccept { o -> consumer.accept(o) }
    }

    fun getDm(): String {
        val dmRand = "ABCDEFGHIJK".toCharArray()
        val dmImgList = "[]"
        val dmImgStr = randomSample(dmRand, 2).joinToString("")
        val dmCoverImgStr = randomSample(dmRand, 2).joinToString("")
        val dmImgInter = "{\"ds\":[],\"wh\":[0,0,0],\"of\":[0,0,0]}"
        val entries = listOf(
            "dm_img_list" to dmImgList,
            "dm_img_str" to dmImgStr,
            "dm_cover_img_str" to dmCoverImgStr,
            "dm_img_inter" to dmImgInter
        )
        return entries.joinToString("&") { "${it.first}=${it.second}" }
    }
}

object DigestUtil {

    fun calculateMD5(input: String): String {
        val md = MessageDigest.getInstance("MD5")
        md.update(input.toByteArray())
        val bytes = md.digest()
        val hexString = StringBuilder()
        bytes.forEach {
            val hex = Integer.toHexString(0xFF and it.toInt())
            hexString.append(if (hex.length == 1) '0' else hex)
        }
        return hexString.toString()
    }
}

object JsonUtil {
    private val gson: Gson = GsonBuilder().create()

    fun strToObject(string: String): JsonObject {
        return gson.fromJson(string, JsonObject::class.java)
    }
}

调用例子

其他接口请自行查找实现,这里只用两个接口做示例。

// 获取指定用户视频列表
BilibiliClient.openClient(
    "https://api.bilibili.com/x/space/wbi/arc/search",
    "mid=232720681&ps=30&${BilibiliClient.getDm()}",
    true
) {
    println(it) // 请求成功后返回的数据
}

// 获取最新 img_url, sub_url
BilibiliClient.openClient("https://api.bilibili.com/x/web-interface/nav") {
    println(it)
}

叁;结尾

以上就是本次分享的全部内容,感谢大家的阅读。