mirror of
https://github.com/czp3009/bilibili-api.git
synced 2025-02-19 20:50:28 +08:00
减少依赖引入
This commit is contained in:
parent
b6a000b905
commit
c546606a66
32
README.md
32
README.md
@ -47,11 +47,7 @@ val code = bilibiliApiException.commonResponse.code
|
||||
# 登录和登出
|
||||
(Bilibili oauth2 v3)
|
||||
|
||||
登陆和登出均为异步方法, 需要在协程上下文中执行.
|
||||
|
||||
如果所使用的语言无法正确调用 `suspend` 方法, 可以使用 `loginFuture` 方法来替代, 它会返回一个 Java8 `CompletableFuture`.
|
||||
|
||||
`logoutFuture` 同理.
|
||||
登陆和登出均为异步方法, 需要在协程上下文中执行(接下去不会特地强调这一点).
|
||||
|
||||
```kotlin
|
||||
runBlocking {
|
||||
@ -97,9 +93,13 @@ login(username, password, challenge, secCode, validate)
|
||||
|
||||
(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).
|
||||
|
||||
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误.
|
||||
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误(想要这么做的人一定脑子瓦特了).
|
||||
|
||||
登陆后, 可以访问全部 API.
|
||||
登陆后, 可以访问全部 API(注意, 有一些明显不需要登录的 API 也有可能需要登录).
|
||||
|
||||
由于各种需要登陆的 API 在未登录时返回的 `code` 并不统一, 因此没有办法做自动 `token` 刷新, 自己看着办.
|
||||
|
||||
在真实的客户端上, 每次一打开 APP 就会访问[个人信息 API](#获取个人信息)来确定 `token` 是否仍然可用, 这就是 B站 自己的解决方案.
|
||||
|
||||
# 访问 API
|
||||
不要问文档, 用自动补全(心)来感受. 以下给出几个示例
|
||||
@ -141,7 +141,7 @@ val videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 7
|
||||
|
||||
https://www.bilibili.com/video/av44541340/?p=2
|
||||
|
||||
实际上就是选择了该 `aid` 下的第二个 `cid`.
|
||||
实际上就是选择了该 `aid` 下的第二个 `cid`(注意, 参数里使用的 `cid` 不是这个 p 的序号, 它也是一个很长的数字).
|
||||
|
||||
简单的来说, `aid` 和 `cid` 加在一起才能表示一个视频流(为什么 `cid` 不能直接表示一个视频我也不知道).
|
||||
|
||||
@ -334,13 +334,15 @@ danmakuList.forEach {
|
||||
}
|
||||
```
|
||||
|
||||
注意, 弹幕的解析是惰性的, `danmakuList` 是一个 `Sequence`. 如果同时持有很多未用完的 `danmakuList` 的引用可能会造成大量内存浪费.
|
||||
|
||||
客户端的弹幕屏蔽设置是对弹幕中的 `user` 属性做的. 而实际上 `danmaku.user` 是一个字符串.
|
||||
|
||||
这个字符串是 用户ID 的 `CRC32` 的校验和.
|
||||
|
||||
众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.
|
||||
|
||||
在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法确定一条弹幕到底是哪个用户发送的.
|
||||
在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法完全确定一条弹幕到底是哪个用户发送的.
|
||||
|
||||
如果想获得发送这条弹幕的所有可能的用户的 ID, 可以通过以下方法:
|
||||
|
||||
@ -378,7 +380,7 @@ bilibiliClient.mainAPI.sendDanmaku(aid = 40675923, cid = 71438168, progress = 22
|
||||
如果不确定视频的长度, 需要从[视频播放地址的 API](#获取视频播放地址) 中的 `data.timelength` 来获得, 单位也是毫秒.
|
||||
|
||||
## 获取直播弹幕
|
||||
刚进入直播间时, 看到的十条弹幕实际上是最近的历史弹幕, 通过以下方式来获取
|
||||
刚进入直播间时, 立即看到的十条弹幕实际上是最近的历史弹幕, 通过以下方式来获取
|
||||
|
||||
```kotlin
|
||||
bilibiliClient.liveAPI.roomMessage(roomId).await()
|
||||
@ -481,19 +483,19 @@ onClose = { liveClient, closeReason ->
|
||||
liveClient.sendMessage("我上我也行").await()
|
||||
```
|
||||
|
||||
注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回.
|
||||
注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回(`code` 为 0).
|
||||
|
||||
完全正常返回时, 返回内容中的 `message` 为一个空字符串.
|
||||
完全正常返回时(弹幕正确的被发送了), 返回内容中的 `message` 为一个空字符串.
|
||||
|
||||
如果不为空字符串, 则表示不完全正常
|
||||
|
||||
例如返回内容的 `message` 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 为 0.
|
||||
例如返回内容的 `message` 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 确实是 0.
|
||||
|
||||
其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.
|
||||
|
||||
正常返回时, 客户端都会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).
|
||||
正常返回时, 就算不完全正常, 客户端也会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).
|
||||
|
||||
所以请额外判断返回的 `message` 是否为空字符串.
|
||||
需要额外判断返回的 `message` 是否为空字符串来确认这条弹幕有没有被正确发送.
|
||||
|
||||
# License
|
||||
GPL V3
|
||||
|
14
build.gradle
14
build.gradle
@ -16,7 +16,7 @@ buildscript {
|
||||
}
|
||||
|
||||
group = 'com.hiczp'
|
||||
version = '0.1.0'
|
||||
version = '1.0.0'
|
||||
description = 'Bilibili Android client API library for Kotlin'
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
@ -32,8 +32,6 @@ dependencies {
|
||||
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: 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 {
|
||||
@ -64,7 +62,7 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/com.jakewharton.retrofit/retrofit2-kotlin-coroutines-adapter
|
||||
compile group: 'com.jakewharton.retrofit', name: 'retrofit2-kotlin-coroutines-adapter', version: '0.9.2'
|
||||
// https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor
|
||||
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.13.1'
|
||||
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.14.0'
|
||||
}
|
||||
|
||||
//ktor
|
||||
@ -75,12 +73,6 @@ dependencies {
|
||||
compile group: 'io.ktor', name: 'ktor-client-cio', version: ktor_version
|
||||
}
|
||||
|
||||
//utils
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/commons-io/commons-io
|
||||
compile group: 'commons-io', name: 'commons-io', version: '2.6'
|
||||
}
|
||||
|
||||
//checksum
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/com.hiczp/crc32-crack
|
||||
@ -90,5 +82,5 @@ dependencies {
|
||||
//unit test
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
|
||||
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.4.0'
|
||||
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.4.1'
|
||||
}
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip
|
||||
|
@ -22,8 +22,6 @@ import com.hiczp.bilibili.api.retrofit.interceptor.SortAndSignInterceptor
|
||||
import com.hiczp.bilibili.api.vc.VcAPI
|
||||
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
|
||||
import io.ktor.http.cio.websocket.CloseReason
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.future.future
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -299,15 +297,6 @@ 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) }
|
||||
|
||||
/**
|
||||
* 登出
|
||||
* 这个方法不一定是线程安全的, 登出的同时如果进行登陆操作可能引发错误
|
||||
@ -322,11 +311,6 @@ class BilibiliClient(
|
||||
loginResponse = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Future 类型的 logout 接口
|
||||
*/
|
||||
fun logoutFuture() = GlobalScope.future { logout() }
|
||||
|
||||
private val sortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
|
||||
private val httpLoggingInterceptor = HttpLoggingInterceptor().setLevel(logLevel)
|
||||
private inline fun <reified T : Any> createAPI(
|
||||
|
@ -1,13 +1,33 @@
|
||||
package com.hiczp.bilibili.api
|
||||
|
||||
import com.hiczp.bilibili.api.thirdpart.commons.BoundedInputStream
|
||||
import io.ktor.util.InternalAPI
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.io.input.BoundedInputStream
|
||||
import org.apache.commons.io.input.BoundedReader
|
||||
import kotlinx.io.errors.EOFException
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
fun InputStream.readFully(length: Int) = IOUtils.readFully(this, length)!!
|
||||
//减少包引入
|
||||
//https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/IOUtils.java
|
||||
fun InputStream.readFully(length: Int): ByteArray {
|
||||
if (length < 0) {
|
||||
throw IllegalArgumentException("Length must not be negative: $length")
|
||||
}
|
||||
|
||||
val byteArray = ByteArray(length)
|
||||
var remaining = length
|
||||
|
||||
while (remaining > 0) {
|
||||
val count = read(byteArray, length - remaining, remaining)
|
||||
if (count == -1) break
|
||||
remaining -= count
|
||||
}
|
||||
|
||||
val actual = length - remaining
|
||||
if (actual != length) {
|
||||
throw EOFException("Length to read: $length actual: $actual")
|
||||
}
|
||||
|
||||
return byteArray
|
||||
}
|
||||
|
||||
/**
|
||||
* 以大端模式从流中读取一个 int
|
||||
@ -27,9 +47,6 @@ fun InputStream.readInt(): Int {
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
fun InputStream.readUInt() = readInt().toUInt()
|
||||
|
||||
fun InputStream.boundedReader(maxCharsFromTargetReader: Int, charset: Charset = Charsets.UTF_8) =
|
||||
BoundedReader(reader(charset), maxCharsFromTargetReader)
|
||||
|
||||
fun InputStream.bounded(size: Long) = BoundedInputStream(this, size)
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
|
@ -25,100 +25,96 @@ import javax.xml.stream.XMLStreamConstants
|
||||
* @see com.hiczp.bilibili.api.danmaku.DanmakuAPI.list
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class DanmakuParser {
|
||||
companion object {
|
||||
/**
|
||||
* 解析弹幕文件
|
||||
*
|
||||
* @param inputStream 输入流, 可以指向任何位置
|
||||
*
|
||||
* @return 返回 flags map 与 弹幕序列. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parse(inputStream: InputStream): Pair<Map<Long, Int>, Sequence<Danmaku>> {
|
||||
//Json 的长度
|
||||
val jsonLength = inputStream.readUInt()
|
||||
object DanmakuParser {
|
||||
/**
|
||||
* 解析弹幕文件
|
||||
*
|
||||
* @param inputStream 输入流, 可以指向任何位置
|
||||
*
|
||||
* @return 返回 flags map 与 弹幕序列. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间.
|
||||
*/
|
||||
fun parse(inputStream: InputStream): Pair<Map<Long, Int>, Sequence<Danmaku>> {
|
||||
//Json 的长度
|
||||
val jsonLength = inputStream.readUInt()
|
||||
|
||||
//弹幕ID-Flag
|
||||
val danmakuFlags = HashMap<Long, Int>()
|
||||
//gson 会从 reader 中自行缓冲 1024 个字符, 这会导致额外的字符被消费. 因此要限制其读取数量
|
||||
//流式解析 Json
|
||||
with(JsonReader(inputStream.bounded(jsonLength).reader())) {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"dmflags" -> {
|
||||
beginArray()
|
||||
//弹幕ID-Flag
|
||||
val danmakuFlags = HashMap<Long, Int>()
|
||||
//gson 会从 reader 中自行缓冲 1024 个字符, 这会导致额外的字符被消费. 因此要限制其读取数量
|
||||
//流式解析 Json
|
||||
with(JsonReader(inputStream.bounded(jsonLength).reader())) {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"dmflags" -> {
|
||||
beginArray()
|
||||
while (hasNext()) {
|
||||
var danmakuId = 0L
|
||||
var flag = 0
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
var danmakuId = 0L
|
||||
var flag = 0
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
when (nextName()) {
|
||||
"dmid" -> danmakuId = nextLong()
|
||||
"flag" -> flag = nextInt()
|
||||
else -> skipValue()
|
||||
}
|
||||
when (nextName()) {
|
||||
"dmid" -> danmakuId = nextLong()
|
||||
"flag" -> flag = nextInt()
|
||||
else -> skipValue()
|
||||
}
|
||||
endObject()
|
||||
danmakuFlags[danmakuId] = flag
|
||||
}
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
endObject()
|
||||
}
|
||||
|
||||
//json 解析完毕后, 剩下的内容是一个 gzip 压缩过的 xml
|
||||
val reader = GZIPInputStream(inputStream).reader()
|
||||
//流式解析 xml
|
||||
val xmlEventReader = XMLInputFactory.newInstance().createXMLEventReader(reader)
|
||||
//lazy sequence
|
||||
val danmakus = sequence {
|
||||
var startD = false //之前解析到的 element 是否是 d
|
||||
var p: String? = null //之前解析到的 p 的值
|
||||
while (xmlEventReader.hasNext()) {
|
||||
val event = xmlEventReader.nextEvent()
|
||||
when (event.eventType) {
|
||||
XMLStreamConstants.START_ELEMENT -> {
|
||||
with(event.asStartElement()) {
|
||||
startD = name.localPart == "d"
|
||||
if (startD) {
|
||||
p = getAttributeByName(P).value
|
||||
}
|
||||
}
|
||||
}
|
||||
XMLStreamConstants.CHARACTERS -> {
|
||||
//如果前一个解析到的是 d 标签, 那么此处得到的一定是 d 标签的 body
|
||||
if (startD) {
|
||||
val danmaku = with(StringTokenizer(p, ",")) {
|
||||
Danmaku(
|
||||
nextToken().toLong(),
|
||||
nextToken(),
|
||||
nextToken().toLong(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toLong(),
|
||||
nextToken(),
|
||||
nextToken(),
|
||||
event.asCharacters().data
|
||||
)
|
||||
}
|
||||
yield(danmaku)
|
||||
}
|
||||
endObject()
|
||||
danmakuFlags[danmakuId] = flag
|
||||
}
|
||||
endArray()
|
||||
}
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
return danmakuFlags to danmakus
|
||||
endObject()
|
||||
}
|
||||
|
||||
//常量, 用于加快速度
|
||||
@JvmStatic
|
||||
private val P = QName("p")
|
||||
//json 解析完毕后, 剩下的内容是一个 gzip 压缩过的 xml
|
||||
val reader = GZIPInputStream(inputStream).reader()
|
||||
//流式解析 xml
|
||||
val xmlEventReader = XMLInputFactory.newInstance().createXMLEventReader(reader)
|
||||
//lazy sequence
|
||||
val danmakus = sequence {
|
||||
var startD = false //之前解析到的 element 是否是 d
|
||||
var p: String? = null //之前解析到的 p 的值
|
||||
while (xmlEventReader.hasNext()) {
|
||||
val event = xmlEventReader.nextEvent()
|
||||
when (event.eventType) {
|
||||
XMLStreamConstants.START_ELEMENT -> {
|
||||
with(event.asStartElement()) {
|
||||
startD = name.localPart == "d"
|
||||
if (startD) {
|
||||
p = getAttributeByName(P).value
|
||||
}
|
||||
}
|
||||
}
|
||||
XMLStreamConstants.CHARACTERS -> {
|
||||
//如果前一个解析到的是 d 标签, 那么此处得到的一定是 d 标签的 body
|
||||
if (startD) {
|
||||
val danmaku = with(StringTokenizer(p, ",")) {
|
||||
Danmaku(
|
||||
nextToken().toLong(),
|
||||
nextToken(),
|
||||
nextToken().toLong(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toInt(),
|
||||
nextToken().toLong(),
|
||||
nextToken(),
|
||||
nextToken(),
|
||||
event.asCharacters().data
|
||||
)
|
||||
}
|
||||
yield(danmaku)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return danmakuFlags to danmakus
|
||||
}
|
||||
|
||||
//常量, 用于加快速度
|
||||
private val P = QName("p")
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ fun FormBody.Builder.addAllEncoded(formBody: FormBody): FormBody.Builder {
|
||||
return this
|
||||
}
|
||||
|
||||
typealias ParamExpression = Pair<String, () -> String?>
|
||||
internal typealias ParamExpression = Pair<String, () -> String?>
|
||||
|
||||
internal inline fun Array<out ParamExpression>.forEachNonNull(action: (String, String) -> Unit) {
|
||||
forEach { (name, valueExpression) ->
|
||||
|
@ -0,0 +1,253 @@
|
||||
package com.hiczp.bilibili.api.thirdpart.commons;
|
||||
|
||||
//用于减少包引入
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* This is a stream that will only supply bytes up to a certain length - if its
|
||||
* position goes above that, it will stop.
|
||||
* <p>
|
||||
* This is useful to wrap ServletInputStreams. The ServletInputStream will block
|
||||
* if you try to read content from it that isn't there, because it doesn't know
|
||||
* whether the content hasn't arrived yet or whether the content has finished.
|
||||
* So, one of these, initialized with the Content-length sent in the
|
||||
* ServletInputStream's header, will stop it blocking, providing it's been sent
|
||||
* with a correct content length.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public class BoundedInputStream extends InputStream {
|
||||
private static int EOF = -1;
|
||||
|
||||
/**
|
||||
* the wrapped input stream
|
||||
*/
|
||||
private final InputStream in;
|
||||
|
||||
/**
|
||||
* the max length to provide
|
||||
*/
|
||||
private final long max;
|
||||
|
||||
/**
|
||||
* the number of bytes already returned
|
||||
*/
|
||||
private long pos = 0;
|
||||
|
||||
/**
|
||||
* the marked position
|
||||
*/
|
||||
private long mark = EOF;
|
||||
|
||||
/**
|
||||
* flag if close should be propagated
|
||||
*/
|
||||
private boolean propagateClose = true;
|
||||
|
||||
/**
|
||||
* Creates a new <code>BoundedInputStream</code> that wraps the given input
|
||||
* stream and limits it to a certain size.
|
||||
*
|
||||
* @param in The wrapped input stream
|
||||
* @param size The maximum number of bytes to return
|
||||
*/
|
||||
public BoundedInputStream(final InputStream in, final long size) {
|
||||
// Some badly designed methods - eg the servlet API - overload length
|
||||
// such that "-1" means stream finished
|
||||
this.max = size;
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new <code>BoundedInputStream</code> that wraps the given input
|
||||
* stream and is unlimited.
|
||||
*
|
||||
* @param in The wrapped input stream
|
||||
*/
|
||||
public BoundedInputStream(final InputStream in) {
|
||||
this(in, EOF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read()</code> method if
|
||||
* the current position is less than the limit.
|
||||
*
|
||||
* @return the byte read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (max >= 0 && pos >= max) {
|
||||
return EOF;
|
||||
}
|
||||
final int result = in.read();
|
||||
pos++;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read(byte[])</code> method.
|
||||
*
|
||||
* @param b the buffer to read the bytes into
|
||||
* @return the number of bytes read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read(final byte[] b) throws IOException {
|
||||
return this.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read(byte[], int, int)</code> method.
|
||||
*
|
||||
* @param b the buffer to read the bytes into
|
||||
* @param off The start offset
|
||||
* @param len The number of bytes to read
|
||||
* @return the number of bytes read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read(final byte[] b, final int off, final int len) throws IOException {
|
||||
if (max >= 0 && pos >= max) {
|
||||
return EOF;
|
||||
}
|
||||
final long maxRead = max >= 0 ? Math.min(len, max - pos) : len;
|
||||
final int bytesRead = in.read(b, off, (int) maxRead);
|
||||
|
||||
if (bytesRead == EOF) {
|
||||
return EOF;
|
||||
}
|
||||
|
||||
pos += bytesRead;
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>skip(long)</code> method.
|
||||
*
|
||||
* @param n the number of bytes to skip
|
||||
* @return the actual number of bytes skipped
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public long skip(final long n) throws IOException {
|
||||
final long toSkip = max >= 0 ? Math.min(n, max - pos) : n;
|
||||
final long skippedBytes = in.skip(toSkip);
|
||||
pos += skippedBytes;
|
||||
return skippedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
if (max >= 0 && pos >= max) {
|
||||
return 0;
|
||||
}
|
||||
return in.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>toString()</code> method.
|
||||
*
|
||||
* @return the delegate's <code>toString()</code>
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return in.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>close()</code> method
|
||||
* if {@link #isPropagateClose()} is {@code true}.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (propagateClose) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>reset()</code> method.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
in.reset();
|
||||
pos = mark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>mark(int)</code> method.
|
||||
*
|
||||
* @param readlimit read ahead limit
|
||||
*/
|
||||
@Override
|
||||
public synchronized void mark(final int readlimit) {
|
||||
in.mark(readlimit);
|
||||
mark = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>markSupported()</code> method.
|
||||
*
|
||||
* @return true if mark is supported, otherwise false
|
||||
*/
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return in.markSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the {@link #close()} method
|
||||
* should propagate to the underling {@link InputStream}.
|
||||
*
|
||||
* @return {@code true} if calling {@link #close()}
|
||||
* propagates to the <code>close()</code> method of the
|
||||
* underlying stream or {@code false} if it does not.
|
||||
*/
|
||||
public boolean isPropagateClose() {
|
||||
return propagateClose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the {@link #close()} method
|
||||
* should propagate to the underling {@link InputStream}.
|
||||
*
|
||||
* @param propagateClose {@code true} if calling
|
||||
* {@link #close()} propagates to the <code>close()</code>
|
||||
* method of the underlying stream or
|
||||
* {@code false} if it does not.
|
||||
*/
|
||||
public void setPropagateClose(final boolean propagateClose) {
|
||||
this.propagateClose = propagateClose;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class DanmakuTest {
|
||||
runBlocking {
|
||||
//著名的炮姐视频 你指尖跃动的电光是我此生不变的信仰
|
||||
val responseBody = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await()
|
||||
timer {
|
||||
printTimeMillis {
|
||||
DanmakuParser.parse(responseBody.byteStream()).second.forEach {
|
||||
println("[${it.time}] ${it.calculatePossibleUserIds()} ${it.content}")
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class FetchReplyTest {
|
||||
val aid = 150998L
|
||||
val bilibiliClient = BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BASIC)
|
||||
|
||||
timer {
|
||||
printTimeMillis {
|
||||
var total: Int? = null
|
||||
var next: Long = 0
|
||||
runBlocking {
|
||||
@ -44,7 +44,7 @@ class FetchReplyTest {
|
||||
//如果没有评论则不做进一步操作
|
||||
if (total == null) {
|
||||
println("<NoReply>")
|
||||
return@timer
|
||||
return@printTimeMillis
|
||||
}
|
||||
|
||||
val pages = list {
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.hiczp.bilibili.api.test
|
||||
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* 土制切面
|
||||
* 输出执行时间
|
||||
*/
|
||||
inline fun timer(block: () -> Unit) {
|
||||
val start = System.currentTimeMillis()
|
||||
block()
|
||||
val end = System.currentTimeMillis()
|
||||
println("Done in ${end - start} ms")
|
||||
inline fun printTimeMillis(block: () -> Unit) {
|
||||
val time = measureTimeMillis(block)
|
||||
println("Done in $time ms")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user