优化代码

This commit is contained in:
czp3009 2019-02-28 13:04:51 +08:00
parent ac182bd791
commit e196b5b141
8 changed files with 162 additions and 85 deletions

View File

@ -36,7 +36,7 @@ dependencies {
compileKotlin {
kotlinOptions {
jvmTarget = jvm_target
freeCompilerArgs = ["-Xjvm-default=enable"]
freeCompilerArgs = ["-Xjvm-default=enable", "-Xuse-experimental=kotlin.Experimental"]
}
}
compileTestKotlin {

View File

@ -0,0 +1,10 @@
package com.hiczp.bilibili.api
import kotlin.experimental.ExperimentalTypeInference
@UseExperimental(ExperimentalTypeInference::class)
internal inline fun <T> list(@BuilderInference block: MutableList<T>.() -> Unit): List<T> {
val list = ArrayList<T>()
block(list)
return list
}

View File

@ -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<Map<Long, Int>, List<Danmaku>> {
fun parse(inputStream: InputStream): Pair<Map<Long, Int>, Sequence<Danmaku>> {
//Json 的长度
val jsonLength = inputStream.readUInt()
@ -74,48 +73,47 @@ class DanmakuParser {
//json 解析完毕后, 剩下的内容是一个 gzip 压缩过的 xml
val reader = GZIPInputStream(inputStream).reader()
val danmakus = LinkedList<Danmaku>()
//流式解析 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
}

View File

@ -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<BangumiMore>
/**
* 发送评论
* 如果发送根评论则 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<SendReplyResponse>
}

View File

@ -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
)
}

View File

@ -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}")
}
}
}
}

View File

@ -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("<NoReply>")
} else {
val results = ArrayList<Deferred<List<Pair<Reply.Data.Reply, Deferred<ChildReply>?>>>>()
//访问每个页
//如果根评论数量刚好能被 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("<NoReply>")
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")
}
}

View File

@ -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")
}