From e196b5b1413f9ccf45ab1895d24436d0fcdf864e Mon Sep 17 00:00:00 2001 From: czp3009 Date: Thu, 28 Feb 2019 13:04:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../hiczp/bilibili/api/CollectionExtension.kt | 10 +++ .../bilibili/api/danmaku/DanmakuParser.kt | 68 +++++++-------- .../com/hiczp/bilibili/api/main/MainAPI.kt | 27 +++++- .../api/main/model/SendReplyResponse.kt | 33 +++++++ .../hiczp/bilibili/api/test/DanmakuTest.kt | 11 +-- .../hiczp/bilibili/api/test/FetchReplyTest.kt | 85 ++++++++++--------- .../hiczp/bilibili/api/test/TestExtension.kt | 11 +++ 8 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 src/main/kotlin/com/hiczp/bilibili/api/CollectionExtension.kt create mode 100644 src/main/kotlin/com/hiczp/bilibili/api/main/model/SendReplyResponse.kt create mode 100644 src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt diff --git a/build.gradle b/build.gradle index 20ae074..42ce8ff 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ dependencies { compileKotlin { kotlinOptions { jvmTarget = jvm_target - freeCompilerArgs = ["-Xjvm-default=enable"] + freeCompilerArgs = ["-Xjvm-default=enable", "-Xuse-experimental=kotlin.Experimental"] } } compileTestKotlin { diff --git a/src/main/kotlin/com/hiczp/bilibili/api/CollectionExtension.kt b/src/main/kotlin/com/hiczp/bilibili/api/CollectionExtension.kt new file mode 100644 index 0000000..171781d --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/CollectionExtension.kt @@ -0,0 +1,10 @@ +package com.hiczp.bilibili.api + +import kotlin.experimental.ExperimentalTypeInference + +@UseExperimental(ExperimentalTypeInference::class) +internal inline fun list(@BuilderInference block: MutableList.() -> Unit): List { + val list = ArrayList() + block(list) + return list +} 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 8bea41a..38b94f9 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/danmaku/DanmakuParser.kt @@ -31,12 +31,11 @@ class DanmakuParser { * 解析弹幕文件 * * @param inputStream 输入流, 可以指向任何位置 - * @param autoClose 是否在解析后自动关闭流 * - * @return 返回 flags map 与 弹幕列表. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间. + * @return 返回 flags map 与 弹幕序列. 注意, 原始的弹幕顺序是按发送时间来排的, 而非播放器时间. */ @JvmStatic - fun parser(inputStream: InputStream, autoClose: Boolean = true): Pair, List> { + fun parse(inputStream: InputStream): Pair, Sequence> { //Json 的长度 val jsonLength = inputStream.readUInt() @@ -74,48 +73,47 @@ class DanmakuParser { //json 解析完毕后, 剩下的内容是一个 gzip 压缩过的 xml val reader = GZIPInputStream(inputStream).reader() - val danmakus = LinkedList() //流式解析 xml val xmlEventReader = XMLInputFactory.newInstance().createXMLEventReader(reader) - 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 + //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 - ) + 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) } - danmakus.add(danmaku) } } } } - //自动关闭流 - if (autoClose) inputStream.close() - return danmakuFlags to danmakus } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt b/src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt index 7ab9fd0..36f9531 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/main/MainAPI.kt @@ -2,8 +2,7 @@ package com.hiczp.bilibili.api.main import com.hiczp.bilibili.api.main.model.* import kotlinx.coroutines.Deferred -import retrofit2.http.GET -import retrofit2.http.Query +import retrofit2.http.* /** * 这也是总站 API @@ -138,4 +137,28 @@ interface MainAPI { @Query("size") size: Int = 10, @Query("wid") wid: String? = "78,79,80,81,59" ): Deferred + + /** + * 发送评论 + * 如果发送根评论则 root 和 parent 为 null + * 如果发送子评论则 root 和 parent 均为根评论的 id + * 如果在子评论中 at 别人(即对子评论进行评论), 那么 root 为所属根评论的 id, parent 为所 at 的那个评论的 id + * at 别人时, 评论的内容必须符合以下格式 "回复 @$username :$message" + * + * @param message 发送的内容 + * @param oid aid + * @param parent 父评论 id + * @param root 根评论 id + */ + @POST("/x/v2/reply/add") + @FormUrlEncoded + fun sendReply( + @Field("from") from: Int? = null, + @Field("message") message: String, + @Field("oid") oid: Long, + @Field("parent") parent: Long? = null, + @Field("plat") plat: Int = 2, + @Field("root") root: Long? = null, + @Field("type") type: Int = 1 + ): Deferred } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/main/model/SendReplyResponse.kt b/src/main/kotlin/com/hiczp/bilibili/api/main/model/SendReplyResponse.kt new file mode 100644 index 0000000..2bd4e07 --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/main/model/SendReplyResponse.kt @@ -0,0 +1,33 @@ +package com.hiczp.bilibili.api.main.model + +import com.google.gson.annotations.SerializedName + +data class SendReplyResponse( + @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("dialog") + var dialog: Long, // 0 + @SerializedName("dialog_str") + var dialogStr: String, // 0 + @SerializedName("parent") + var parent: Long, // 0 + @SerializedName("parent_str") + var parentStr: String, // 0 + @SerializedName("root") + var root: Long, // 0 + @SerializedName("root_str") + var rootStr: String, // 0 + @SerializedName("rpid") + var rpid: Long, // 1422858564 + @SerializedName("rpid_str") + var rpidStr: String // 1422858564 + ) +} 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 f2ea9d9..a35091c 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/DanmakuTest.kt @@ -5,15 +5,16 @@ import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test class DanmakuTest { - //339.6kib 的弹幕文件在 0.5s 内解析完毕, 通常视频的弹幕不会超过这个容量 + //339.6kib 的弹幕文件在 0.5s 内解析完毕(i5-4200H), 通常视频的弹幕不会超过这个容量 @Test fun fetchAndParseDanmaku() { runBlocking { //著名的炮姐视频 你指尖跃动的电光是我此生不变的信仰 - bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await().let { - DanmakuParser.parser(it.byteStream()) - }.second.forEach { - println("[${it.time}] ${it.content}") + val responseBody = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await() + timer { + DanmakuParser.parse(responseBody.byteStream()).second.forEach { + println("[${it.time}] ${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 7c430cc..dfdff59 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/FetchReplyTest.kt @@ -1,12 +1,11 @@ package com.hiczp.bilibili.api.test import com.hiczp.bilibili.api.BilibiliClient -import com.hiczp.bilibili.api.main.model.ChildReply -import com.hiczp.bilibili.api.main.model.Reply -import kotlinx.coroutines.Deferred +import com.hiczp.bilibili.api.list import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking +import okhttp3.logging.HttpLoggingInterceptor import org.junit.jupiter.api.Test class FetchReplyTest { @@ -27,59 +26,61 @@ class FetchReplyTest { //打印一个视频下全部的评论 @Test fun printAllReplies() { - val start = System.currentTimeMillis() - val aid = 150998L - val bilibiliClient = BilibiliClient() + val bilibiliClient = BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BASIC) - var total: Int? = null - var next: Long = 0 - runBlocking { - //pageSize=1 来获得评论总楼层数量 - val reply = bilibiliClient.mainAPI.reply(oid = aid, pageSize = 1).await() - //得到评论总数(根评论+子评论) - total = reply.data.cursor.allCount - //最后一楼 - next = reply.data.cursor.next - } + timer { + var total: Int? = null + var next: Long = 0 + runBlocking { + //pageSize=1 来获得评论总楼层数量 + val reply = bilibiliClient.mainAPI.reply(oid = aid, pageSize = 1).await() + //得到评论总数(根评论+子评论) + total = reply.data.cursor.allCount + //最后一楼 + next = reply.data.cursor.next + } - //如果没有评论则不做进一步操作 - if (total == null) { - println("") - } else { - val results = ArrayList?>>>>() - //访问每个页 - //如果根评论数量刚好能被 50 整除, 那么最后一次访问时的 next 为 1, 这会导致 replies 为 null - //因此 downTo 2 - for (i in next + 1 downTo 2 step 50) { - GlobalScope.async { - val replies = bilibiliClient.mainAPI.reply(oid = aid, next = i, pageSize = 50).await().data.replies - //获取该页的评论(复数)的子评论 - replies!!.map { - it to if (it.rcount == 0) { - null - } else { - bilibiliClient.mainAPI.childReply(oid = aid, root = it.rpid, size = Int.MAX_VALUE) + //如果没有评论则不做进一步操作 + if (total == null) { + println("") + return@timer + } + + val pages = list { + //访问每个页 + //如果根评论数量刚好能被 50 整除, 那么最后一次访问时的 next 为 1, 这会导致 replies 为 null + //因此 downTo 2 + for (i in next + 1 downTo 2 step 50) { + GlobalScope.async { + //一个页下的所有根评论 + val replies = bilibiliClient.mainAPI.reply(oid = aid, next = i, pageSize = 50).await().data.replies + //获取根评论(复数)的子评论 + replies!!.map { + it to if (it.rcount == 0) { + null + } else { + bilibiliClient.mainAPI.childReply(oid = aid, root = it.rpid, size = Int.MAX_VALUE).await().data.root.replies + } } + }.let { + add(it) } - }.let { - results.add(it) } } + //join runBlocking { - results.forEach { deferred -> - deferred.await().forEach { (reply, childReplyResponse) -> - println("#${reply.floor} [${reply.member.uname}] ${reply.content.message}") - childReplyResponse?.await()?.data?.root?.replies?.forEach { + pages.forEach { page -> + page.await().forEach { (rootReply, childReplies) -> + //输出这一页的评论 + println("#${rootReply.floor} [${rootReply.member.uname}] ${rootReply.content.message}") + childReplies?.forEach { println("└──#${it.floor} [${it.member.uname}] ${it.content.message}") } } } } } - - val end = System.currentTimeMillis() - println("Done in ${end - start} ms") } } diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt new file mode 100644 index 0000000..b91bd14 --- /dev/null +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/TestExtension.kt @@ -0,0 +1,11 @@ +package com.hiczp.bilibili.api.test + +/** + * 土制切面 + */ +inline fun timer(block: () -> Unit) { + val start = System.currentTimeMillis() + block() + val end = System.currentTimeMillis() + println("Done in ${end - start} ms") +}