diff --git a/README.md b/README.md index 9b79f33..50c8edd 100644 --- a/README.md +++ b/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 diff --git a/build.gradle b/build.gradle index 6779002..0f41ef8 100644 --- a/build.gradle +++ b/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' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 56a43bc..b9d51f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt index dc9de89..e0ab659 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt @@ -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 createAPI( diff --git a/src/main/kotlin/com/hiczp/bilibili/api/IOExtension.kt b/src/main/kotlin/com/hiczp/bilibili/api/IOExtension.kt index 34b6ed0..c978d65 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/IOExtension.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/IOExtension.kt @@ -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) diff --git a/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt b/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt index 38b94f9..bd1bcad 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt @@ -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, Sequence> { - //Json 的长度 - val jsonLength = inputStream.readUInt() +object DanmakuParser { + /** + * 解析弹幕文件 + * + * @param inputStream 输入流, 可以指向任何位置 + * + * @return 返回 flags map 与 弹幕序列. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间. + */ + fun parse(inputStream: InputStream): Pair, Sequence> { + //Json 的长度 + val jsonLength = inputStream.readUInt() - //弹幕ID-Flag - val danmakuFlags = HashMap() - //gson 会从 reader 中自行缓冲 1024 个字符, 这会导致额外的字符被消费. 因此要限制其读取数量 - //流式解析 Json - with(JsonReader(inputStream.bounded(jsonLength).reader())) { - beginObject() - while (hasNext()) { - when (nextName()) { - "dmflags" -> { - beginArray() + //弹幕ID-Flag + val danmakuFlags = HashMap() + //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") } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt index e8ded36..c841add 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt @@ -42,7 +42,7 @@ fun FormBody.Builder.addAllEncoded(formBody: FormBody): FormBody.Builder { return this } -typealias ParamExpression = Pair String?> +internal typealias ParamExpression = Pair String?> internal inline fun Array.forEachNonNull(action: (String, String) -> Unit) { forEach { (name, valueExpression) -> diff --git a/src/main/kotlin/com/hiczp/bilibili/api/thirdpart/commons/BoundedInputStream.java b/src/main/kotlin/com/hiczp/bilibili/api/thirdpart/commons/BoundedInputStream.java new file mode 100644 index 0000000..410f792 --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/thirdpart/commons/BoundedInputStream.java @@ -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. + *

+ * 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 BoundedInputStream 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 BoundedInputStream 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 read() 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 read(byte[]) 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 read(byte[], int, int) 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 skip(long) 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 toString() method. + * + * @return the delegate's toString() + */ + @Override + public String toString() { + return in.toString(); + } + + /** + * Invokes the delegate's close() 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 reset() method. + * + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + in.reset(); + pos = mark; + } + + /** + * Invokes the delegate's mark(int) method. + * + * @param readlimit read ahead limit + */ + @Override + public synchronized void mark(final int readlimit) { + in.mark(readlimit); + mark = pos; + } + + /** + * Invokes the delegate's markSupported() 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 close() 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 close() + * method of the underlying stream or + * {@code false} if it does not. + */ + public void setPropagateClose(final boolean propagateClose) { + this.propagateClose = propagateClose; + } +} + diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt index ed5de64..e4c3534 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt @@ -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}") } diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt index dfdff59..7ec9752 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt @@ -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("") - return@timer + return@printTimeMillis } val pages = list { diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt index b91bd14..deb0069 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt @@ -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") }