mirror of
https://github.com/aaa1115910/bv.git
synced 2025-05-17 13:15:55 +08:00
更新接口
This commit is contained in:
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@ -8,4 +8,5 @@
|
||||
/navEditor.xml
|
||||
/assetWizardSettings.xml
|
||||
/misc.xml
|
||||
/compiler.xml
|
||||
/compiler.xml
|
||||
/inspectionProfiles/Project_Default.xml
|
@ -81,6 +81,7 @@ dependencies {
|
||||
implementation(libs.logging)
|
||||
implementation(libs.material)
|
||||
implementation(libs.slf4j.android.mvysny)
|
||||
implementation(project(mapOf("path" to ":bili-api")))
|
||||
androidTestImplementation(platform("${androidx.compose.bom.get()}"))
|
||||
androidTestImplementation(androidx.compose.ui.test.junit4)
|
||||
debugImplementation(androidx.compose.ui.test.manifest)
|
||||
|
@ -1,820 +0,0 @@
|
||||
package dev.aaa1115910.bv
|
||||
|
||||
import android.util.Xml
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.compression.ContentEncoding
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import mu.KotlinLogging
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
|
||||
object BiliApi {
|
||||
private var endPoint: String = ""
|
||||
private lateinit var client: HttpClient
|
||||
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
init {
|
||||
createClient()
|
||||
}
|
||||
|
||||
private fun createClient() {
|
||||
client = HttpClient(OkHttp) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
})
|
||||
}
|
||||
install(ContentEncoding) {
|
||||
deflate(1.0F)
|
||||
gzip(0.9F)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门视频列表
|
||||
*/
|
||||
suspend fun getPopularVideoData(
|
||||
pageNumber: Int = 1,
|
||||
pageSize: Int = 20
|
||||
): PopularVideosResponse = client.get("https://api.bilibili.com/x/web-interface/popular") {
|
||||
parameter("pn", pageNumber)
|
||||
parameter("ps", pageSize)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取视频详细信息
|
||||
*/
|
||||
suspend fun getVideoInfo(
|
||||
av: Int? = null,
|
||||
bv: String? = null
|
||||
): VideoInfoResponse = client.get("https://api.bilibili.com/x/web-interface/view") {
|
||||
parameter("aid", av)
|
||||
parameter("bvid", bv)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取视频流
|
||||
*/
|
||||
suspend fun getVideoPlayUrl(
|
||||
av: Int? = null,
|
||||
bv: Int? = null,
|
||||
cid: Int,
|
||||
qn: Int? = null,
|
||||
fnval: Int? = null,
|
||||
fnver: Int? = null,
|
||||
fourk: Int? = 0,
|
||||
session: String? = null,
|
||||
otype: String = "json",
|
||||
type: String = "",
|
||||
platform: String = "oc"
|
||||
): PlayUrlResponse = client.get("https://api.bilibili.com/x/player/playurl") {
|
||||
parameter("avid", av)
|
||||
parameter("bvid", bv)
|
||||
parameter("cid", cid)
|
||||
parameter("qn", qn)
|
||||
parameter("fnval", fnval)
|
||||
parameter("fnver", fnver)
|
||||
parameter("fourk", fourk)
|
||||
parameter("session", session)
|
||||
parameter("otype", otype)
|
||||
parameter("type", type)
|
||||
parameter("platform", platform)
|
||||
}.body()
|
||||
|
||||
suspend fun getDanmakuXml(
|
||||
cid: Int
|
||||
): DanmakuResponse {
|
||||
val xmlChannel = client.get("https://api.bilibili.com/x/v1/dm/list.so") {
|
||||
parameter("oid", cid)
|
||||
}.bodyAsChannel()
|
||||
|
||||
val xml = Xml.newPullParser()
|
||||
xml.setInput(xmlChannel.toInputStream().reader())
|
||||
|
||||
var chatserver = ""
|
||||
var chatId = 0
|
||||
var maxLimit = 0
|
||||
var state = 0
|
||||
var realName = 0
|
||||
var source = ""
|
||||
|
||||
val data = mutableListOf<DanmakuData>()
|
||||
|
||||
var eventType = xml.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlPullParser.START_TAG -> {
|
||||
when (xml.name) {
|
||||
"chatserver" -> {
|
||||
xml.next()
|
||||
chatserver = xml.text
|
||||
}
|
||||
|
||||
"chatid" -> {
|
||||
xml.next()
|
||||
chatId = xml.text.toInt()
|
||||
}
|
||||
|
||||
"maxlimit" -> {
|
||||
xml.next()
|
||||
maxLimit = xml.text.toInt()
|
||||
}
|
||||
|
||||
"state" -> {
|
||||
xml.next()
|
||||
state = xml.text.toInt()
|
||||
}
|
||||
|
||||
"real_name" -> {
|
||||
xml.next()
|
||||
realName = xml.text.toInt()
|
||||
}
|
||||
|
||||
"source" -> {
|
||||
xml.next()
|
||||
source = xml.text
|
||||
}
|
||||
|
||||
"d" -> {
|
||||
val p = xml.getAttributeValue(0)
|
||||
xml.next()
|
||||
val text = xml.text
|
||||
data.add(DanmakuData.fromString(p, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = xml.next();
|
||||
}
|
||||
|
||||
val response = DanmakuResponse(chatserver, chatId, maxLimit, state, realName, source, data)
|
||||
return response
|
||||
}
|
||||
|
||||
/*
|
||||
suspend fun getDanmakuXml(
|
||||
cid: Int
|
||||
) :String{
|
||||
val url="https://api.bilibili.com/x/v1/dm/list.so?oid=$cid"
|
||||
val okhttp=OkHttpClient()
|
||||
val call = okhttp.newCall(Request.Builder().url(url).build())
|
||||
val body= call.execute().body!!.string()
|
||||
val xmlFormat = XML {
|
||||
unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() }
|
||||
}
|
||||
val a = xmlFormat.decodeFromString(DanmakuResponse.serializer(), body)
|
||||
|
||||
return a.toString()
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
fun main() {
|
||||
runBlocking {
|
||||
val data = BiliApi.getDanmakuXml(cid = 267714)
|
||||
println(data)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PopularVideosResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: PopularVideoData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VideoInfoResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: VideoInfo
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PopularVideoData(
|
||||
val list: List<VideoInfo>,
|
||||
@SerialName("no_more")
|
||||
val noMore: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频详细信息
|
||||
*
|
||||
* @param bvid 稿件bvid
|
||||
* @param aid 稿件avid
|
||||
* @param videos 稿件分P总数 默认为1
|
||||
* @param tid 分区tid
|
||||
* @param tname 子分区名称
|
||||
* @param copyright 视频类型 1:原创 2:转载
|
||||
* @param pic 稿件封面图片url
|
||||
* @param title 稿件标题
|
||||
* @param pubdate 稿件发布时间 秒级时间戳
|
||||
* @param ctime 用户投稿时间 秒级时间戳
|
||||
* @param desc 视频简介
|
||||
* @param state 视频状态
|
||||
* @param duration 稿件总时长(所有分P) 单位为秒
|
||||
* @param forward 撞车视频跳转avid 仅撞车视频存在此字段
|
||||
* @param missionId 稿件参与的活动id
|
||||
* @param redirectUrl 稿重定向url 仅番剧或影视视频存在此字段,用于番剧&影视的av/bv->ep
|
||||
* @param rights 视频属性标志
|
||||
* @param owner 视频UP主信息
|
||||
* @param stat 视频状态数
|
||||
* @param dynamic 视频同步发布的的动态的文字内容
|
||||
* @param cid 视频1P cid
|
||||
* @param dimension 视频1P分辨率
|
||||
* @param premiere null
|
||||
* @param teenageMode
|
||||
* @param isChargeableSeason
|
||||
* @param isStory
|
||||
* @param noCache
|
||||
* @param pages 视频分P列表
|
||||
* @param subtitle 视频CC字幕信息
|
||||
* @param staff 合作成员列表 非合作视频无此项
|
||||
* @param isSeasonDisplay
|
||||
* @param userGarb 用户装扮信息
|
||||
* @param honorReply
|
||||
* @param likeIcon
|
||||
* @param shortLink
|
||||
* @param shortLinkV2
|
||||
* @param firstFrame
|
||||
* @param pubLocation
|
||||
* @param seasonType
|
||||
* @param isOgv
|
||||
* @param ogvInfo
|
||||
*
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoInfo(
|
||||
val bvid: String,
|
||||
val aid: Int,
|
||||
val videos: Int,
|
||||
val tid: Int,
|
||||
val tname: String,
|
||||
val copyright: Int,
|
||||
val pic: String,
|
||||
val title: String,
|
||||
val pubdate: Int,
|
||||
val ctime: Int,
|
||||
val desc: String,
|
||||
val state: Int,
|
||||
val duration: Int,
|
||||
val forward: Int? = null,
|
||||
@SerialName("mission_id")
|
||||
val missionId: Int? = null,
|
||||
@SerialName("redirect_url")
|
||||
val redirectUrl: String? = null,
|
||||
val rights: VideoRights,
|
||||
val owner: VideoOwner,
|
||||
val stat: VideoStat,
|
||||
val dynamic: String,
|
||||
val cid: Int,
|
||||
val dimension: Dimension,
|
||||
val premiere: String? = null,
|
||||
@SerialName("teenage_mode")
|
||||
val teenageMode: Int = 0,
|
||||
@SerialName("is_chargeable_season")
|
||||
val isChargeableSeason: Boolean = false,
|
||||
@SerialName("is_story")
|
||||
val isStory: Boolean = false,
|
||||
@SerialName("no_cache")
|
||||
val noCache: Boolean = false,
|
||||
val pages: List<VideoPage> = emptyList(),
|
||||
val subtitle: Subtitle? = null,
|
||||
val staff: List<Staff> = emptyList(),
|
||||
@SerialName("is_season_display")
|
||||
val isSeasonDisplay: Boolean = false,
|
||||
@SerialName("user_garb")
|
||||
val userGarb: UserGarb? = null,
|
||||
@SerialName("honor_reply")
|
||||
val honorReply: HonorReply? = null,
|
||||
@SerialName("like_icon")
|
||||
val likeIcon: String? = null,
|
||||
@SerialName("short_link")
|
||||
val shortLink: String? = null,
|
||||
@SerialName("short_link_v2")
|
||||
val shortLinkV2: String? = null,
|
||||
@SerialName("first_frame")
|
||||
val firstFrame: String? = null,
|
||||
@SerialName("pub_location")
|
||||
val pubLocation: String? = null,
|
||||
@SerialName("season_type")
|
||||
val seasonType: Int? = null,
|
||||
@SerialName("is_ogv")
|
||||
val isOgv: Boolean = false,
|
||||
@SerialName("ogv_info")
|
||||
val ogvInfo: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频属性标志
|
||||
*
|
||||
* @param bp 是否允许承包
|
||||
* @param elec 是否支持充电
|
||||
* @param download 是否允许下载
|
||||
* @param movie 是否电影
|
||||
* @param pay 是否PGC付费
|
||||
* @param hd5 是否有高码率
|
||||
* @param noReprint 是否显示“禁止转载”标志
|
||||
* @param autoplay 是否自动播放
|
||||
* @param ugcPay 是否UGC付费
|
||||
* @param isCooperation 是否为联合投稿
|
||||
* @param ugcPayPreview
|
||||
* @param noBackground
|
||||
* @param cleanMode
|
||||
* @param isSteinGate 是否为互动视频
|
||||
* @param is360 是否为全景视频
|
||||
* @param noShare
|
||||
* @param arcPay
|
||||
* @param payFreeWatch
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoRights(
|
||||
val bp: Int,
|
||||
val elec: Int,
|
||||
val download: Int,
|
||||
val movie: Int,
|
||||
val pay: Int,
|
||||
val hd5: Int,
|
||||
@SerialName("no_reprint")
|
||||
val noReprint: Int,
|
||||
val autoplay: Int,
|
||||
@SerialName("ugc_pay")
|
||||
val ugcPay: Int,
|
||||
@SerialName("is_cooperation")
|
||||
val isCooperation: Int,
|
||||
@SerialName("ugc_pay_preview")
|
||||
val ugcPayPreview: Int,
|
||||
@SerialName("no_background")
|
||||
val noBackground: Int,
|
||||
@SerialName("clean_mode")
|
||||
val cleanMode: Int? = null,
|
||||
@SerialName("is_stein_gate")
|
||||
val isSteinGate: Int? = null,
|
||||
@SerialName("is_360")
|
||||
val is360: Int? = null,
|
||||
@SerialName("no_share")
|
||||
val noShare: Int? = null,
|
||||
@SerialName("arc_pay")
|
||||
val arcPay: Int,
|
||||
@SerialName("pay_free_watch")
|
||||
val payFreeWatch: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频作者
|
||||
*
|
||||
* @param mid UP主mid
|
||||
* @param name UP主昵称
|
||||
* @param face UP主头像
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoOwner(
|
||||
val mid: Long,
|
||||
val name: String,
|
||||
val face: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频数据
|
||||
*
|
||||
* @param aid 稿件avid
|
||||
* @param view 播放数
|
||||
* @param danmaku 弹幕数
|
||||
* @param reply 评论数
|
||||
* @param favorite 收藏数
|
||||
* @param coin 投币数
|
||||
* @param share 分享数
|
||||
* @param nowRank 当前排名
|
||||
* @param hisRank 历史最高排行
|
||||
* @param like 获赞数
|
||||
* @param dislike 点踩数 恒为0
|
||||
* @param evaluation 视频评分
|
||||
* @param argueMsg 警告/争议提示信息
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoStat(
|
||||
val aid: Int,
|
||||
val view: Int,
|
||||
val danmaku: Int,
|
||||
val reply: Int,
|
||||
val favorite: Int,
|
||||
val coin: Int,
|
||||
val share: Int,
|
||||
@SerialName("now_rank")
|
||||
val nowRank: Int,
|
||||
@SerialName("his_rank")
|
||||
val hisRank: Int,
|
||||
val like: Int,
|
||||
val dislike: Int,
|
||||
val evaluation: String = "",
|
||||
@SerialName("argue_msg")
|
||||
val argueMsg: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* 分辨率
|
||||
*
|
||||
* @param width 当前分P 宽度
|
||||
* @param height 当前分P 高度
|
||||
* @param rotate 是否将宽高对换 0:正常,1:对换
|
||||
*/
|
||||
@Serializable
|
||||
data class Dimension(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val rotate: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频分P
|
||||
*
|
||||
* @param cid 分P cid
|
||||
* @param page 分P序号 从1开始
|
||||
* @param from 视频来源 vupload:普通上传(B站)
|
||||
* @param hunan:芒果TV
|
||||
* @param qq:腾讯
|
||||
* @param part 分P标题
|
||||
* @param duration 分P持续时间 单位为秒
|
||||
* @param vid 站外视频vid 仅站外视频有效
|
||||
* @param weblink 站外视频跳转url 仅站外视频有效
|
||||
* @param dimension 当前分P分辨率 部分较老视频无分辨率值
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoPage(
|
||||
val cid: Int,
|
||||
val page: Int,
|
||||
val from: String,
|
||||
val part: String,
|
||||
val duration: Int,
|
||||
val vid: String,
|
||||
val weblink: String,
|
||||
val dimension: Dimension
|
||||
)
|
||||
|
||||
/**
|
||||
* 字幕信息
|
||||
* @param allowSubmit 是否允许提交字幕
|
||||
* @param list 字幕列表
|
||||
*/
|
||||
@Serializable
|
||||
data class Subtitle(
|
||||
val allowSubmit: Boolean = false,
|
||||
val list: List<SubtitleListItem> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* 字幕列表项
|
||||
*
|
||||
* @param id 字幕id
|
||||
* @param lan 字幕语言
|
||||
* @param lanDoc 字幕语言名称
|
||||
* @param isLock 是否锁定
|
||||
* @param authorMid 字幕上传者mid
|
||||
* @param subtitleUrl json格式字幕文件url
|
||||
* @param author 字幕上传者信息
|
||||
*/
|
||||
@Serializable
|
||||
data class SubtitleListItem(
|
||||
val id: Long,
|
||||
val lan: String,
|
||||
@SerialName("lan_doc")
|
||||
val lanDoc: String,
|
||||
@SerialName("is_lock")
|
||||
val isLock: Boolean,
|
||||
@SerialName("author_mid")
|
||||
val authorMid: Int? = null,
|
||||
@SerialName("subtitle_url")
|
||||
val subtitleUrl: String,
|
||||
val author: SubtitleAuthor
|
||||
)
|
||||
|
||||
/**
|
||||
* 字幕作者
|
||||
*
|
||||
* @param mid 字幕上传者mid
|
||||
* @param name 字幕上传者昵称
|
||||
* @param sex 字幕上传者性别 男 女 保密
|
||||
* @param face 字幕上传者头像url
|
||||
* @param sign 字幕上传者签名
|
||||
* @param rank 10000 作用尚不明确
|
||||
* @param birthday 0 作用尚不明确
|
||||
* @param isFakeAccount 0 作用尚不明确
|
||||
* @param isDeleted 0 作用尚不明确
|
||||
*/
|
||||
@Serializable
|
||||
data class SubtitleAuthor(
|
||||
val mid: Int,
|
||||
val name: String,
|
||||
val sex: String,
|
||||
val face: String,
|
||||
val sign: String,
|
||||
val rank: Int,
|
||||
val birthday: Int,
|
||||
@SerialName("is_fake_account")
|
||||
val isFakeAccount: Int,
|
||||
@SerialName("is_deleted")
|
||||
val isDeleted: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 创作者个人信息
|
||||
*
|
||||
* @param mid 成员mid
|
||||
* @param title 成员名称
|
||||
* @param name 成员昵称
|
||||
* @param face 成员头像url
|
||||
* @param vip 成员大会员状态
|
||||
* @param official 成员认证信息
|
||||
* @param follower 成员粉丝数
|
||||
*/
|
||||
@Serializable
|
||||
data class Staff(
|
||||
val mid: Int,
|
||||
val title: String,
|
||||
val name: String,
|
||||
val face: String,
|
||||
val vip: Vip,
|
||||
val official: Official,
|
||||
val follower: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 会员信息
|
||||
*
|
||||
* @param type 成员会员类型 0:无 1:月会员 2:年会员
|
||||
* @param status 会员状态 0:无 1:有
|
||||
* @param themeType num 0
|
||||
*/
|
||||
@Serializable
|
||||
data class Vip(
|
||||
val type: Int,
|
||||
val status: Int,
|
||||
@SerialName("theme_type")
|
||||
val themeType: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
*
|
||||
* @param role 成员认证级别 0:无 1 2 7:个人认证 3 4 5 6:机构认证
|
||||
* @param title 成员认证名 无为空
|
||||
* @param desc 成员认证备注 无为空
|
||||
* @param type 成员认证类型 -1:无 0:有
|
||||
*/
|
||||
@Serializable
|
||||
data class Official(
|
||||
val role: Int,
|
||||
val title: String,
|
||||
val desc: String,
|
||||
val type: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 用户头像挂件
|
||||
*
|
||||
* @param urlImageAniCut
|
||||
*/
|
||||
@Serializable
|
||||
data class UserGarb(
|
||||
@SerialName("url_image_ani_cut")
|
||||
val urlImageAniCut: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 推荐理由
|
||||
*
|
||||
* @param honor
|
||||
*/
|
||||
@Serializable
|
||||
data class HonorReply(
|
||||
val honor: List<HonorReplyItem> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* 推荐信息
|
||||
*
|
||||
* @param aid 当前稿件aid
|
||||
* @param type 2:第?期每周必看 3:全站排行榜最高第?名 4:热门
|
||||
* @param desc 描述
|
||||
* @param weeklyRecommendNum
|
||||
*/
|
||||
@Serializable
|
||||
data class HonorReplyItem(
|
||||
val aid: Int,
|
||||
val type: Int,
|
||||
val desc: String,
|
||||
@SerialName("weekly_recommend_num")
|
||||
val weeklyRecommendNum: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class VideoQuality(val qn: Int, val displayName: String) {
|
||||
Q260P(6, "240P 极速"),
|
||||
Q360P(16, "360P 流畅"),
|
||||
Q480P(32, "480P 清晰"),
|
||||
Q720P(64, ""),
|
||||
Q720P60(74, ""),
|
||||
Q1080P(80, ""),
|
||||
Q1080PPlus(112, ""),
|
||||
Q1080P60(116, ""),
|
||||
Q4K(120, ""),
|
||||
HDR(125, ""),
|
||||
Dolby(126, ""),
|
||||
Q8K(127, "")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PlayUrlResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: PlayUrlData? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* @param from local 作用尚不明确
|
||||
* @param result suee 作用尚不明确
|
||||
* @param message 空 作用尚不明确
|
||||
* @param quality 当前的视频分辨率代码 值含义见上表
|
||||
* @param format 视频格式
|
||||
* @param timeLength 视频长度(毫秒值) 单位为毫秒 不同分辨率/格式可能有略微差异
|
||||
* @param acceptFormat 视频支持的全部格式 每项用,分隔
|
||||
* @param acceptDescription 视频支持的分辨率列表
|
||||
* @param acceptQuality 视频支持的分辨率代码列表 值含义见上表
|
||||
* @param videoCodecId 默认选择视频流的编码id 见视频编码代码
|
||||
* @param seekParam 固定值:start 作用尚不明确
|
||||
* @param seekType offset(dash、flv) second(mp4) 作用尚不明确
|
||||
* @param durl 视频分段 注:仅flv/mp4存在此项
|
||||
* @param dash dash音视频流信息 注:仅dash存在此项
|
||||
* @param supportFormats 支持格式的详细信息
|
||||
* @param high_format null
|
||||
* @param lastPlayTime 上次播放进度 毫秒值
|
||||
* @param lastPlayCid 上次播放分p的cid
|
||||
*/
|
||||
@Serializable
|
||||
data class PlayUrlData(
|
||||
val from: String,
|
||||
val result: String,
|
||||
val message: String,
|
||||
val quality: Int,
|
||||
val format: String,
|
||||
@SerialName("timelength")
|
||||
val timeLength: Int,
|
||||
@SerialName("accept_format")
|
||||
val acceptFormat: String,
|
||||
@SerialName("accept_description")
|
||||
val acceptDescription: List<String> = emptyList(),
|
||||
@SerialName("accept_quality")
|
||||
val acceptQuality: List<Int> = emptyList(),
|
||||
@SerialName("video_codecid")
|
||||
val videoCodecId: Int,
|
||||
@SerialName("seek_param")
|
||||
val seekParam: String,
|
||||
@SerialName("seek_type")
|
||||
val seekType: String,
|
||||
val durl: List<Durl> = emptyList(),
|
||||
val dash: Dash? = null,
|
||||
@SerialName("support_formats")
|
||||
val supportFormats: List<SupportFormat> = emptyList(),
|
||||
@SerialName("last_play_time")
|
||||
val lastPlayTime: Int,
|
||||
@SerialName("last_play_cid")
|
||||
val lastPlayCid: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频播放地址
|
||||
*
|
||||
* @param order 视频分段序号 某些视频会分为多个片段(从1顺序增长)
|
||||
* @param length 视频长度 单位为毫秒
|
||||
* @param size 视频大小 单位为Byte
|
||||
* @param ahead 空 作用尚不明确
|
||||
* @param vhead 空 作用尚不明确
|
||||
* @param url 视频流url 注:url内容存在转义符 有效时间为120min
|
||||
* @param backupUrl 备用视频流
|
||||
*/
|
||||
@Serializable
|
||||
data class Durl(
|
||||
val order: Int,
|
||||
val length: Int,
|
||||
val size: Int,
|
||||
val ahead: String,
|
||||
val vhead: String,
|
||||
val url: String,
|
||||
@SerialName("backup_url")
|
||||
val backupUrl: List<String> = emptyList()
|
||||
)
|
||||
|
||||
//TODO
|
||||
@Serializable
|
||||
data class Dash(
|
||||
val duration: Int,
|
||||
val minBufferTime: Float,
|
||||
val video: List<DashData> = emptyList(),
|
||||
val audio: List<DashData> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DashData(
|
||||
val id: Int,
|
||||
val baseUrl: String,
|
||||
val backupUrl: List<String> = emptyList(),
|
||||
val mimeType: String,
|
||||
val codecs: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val frameRate: String,
|
||||
val sar: String,
|
||||
val startWithSap: Int,
|
||||
@SerialName("segment_base")
|
||||
val segmentBase: SegmentBase,
|
||||
@SerialName("codecid")
|
||||
val codecId: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SegmentBase(
|
||||
val initialization: String,
|
||||
@SerialName("index_range")
|
||||
val indexRange: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 支持的视频格式
|
||||
*
|
||||
* @param quality 视频清晰度代码
|
||||
* @param format 视频格式
|
||||
* @param newDescription 格式描述
|
||||
* @param displayDesc 格式描述
|
||||
* @param superScript (?)
|
||||
* @param codecs 可用编码格式列表
|
||||
*/
|
||||
@Serializable
|
||||
data class SupportFormat(
|
||||
val quality: Int,
|
||||
val format: String,
|
||||
@SerialName("new_description")
|
||||
val newDescription: String,
|
||||
@SerialName("display_desc")
|
||||
val displayDesc: String,
|
||||
@SerialName("superscript")
|
||||
val superScript: String,
|
||||
val codecs: List<String>? = emptyList()
|
||||
)
|
||||
|
||||
data class DanmakuResponse(
|
||||
val chatserver: String,
|
||||
val chatId: Int,
|
||||
val maxLimit: Int,
|
||||
val state: Int,
|
||||
val realName: Int,
|
||||
val source: String,
|
||||
val data: List<DanmakuData> = emptyList()
|
||||
)
|
||||
|
||||
data class DanmakuData(
|
||||
val time: Float,
|
||||
val type: Int,
|
||||
val size: Int,
|
||||
val color: Int,
|
||||
val timestamp: Int,
|
||||
val pool: Int,
|
||||
val midHash: String,
|
||||
val dmid: Long,
|
||||
val level: Int,
|
||||
val text: String
|
||||
) {
|
||||
companion object {
|
||||
fun fromString(p: String, text: String): DanmakuData {
|
||||
val data = p.split(",")
|
||||
return DanmakuData(
|
||||
time = data[0].toFloat(),
|
||||
type = data[1].toInt(),
|
||||
size = data[2].toInt(),
|
||||
color = data[3].toInt(),
|
||||
timestamp = data[4].toInt(),
|
||||
pool = data[5].toInt(),
|
||||
midHash = data[6],
|
||||
dmid = data[7].toLong(),
|
||||
level = data[8].toInt(),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -44,6 +44,8 @@ import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
||||
import androidx.tv.foundation.lazy.grid.items
|
||||
import androidx.tv.foundation.lazy.list.TvLazyRow
|
||||
import coil.compose.AsyncImage
|
||||
import dev.aaa1115910.biliapi.BiliApi
|
||||
import dev.aaa1115910.biliapi.entity.video.VideoInfo
|
||||
import dev.aaa1115910.bv.component.HomeCarousel
|
||||
import dev.aaa1115910.bv.component.TopNav
|
||||
import dev.aaa1115910.bv.ui.theme.BVTheme
|
||||
|
@ -62,6 +62,10 @@ import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.BlurTransformation
|
||||
import dev.aaa1115910.biliapi.BiliApi
|
||||
import dev.aaa1115910.biliapi.entity.video.Dimension
|
||||
import dev.aaa1115910.biliapi.entity.video.VideoInfo
|
||||
import dev.aaa1115910.biliapi.entity.video.VideoPage
|
||||
import dev.aaa1115910.bv.component.FavoriteButton
|
||||
import dev.aaa1115910.bv.component.UpIcon
|
||||
import dev.aaa1115910.bv.ui.theme.BVTheme
|
||||
@ -237,7 +241,7 @@ fun VideoInfoData(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row{
|
||||
Row {
|
||||
Box(modifier = Modifier.focusable(true)) {}
|
||||
FavoriteButton(
|
||||
isFavorite = false,
|
||||
|
@ -16,7 +16,7 @@ import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import com.kuaishou.akdanmaku.data.DanmakuItemData
|
||||
import com.kuaishou.akdanmaku.ui.DanmakuPlayer
|
||||
import dev.aaa1115910.bv.BiliApi
|
||||
import dev.aaa1115910.biliapi.BiliApi
|
||||
import dev.aaa1115910.bv.Keys
|
||||
import dev.aaa1115910.bv.RequestState
|
||||
import dev.aaa1115910.bv.util.swapMap
|
||||
|
1
bili-api/.gitignore
vendored
Normal file
1
bili-api/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
22
bili-api/build.gradle.kts
Normal file
22
bili-api/build.gradle.kts
Normal file
@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
alias(gradleLibs.plugins.kotlin.jvm)
|
||||
alias(gradleLibs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
group = "dev.aaa1115910"
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(libs.ktor.content.negotiation)
|
||||
implementation(libs.ktor.core)
|
||||
implementation(libs.ktor.encoding)
|
||||
implementation(libs.ktor.okhttp)
|
||||
implementation(libs.ktor.serialization.kotlinx)
|
||||
implementation(libs.logging)
|
||||
implementation(libs.slf4j.android.mvysny)
|
||||
testImplementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
169
bili-api/src/main/kotlin/dev/aaa1115910/biliapi/BiliApi.kt
Normal file
169
bili-api/src/main/kotlin/dev/aaa1115910/biliapi/BiliApi.kt
Normal file
@ -0,0 +1,169 @@
|
||||
package dev.aaa1115910.biliapi
|
||||
|
||||
import dev.aaa1115910.biliapi.entity.danmaku.DanmakuData
|
||||
import dev.aaa1115910.biliapi.entity.danmaku.DanmakuResponse
|
||||
import dev.aaa1115910.biliapi.entity.live.HistoryDanmakuResponse
|
||||
import dev.aaa1115910.biliapi.entity.live.LiveDanmuInfoResponse
|
||||
import dev.aaa1115910.biliapi.entity.live.LiveRoomPlayInfoResponse
|
||||
import dev.aaa1115910.biliapi.entity.video.PlayUrlResponse
|
||||
import dev.aaa1115910.biliapi.entity.video.PopularVideosResponse
|
||||
import dev.aaa1115910.biliapi.entity.video.VideoInfoResponse
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.BrowserUserAgent
|
||||
import io.ktor.client.plugins.compression.ContentEncoding
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import mu.KotlinLogging
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
object BiliApi {
|
||||
private var endPoint: String = ""
|
||||
private lateinit var client: HttpClient
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
init {
|
||||
createClient()
|
||||
}
|
||||
|
||||
private fun createClient() {
|
||||
client = HttpClient(OkHttp) {
|
||||
BrowserUserAgent()
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
})
|
||||
}
|
||||
install(ContentEncoding) {
|
||||
deflate(1.0F)
|
||||
gzip(0.9F)
|
||||
}
|
||||
defaultRequest {
|
||||
host = "api.bilibili.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门视频列表
|
||||
*/
|
||||
suspend fun getPopularVideoData(
|
||||
pageNumber: Int = 1,
|
||||
pageSize: Int = 20
|
||||
): PopularVideosResponse = client.get("/x/web-interface/popular") {
|
||||
parameter("pn", pageNumber)
|
||||
parameter("ps", pageSize)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取视频详细信息
|
||||
*/
|
||||
suspend fun getVideoInfo(
|
||||
av: Int? = null,
|
||||
bv: String? = null
|
||||
): VideoInfoResponse = client.get("/x/web-interface/view") {
|
||||
parameter("aid", av)
|
||||
parameter("bvid", bv)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取视频流
|
||||
*/
|
||||
suspend fun getVideoPlayUrl(
|
||||
av: Int? = null,
|
||||
bv: Int? = null,
|
||||
cid: Int,
|
||||
qn: Int? = null,
|
||||
fnval: Int? = null,
|
||||
fnver: Int? = null,
|
||||
fourk: Int? = 0,
|
||||
session: String? = null,
|
||||
otype: String = "json",
|
||||
type: String = "",
|
||||
platform: String = "oc"
|
||||
): PlayUrlResponse = client.get("/x/player/playurl") {
|
||||
parameter("avid", av)
|
||||
parameter("bvid", bv)
|
||||
parameter("cid", cid)
|
||||
parameter("qn", qn)
|
||||
parameter("fnval", fnval)
|
||||
parameter("fnver", fnver)
|
||||
parameter("fourk", fourk)
|
||||
parameter("session", session)
|
||||
parameter("otype", otype)
|
||||
parameter("type", type)
|
||||
parameter("platform", platform)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 通过[cid]获取视频弹幕
|
||||
*/
|
||||
suspend fun getDanmakuXml(
|
||||
cid: Int
|
||||
): DanmakuResponse {
|
||||
val xmlChannel = client.get("/x/v1/dm/list.so") {
|
||||
parameter("oid", cid)
|
||||
}.bodyAsChannel()
|
||||
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
val doc = withContext(Dispatchers.IO) {
|
||||
dBuilder.parse(xmlChannel.toInputStream())
|
||||
}
|
||||
doc.documentElement.normalize()
|
||||
|
||||
val chatServer = doc.getElementsByTagName("chatserver").item(0).textContent
|
||||
val chatId = doc.getElementsByTagName("chatid").item(0).textContent.toInt()
|
||||
val maxLimit = doc.getElementsByTagName("maxlimit").item(0).textContent.toInt()
|
||||
val state = doc.getElementsByTagName("state").item(0).textContent.toInt()
|
||||
val realName = doc.getElementsByTagName("real_name").item(0).textContent.toInt()
|
||||
val source = doc.getElementsByTagName("source").item(0).textContent
|
||||
|
||||
val data = mutableListOf<DanmakuData>()
|
||||
val danmakuNodes = doc.getElementsByTagName("d")
|
||||
|
||||
for (i in 0 until danmakuNodes.length) {
|
||||
val danmakuNode = danmakuNodes.item(i)
|
||||
val p = danmakuNode.attributes.item(0).textContent
|
||||
val text = danmakuNode.textContent
|
||||
data.add(DanmakuData.fromString(p, text))
|
||||
}
|
||||
|
||||
val response = DanmakuResponse(chatServer, chatId, maxLimit, state, realName, source, data)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直播间[roomId]的弹幕连接地址等信息,例如 token
|
||||
*/
|
||||
suspend fun getLiveDanmuInfo(roomId: Int): LiveDanmuInfoResponse =
|
||||
client.get("/xlive/web-room/v1/index/getDanmuInfo") {
|
||||
parameter("id", roomId)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取直播间[roomId]的信息
|
||||
*/
|
||||
suspend fun getLiveRoomPlayInfo(roomId: Int): LiveRoomPlayInfoResponse =
|
||||
client.get("/xlive/web-room/v1/index/getRoomPlayInfo") {
|
||||
parameter("room_id", roomId)
|
||||
}.body()
|
||||
|
||||
/**
|
||||
* 获取直播间[roomId]的历史弹幕
|
||||
*/
|
||||
suspend fun getLiveDanmuHistory(roomId: Int): HistoryDanmakuResponse =
|
||||
client.get("https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory") {
|
||||
parameter("roomid", roomId)
|
||||
}.body()
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
package dev.aaa1115910.biliapi
|
||||
|
||||
import dev.aaa1115910.biliapi.entity.live.DanmakuEvent
|
||||
import dev.aaa1115910.biliapi.entity.live.FrameHeader
|
||||
import dev.aaa1115910.biliapi.entity.live.LiveEvent
|
||||
import dev.aaa1115910.biliapi.entity.live.readFrameHeader
|
||||
import dev.aaa1115910.biliapi.util.zlibDecompress
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.BrowserUserAgent
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.wss
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import io.ktor.utils.io.core.buildPacket
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import io.ktor.utils.io.core.toByteArray
|
||||
import io.ktor.utils.io.core.writeInt
|
||||
import io.ktor.utils.io.core.writeShort
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.put
|
||||
import mu.KotlinLogging
|
||||
|
||||
object LiveDataWebSocket {
|
||||
private lateinit var client: HttpClient
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
private val heartbeat = byteArrayOf(
|
||||
0, 0, 0, 0x1f,
|
||||
0, 0x10, 0, 0x1,
|
||||
0, 0, 0, 0x2,
|
||||
0, 0, 0, 0x1,
|
||||
0x5b, 0x6f, 0x62, 0x6a,
|
||||
0x65, 0x63, 0x74, 0x20,
|
||||
0x4f, 0x62, 0x6a, 0x65,
|
||||
0x63, 0x74, 0x5d
|
||||
)
|
||||
|
||||
init {
|
||||
createClient()
|
||||
}
|
||||
|
||||
private fun createClient() {
|
||||
client = HttpClient(OkHttp) {
|
||||
BrowserUserAgent()
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun connectLiveEvent(
|
||||
roomId: Int,
|
||||
onEvent: (event: LiveEvent) -> Unit
|
||||
) {
|
||||
val danmuInfo = BiliApi.getLiveDanmuInfo(roomId).data ?: throw CancellationException()
|
||||
val realRoomId =
|
||||
BiliApi.getLiveRoomPlayInfo(roomId).data?.roomId ?: throw CancellationException()
|
||||
val hosts = danmuInfo.hostList.last()
|
||||
|
||||
val data = buildJsonObject {
|
||||
put("uid", 0)
|
||||
put("roomid", realRoomId)
|
||||
put("protover", 2)
|
||||
put("platform", "web")
|
||||
put("type", 2)
|
||||
put("key", danmuInfo.token)
|
||||
}.toString().toByteArray()
|
||||
val b = buildPacket {
|
||||
val size = 16 + data.size
|
||||
writeInt(size) // 封包总大小
|
||||
writeShort(0x10) // 头部大小
|
||||
writeShort(1) // 协议版本
|
||||
writeInt(7) // 类型
|
||||
writeInt(1)
|
||||
writePacket(ByteReadPacket(data))
|
||||
}
|
||||
|
||||
val job = client.launch {
|
||||
client.wss(
|
||||
host = hosts.host,
|
||||
port = hosts.wssPort,
|
||||
path = "/sub"
|
||||
) {
|
||||
val byte = b.readBytes()
|
||||
outgoing.send(Frame.Binary(true, byte))
|
||||
launch {
|
||||
delay(5000)
|
||||
while (isActive) {
|
||||
//println("send heart")
|
||||
outgoing.send(Frame.Binary(true, heartbeat))
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
while (isActive) {
|
||||
val frame = incoming.receive()
|
||||
val eventData = frame.data
|
||||
launch {
|
||||
|
||||
handleLiveEventData(eventData).forEach { event ->
|
||||
onEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
job.invokeOnCompletion {
|
||||
it?.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleLiveEventData(data: ByteArray): List<LiveEvent> {
|
||||
val result = mutableListOf<LiveEvent>()
|
||||
withContext(Dispatchers.IO) {
|
||||
if (data.size <= 16) return@withContext
|
||||
val bytePack = ByteReadPacket(data)
|
||||
val head = bytePack.readFrameHeader()
|
||||
val body = bytePack.readBytes((head.totalLength - head.headerLength))
|
||||
result.addAll(handleLiveEventBody(head, body))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun handleLiveEventBody(head: FrameHeader, data: ByteArray): List<LiveEvent> {
|
||||
val result = mutableListOf<LiveEvent>()
|
||||
val bytePack = ByteReadPacket(data)
|
||||
when (head.type) {
|
||||
//心跳包回复(人气值)
|
||||
3 -> {
|
||||
//println("接收心跳,房间人气值: ${bytePack.readInt()}")
|
||||
}
|
||||
|
||||
//普通包(命令)
|
||||
5 -> {
|
||||
when (head.version.toInt()) {
|
||||
//0 普通包正文不使用压缩
|
||||
//1 心跳及认证包正文不使用压缩
|
||||
0, 1 -> {
|
||||
val strData = io.ktor.utils.io.core.String(bytePack.readBytes())
|
||||
handleLiveCMDEventString(strData)?.let { result += it }
|
||||
}
|
||||
|
||||
//普通包正文使用zlib压缩
|
||||
2 -> {
|
||||
val decompress = bytePack.readBytes().zlibDecompress()
|
||||
result += handleLiveEventBodyDecompress(decompress)
|
||||
}
|
||||
|
||||
//普通包正文使用brotli压缩,解压为一个带头部的协议0普通包
|
||||
3 -> {
|
||||
logger.warn { "todo package version: ${head.version}" }
|
||||
bytePack.readBytes()
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.warn { "Unknown package version: ${head.version}" }
|
||||
bytePack.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//认证包回复
|
||||
8 -> {
|
||||
bytePack.readBytes(10)
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.warn { "Unknown package type: ${head.type}" }
|
||||
bytePack.readBytes()
|
||||
}
|
||||
}
|
||||
return if (bytePack.remaining > 16) result + handleLiveEventBody(
|
||||
bytePack.readFrameHeader(),
|
||||
bytePack.readBytes()
|
||||
)
|
||||
else result
|
||||
}
|
||||
|
||||
private fun handleLiveEventBodyDecompress(data: ByteArray): List<LiveEvent> {
|
||||
val result = mutableListOf<LiveEvent>()
|
||||
val bytePack = ByteReadPacket(data)
|
||||
val header = bytePack.readFrameHeader()
|
||||
val body = bytePack.readBytes(header.dataLength)
|
||||
result += handleLiveCMDEvent(header, body)
|
||||
return if (bytePack.remaining > 0) result + handleLiveEventBodyDecompress(bytePack.readBytes()) else result
|
||||
}
|
||||
|
||||
private fun handleLiveCMDEvent(head: FrameHeader, data: ByteArray): List<LiveEvent> {
|
||||
val result = mutableListOf<LiveEvent>()
|
||||
val strData: String
|
||||
when (head.version.toInt()) {
|
||||
0 -> {
|
||||
strData = io.ktor.utils.io.core.String(data)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val decompress = data.zlibDecompress()
|
||||
val bytePack = ByteReadPacket(decompress)
|
||||
val packageHeader = bytePack.readFrameHeader()
|
||||
val body =
|
||||
bytePack.readBytes((packageHeader.totalLength - packageHeader.headerLength).toInt())
|
||||
if (bytePack.remaining > 16) {
|
||||
result += handleLiveEventBody(bytePack.readFrameHeader(), bytePack.readBytes())
|
||||
}
|
||||
strData = io.ktor.utils.io.core.String(body)
|
||||
}
|
||||
|
||||
else -> return result
|
||||
}
|
||||
handleLiveCMDEventString(strData)?.let { result += it }
|
||||
return result
|
||||
}
|
||||
|
||||
private fun handleLiveCMDEventString(strData: String): LiveEvent? {
|
||||
val dataJson = Json.parseToJsonElement(strData).jsonObject
|
||||
val cmd = dataJson["cmd"]!!.jsonPrimitive.content
|
||||
|
||||
when (cmd) {
|
||||
"COMBO_SEND" -> {}
|
||||
"DANMU_MSG" -> {
|
||||
runCatching {
|
||||
val danmakuContent = dataJson["info"]!!.jsonArray[1].jsonPrimitive.content
|
||||
val senderMid = dataJson["info"]!!.jsonArray[2].jsonArray[0].jsonPrimitive.long
|
||||
val senderUsername =
|
||||
dataJson["info"]!!.jsonArray[2].jsonArray[1].jsonPrimitive.content
|
||||
var medalLevel: Int? = null
|
||||
var medalName: String? = null
|
||||
runCatching {
|
||||
medalLevel =
|
||||
dataJson["info"]?.jsonArray?.get(3)?.jsonArray?.get(0)?.jsonPrimitive?.int
|
||||
medalName =
|
||||
dataJson["info"]?.jsonArray?.get(3)?.jsonArray?.get(1)?.jsonPrimitive?.content
|
||||
}
|
||||
|
||||
return DanmakuEvent(
|
||||
content = danmakuContent,
|
||||
mid = senderMid,
|
||||
username = senderUsername,
|
||||
medalName = medalName,
|
||||
medalLevel = medalLevel
|
||||
)
|
||||
}.onFailure {
|
||||
logger.warn { "Parse danmaku content failed: ${it.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
"ENTRY_EFFECT" -> {}
|
||||
//有人上舰
|
||||
"GUARD_BUY" -> {}
|
||||
//千舰通知
|
||||
"GUARD_HONOR_THOUSAND" -> {
|
||||
println(dataJson)
|
||||
}
|
||||
|
||||
"HOT_RANK_CHANGED" -> {}
|
||||
"HOT_RANK_CHANGED_V2" -> {}
|
||||
"HOT_RANK_SETTLEMENT" -> {}
|
||||
"HOT_RANK_SETTLEMENT_V2" -> {}
|
||||
"HOT_ROOM_NOTIFY" -> {}
|
||||
"INTERACT_WORD" -> {}
|
||||
"LIVE" -> {
|
||||
println(dataJson)
|
||||
}
|
||||
|
||||
"LIVE_INTERACTIVE_GAME" -> {}
|
||||
"LIKE_INFO_V3_CLICK" -> {}
|
||||
"LIKE_INFO_V3_UPDATE" -> {}
|
||||
"NOTICE_MSG" -> {}
|
||||
"ONLINE_RANK_COUNT" -> {}
|
||||
"ONLINE_RANK_V2" -> {}
|
||||
"ONLINE_RANK_TOP3" -> {}
|
||||
"PREPARING" -> {}
|
||||
"ROOM_REAL_TIME_MESSAGE_UPDATE" -> {}
|
||||
"SEND_GIFT" -> {}
|
||||
"STOP_LIVE_ROOM_LIST" -> {}
|
||||
//醒目留言入口提醒(氪金提醒)
|
||||
"SUPER_CHAT_ENTRANCE" -> {}
|
||||
//醒目留言
|
||||
"SUPER_CHAT_MESSAGE" -> {}
|
||||
//醒目留言
|
||||
"SUPER_CHAT_MESSAGE_JPN" -> {}
|
||||
"SYS_MSG" -> {
|
||||
println(dataJson)
|
||||
}
|
||||
|
||||
"USER_TOAST_MSG" -> {}
|
||||
"WATCHED_CHANGE" -> {}
|
||||
"WIDGET_BANNER" -> {}
|
||||
else -> {
|
||||
logger.warn { "Unknown live event: $cmd" }
|
||||
logger.warn { dataJson }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package dev.aaa1115910.biliapi.entity.danmaku
|
||||
|
||||
|
||||
data class DanmakuResponse(
|
||||
val chatserver: String,
|
||||
val chatId: Int,
|
||||
val maxLimit: Int,
|
||||
val state: Int,
|
||||
val realName: Int,
|
||||
val source: String,
|
||||
val data: List<DanmakuData> = emptyList()
|
||||
)
|
||||
|
||||
data class DanmakuData(
|
||||
val time: Float,
|
||||
val type: Int,
|
||||
val size: Int,
|
||||
val color: Int,
|
||||
val timestamp: Int,
|
||||
val pool: Int,
|
||||
val midHash: String,
|
||||
val dmid: Long,
|
||||
val level: Int,
|
||||
val text: String
|
||||
) {
|
||||
companion object {
|
||||
fun fromString(p: String, text: String): DanmakuData {
|
||||
val data = p.split(",")
|
||||
return DanmakuData(
|
||||
time = data[0].toFloat(),
|
||||
type = data[1].toInt(),
|
||||
size = data[2].toInt(),
|
||||
color = data[3].toInt(),
|
||||
timestamp = data[4].toInt(),
|
||||
pool = data[5].toInt(),
|
||||
midHash = data[6],
|
||||
dmid = data[7].toLong(),
|
||||
level = data[8].toInt(),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
package dev.aaa1115910.biliapi.entity.live
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class HistoryDanmakuResponse(
|
||||
val code: Int,
|
||||
val msg: String,
|
||||
val message: String,
|
||||
val data: HistoryDanmaku
|
||||
) {
|
||||
@Serializable
|
||||
data class HistoryDanmaku(
|
||||
//val admin:List<Any>,
|
||||
val room: List<HistoryDanmakuItem>
|
||||
) {
|
||||
@Serializable
|
||||
data class HistoryDanmakuItem(
|
||||
val text: String,
|
||||
@SerialName("dm_type")
|
||||
val dmType: Int,
|
||||
val uid: Long,
|
||||
val nickname: String,
|
||||
@SerialName("uname_color")
|
||||
val unameColor: String,
|
||||
val timeline: String,
|
||||
@SerialName("isadmin")
|
||||
val isAdmin: Int,
|
||||
val vip: Int,
|
||||
val svip: Int,
|
||||
@SerialName("medal")
|
||||
private val _medal: List<JsonElement>,
|
||||
@Transient
|
||||
var medal: Medal? = null,
|
||||
val title: List<String>,
|
||||
@SerialName("user_level")
|
||||
val userLevel: List<JsonElement>,
|
||||
val rank: Int,
|
||||
@SerialName("teamid")
|
||||
val teamId: Int,
|
||||
val rnd: Int,
|
||||
@SerialName("user_title")
|
||||
val userTitle: String,
|
||||
@SerialName("guard_level")
|
||||
val guardLevel: Int,
|
||||
val bubble: Int,
|
||||
@SerialName("bubble_color")
|
||||
val bubbleColor: String,
|
||||
val lpl: Int,
|
||||
@SerialName("yeah_space_url")
|
||||
val yeahSpaceUrl: String,
|
||||
@SerialName("jump_to_url")
|
||||
val jumpToUrl: String,
|
||||
@SerialName("check_info")
|
||||
val checkInfo: CheckInfo,
|
||||
@SerialName("voice_dm_info")
|
||||
val voiceDmInfo: VoiceDmInfo,
|
||||
val emoticon: Emoticon
|
||||
) {
|
||||
init {
|
||||
medal = runCatching {
|
||||
Medal(
|
||||
level = _medal[0].jsonPrimitive.int,
|
||||
name = _medal[1].jsonPrimitive.content,
|
||||
up = _medal[2].jsonPrimitive.content,
|
||||
roomId = _medal[3].jsonPrimitive.int
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 粉丝勋章
|
||||
*
|
||||
* 返回样例
|
||||
* ```
|
||||
* [
|
||||
* 16, //level
|
||||
* "迷你鲨", //name
|
||||
* "hufang360",//up
|
||||
* 22739471, //up room id
|
||||
* 12478086,
|
||||
* "",
|
||||
* 0,
|
||||
* 12478086, //medal_color_border(可能是)
|
||||
* 12478086, //medal_color_end(可能是)
|
||||
* 12478086, //medal_color_start(可能是)
|
||||
* 0,
|
||||
* 1,
|
||||
* 4328524
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param level 勋章等级
|
||||
* @param name 勋章名称
|
||||
* @param up 主播昵称
|
||||
* @param roomId 主播房间号
|
||||
*/
|
||||
data class Medal(
|
||||
val level: Int,
|
||||
val name: String,
|
||||
val up: String,
|
||||
val roomId: Int
|
||||
)
|
||||
|
||||
/*
|
||||
"medal": [
|
||||
16, //level
|
||||
"迷你鲨", //name
|
||||
"hufang360",//up
|
||||
22739471, //up room id
|
||||
12478086,
|
||||
"",
|
||||
0,
|
||||
12478086, //medal_color_border
|
||||
12478086, //medal_color_end
|
||||
12478086, //medal_color_start
|
||||
0,
|
||||
1,
|
||||
4328524
|
||||
],
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class CheckInfo(
|
||||
val ts: Int,
|
||||
val ct: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VoiceDmInfo(
|
||||
@SerialName("voice_url")
|
||||
val voiceUrl: String,
|
||||
@SerialName("file_format")
|
||||
val fileFormat: String,
|
||||
val text: String,
|
||||
@SerialName("file_duration")
|
||||
val fileDuration: Int,
|
||||
@SerialName("file_id")
|
||||
val fileId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Emoticon(
|
||||
val id: Int,
|
||||
@SerialName("emoticon_unique")
|
||||
val emoticonUnique: String,
|
||||
val text: String,
|
||||
val perm: Int,
|
||||
val url: String,
|
||||
@SerialName("in_player_area")
|
||||
val inPlayerArea: Int,
|
||||
@SerialName("bulge_display")
|
||||
val bulgeDisplay: Int,
|
||||
@SerialName("is_dynamic")
|
||||
val isDynamic: Int,
|
||||
val height: Int,
|
||||
val width: Int
|
||||
)
|
@ -0,0 +1,38 @@
|
||||
package dev.aaa1115910.biliapi.entity.live
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LiveDanmuInfoResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: DanmuInfoData? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DanmuInfoData(
|
||||
val group: String,
|
||||
@SerialName("business_id")
|
||||
val businessId: Int,
|
||||
@SerialName("refresh_row_factor")
|
||||
val refreshRowFactor: Float,
|
||||
@SerialName("refresh_rate")
|
||||
val refreshRate: Int,
|
||||
@SerialName("max_delay")
|
||||
val maxDelay: Int,
|
||||
val token: String,
|
||||
@SerialName("host_list")
|
||||
val hostList: List<HostListItem> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HostListItem(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
@SerialName("wss_port")
|
||||
val wssPort: Int,
|
||||
@SerialName("ws_port")
|
||||
val wsPort: Int
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
package dev.aaa1115910.biliapi.entity.live
|
||||
|
||||
interface LiveEvent
|
||||
|
||||
data class DanmakuEvent(
|
||||
val content: String,
|
||||
val mid: Long,
|
||||
val username: String,
|
||||
val medalName: String? = null,
|
||||
val medalLevel: Int? = null
|
||||
) : LiveEvent
|
@ -0,0 +1,106 @@
|
||||
package dev.aaa1115910.biliapi.entity.live
|
||||
|
||||
import io.ktor.utils.io.core.ByteReadPacket
|
||||
import io.ktor.utils.io.core.buildPacket
|
||||
import io.ktor.utils.io.core.readInt
|
||||
import io.ktor.utils.io.core.readShort
|
||||
import io.ktor.utils.io.core.toByteArray
|
||||
import io.ktor.utils.io.core.writeInt
|
||||
import io.ktor.utils.io.core.writeShort
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.DataInputStream
|
||||
|
||||
internal fun ByteReadPacket.readFrameHeader(): FrameHeader = FrameHeader(
|
||||
readInt(), readShort(), readShort(), readInt(), readInt()
|
||||
)
|
||||
|
||||
/**
|
||||
* 数据包头部
|
||||
*
|
||||
* @param totalLength 封包总大小(头部大小+正文大小)
|
||||
* @param headerLength 头部大小(一般为0x0010,16字节)
|
||||
* @param version 协议版本: 0普通包正文不使用压缩 1心跳及认证包正文不使用压缩2普通包正文使用zlib压缩 3普通包正文使用brotli压缩,解压为一个带头部的协议0普通包
|
||||
* @param type 操作码(封包类型)
|
||||
* @param sequence 保留字段不使用
|
||||
*/
|
||||
data class FrameHeader(
|
||||
val totalLength: Int,
|
||||
val headerLength: Short,
|
||||
val version: Short,
|
||||
val type: Int,
|
||||
val sequence: Int
|
||||
) {
|
||||
val dataLength get() = totalLength - headerLength
|
||||
fun toBinary(): ByteReadPacket {
|
||||
return buildPacket {
|
||||
writeInt(this@FrameHeader.totalLength)
|
||||
writeShort(headerLength)
|
||||
writeShort(version)
|
||||
writeInt(this@FrameHeader.type)
|
||||
writeInt(sequence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class FrameType(val code: Int) {
|
||||
HeartRequest(2), HeartResponse(3), Normal(5), AuthRequest(7), AuthResponse(8)
|
||||
}
|
||||
|
||||
interface RequestFrame {
|
||||
fun toBinary(): ByteReadPacket
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AuthRequest(
|
||||
val uid: Int = 0,
|
||||
@SerialName("roomid")
|
||||
val roomId: Int,
|
||||
@SerialName("protover")
|
||||
val protoVer: Int = 3,
|
||||
val platform: String = "web",
|
||||
val type: Int = 2,
|
||||
val key: String = ""
|
||||
) : RequestFrame {
|
||||
override fun toBinary(): ByteReadPacket {
|
||||
val data = Json.encodeToString(this).toByteArray()
|
||||
val header = FrameHeader(
|
||||
totalLength = data.size + 16,
|
||||
headerLength = 16,
|
||||
version = 1,
|
||||
type = FrameType.AuthRequest.code,
|
||||
sequence = 1
|
||||
)
|
||||
return buildPacket {
|
||||
this.writePacket(header.toBinary())
|
||||
writePacket(ByteReadPacket(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
val code: Int = -1
|
||||
) {
|
||||
companion object {
|
||||
fun parse(data: ByteArray): AuthResponse {
|
||||
val bis = ByteArrayInputStream(data)
|
||||
val dis = DataInputStream(bis)
|
||||
val totalLength = dis.readInt()
|
||||
val headerLength = dis.readUnsignedShort()
|
||||
val version = dis.readUnsignedShort()
|
||||
val type = dis.readInt()
|
||||
val sequence = dis.readInt()
|
||||
|
||||
//TODO do some verity
|
||||
|
||||
val jsonString = String(dis.readBytes())
|
||||
return Json.decodeFromString(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package dev.aaa1115910.biliapi.entity.live
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LiveRoomPlayInfoResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: RoomPlayInfoData? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RoomPlayInfoData(
|
||||
@SerialName("room_id")
|
||||
val roomId: Int,
|
||||
@SerialName("short_id")
|
||||
val shortId: Int,
|
||||
val uid: Long,
|
||||
@SerialName("need_p2p")
|
||||
val needP2P: Int,
|
||||
@SerialName("is_hidden")
|
||||
val isHidden: Boolean,
|
||||
@SerialName("is_locked")
|
||||
val isLocked: Boolean,
|
||||
@SerialName("is_portrait")
|
||||
val isPortrait: Boolean,
|
||||
@SerialName("live_status")
|
||||
val liveStatus: Int,
|
||||
@SerialName("hidden_till")
|
||||
val hiddenTill: Int,
|
||||
@SerialName("lock_till")
|
||||
val lockTill: Int,
|
||||
val encrypted: Boolean,
|
||||
@SerialName("pwd_verified")
|
||||
val pwdVerified: Boolean,
|
||||
@SerialName("live_time")
|
||||
val liveTime: Int,
|
||||
@SerialName("room_shield")
|
||||
val roomShield: Int,
|
||||
@SerialName("is_sp")
|
||||
val isSp: Int,
|
||||
@SerialName("special_type")
|
||||
val specialType: Int,
|
||||
val playUrl: String? = null,
|
||||
@SerialName("all_special_types")
|
||||
val allSpecialTypes: List<Int> = emptyList()
|
||||
)
|
||||
/*
|
||||
"data": {
|
||||
"play_url": null,
|
||||
"all_special_types": [
|
||||
50
|
||||
]
|
||||
*/
|
@ -0,0 +1,70 @@
|
||||
package dev.aaa1115910.biliapi.entity.subtitle
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
/**
|
||||
* 字幕信息
|
||||
* @param allowSubmit 是否允许提交字幕
|
||||
* @param list 字幕列表
|
||||
*/
|
||||
@Serializable
|
||||
data class Subtitle(
|
||||
val allowSubmit: Boolean = false,
|
||||
val list: List<SubtitleListItem> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* 字幕列表项
|
||||
*
|
||||
* @param id 字幕id
|
||||
* @param lan 字幕语言
|
||||
* @param lanDoc 字幕语言名称
|
||||
* @param isLock 是否锁定
|
||||
* @param authorMid 字幕上传者mid
|
||||
* @param subtitleUrl json格式字幕文件url
|
||||
* @param author 字幕上传者信息
|
||||
*/
|
||||
@Serializable
|
||||
data class SubtitleListItem(
|
||||
val id: Long,
|
||||
val lan: String,
|
||||
@SerialName("lan_doc")
|
||||
val lanDoc: String,
|
||||
@SerialName("is_lock")
|
||||
val isLock: Boolean,
|
||||
@SerialName("author_mid")
|
||||
val authorMid: Int? = null,
|
||||
@SerialName("subtitle_url")
|
||||
val subtitleUrl: String,
|
||||
val author: SubtitleAuthor
|
||||
)
|
||||
|
||||
/**
|
||||
* 字幕作者
|
||||
*
|
||||
* @param mid 字幕上传者mid
|
||||
* @param name 字幕上传者昵称
|
||||
* @param sex 字幕上传者性别 男 女 保密
|
||||
* @param face 字幕上传者头像url
|
||||
* @param sign 字幕上传者签名
|
||||
* @param rank 10000 作用尚不明确
|
||||
* @param birthday 0 作用尚不明确
|
||||
* @param isFakeAccount 0 作用尚不明确
|
||||
* @param isDeleted 0 作用尚不明确
|
||||
*/
|
||||
@Serializable
|
||||
data class SubtitleAuthor(
|
||||
val mid: Int,
|
||||
val name: String,
|
||||
val sex: String,
|
||||
val face: String,
|
||||
val sign: String,
|
||||
val rank: Int,
|
||||
val birthday: Int,
|
||||
@SerialName("is_fake_account")
|
||||
val isFakeAccount: Int,
|
||||
@SerialName("is_deleted")
|
||||
val isDeleted: Int
|
||||
)
|
@ -0,0 +1,58 @@
|
||||
package dev.aaa1115910.biliapi.entity.user
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
/**
|
||||
* 创作者个人信息
|
||||
*
|
||||
* @param mid 成员mid
|
||||
* @param title 成员名称
|
||||
* @param name 成员昵称
|
||||
* @param face 成员头像url
|
||||
* @param vip 成员大会员状态
|
||||
* @param official 成员认证信息
|
||||
* @param follower 成员粉丝数
|
||||
*/
|
||||
@Serializable
|
||||
data class Staff(
|
||||
val mid: Int,
|
||||
val title: String,
|
||||
val name: String,
|
||||
val face: String,
|
||||
val vip: Vip,
|
||||
val official: Official,
|
||||
val follower: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 会员信息
|
||||
*
|
||||
* @param type 成员会员类型 0:无 1:月会员 2:年会员
|
||||
* @param status 会员状态 0:无 1:有
|
||||
* @param themeType num 0
|
||||
*/
|
||||
@Serializable
|
||||
data class Vip(
|
||||
val type: Int,
|
||||
val status: Int,
|
||||
@SerialName("theme_type")
|
||||
val themeType: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
*
|
||||
* @param role 成员认证级别 0:无 1 2 7:个人认证 3 4 5 6:机构认证
|
||||
* @param title 成员认证名 无为空
|
||||
* @param desc 成员认证备注 无为空
|
||||
* @param type 成员认证类型 -1:无 0:有
|
||||
*/
|
||||
@Serializable
|
||||
data class Official(
|
||||
val role: Int,
|
||||
val title: String,
|
||||
val desc: String,
|
||||
val type: Int
|
||||
)
|
@ -0,0 +1,15 @@
|
||||
package dev.aaa1115910.biliapi.entity.user
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 用户头像挂件
|
||||
*
|
||||
* @param urlImageAniCut
|
||||
*/
|
||||
@Serializable
|
||||
data class UserGarb(
|
||||
@SerialName("url_image_ani_cut")
|
||||
val urlImageAniCut: String
|
||||
)
|
@ -0,0 +1,160 @@
|
||||
package dev.aaa1115910.biliapi.entity.video
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
data class PlayUrlResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: PlayUrlData? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* @param from local 作用尚不明确
|
||||
* @param result suee 作用尚不明确
|
||||
* @param message 空 作用尚不明确
|
||||
* @param quality 当前的视频分辨率代码 值含义见上表
|
||||
* @param format 视频格式
|
||||
* @param timeLength 视频长度(毫秒值) 单位为毫秒 不同分辨率/格式可能有略微差异
|
||||
* @param acceptFormat 视频支持的全部格式 每项用,分隔
|
||||
* @param acceptDescription 视频支持的分辨率列表
|
||||
* @param acceptQuality 视频支持的分辨率代码列表 值含义见上表
|
||||
* @param videoCodecId 默认选择视频流的编码id 见视频编码代码
|
||||
* @param seekParam 固定值:start 作用尚不明确
|
||||
* @param seekType offset(dash、flv) second(mp4) 作用尚不明确
|
||||
* @param durl 视频分段 注:仅flv/mp4存在此项
|
||||
* @param dash dash音视频流信息 注:仅dash存在此项
|
||||
* @param supportFormats 支持格式的详细信息
|
||||
* @param high_format null
|
||||
* @param lastPlayTime 上次播放进度 毫秒值
|
||||
* @param lastPlayCid 上次播放分p的cid
|
||||
*/
|
||||
@Serializable
|
||||
data class PlayUrlData(
|
||||
val from: String,
|
||||
val result: String,
|
||||
val message: String,
|
||||
val quality: Int,
|
||||
val format: String,
|
||||
@SerialName("timelength")
|
||||
val timeLength: Int,
|
||||
@SerialName("accept_format")
|
||||
val acceptFormat: String,
|
||||
@SerialName("accept_description")
|
||||
val acceptDescription: List<String> = emptyList(),
|
||||
@SerialName("accept_quality")
|
||||
val acceptQuality: List<Int> = emptyList(),
|
||||
@SerialName("video_codecid")
|
||||
val videoCodecId: Int,
|
||||
@SerialName("seek_param")
|
||||
val seekParam: String,
|
||||
@SerialName("seek_type")
|
||||
val seekType: String,
|
||||
val durl: List<Durl> = emptyList(),
|
||||
val dash: Dash? = null,
|
||||
@SerialName("support_formats")
|
||||
val supportFormats: List<SupportFormat> = emptyList(),
|
||||
@SerialName("last_play_time")
|
||||
val lastPlayTime: Int,
|
||||
@SerialName("last_play_cid")
|
||||
val lastPlayCid: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频播放地址
|
||||
*
|
||||
* @param order 视频分段序号 某些视频会分为多个片段(从1顺序增长)
|
||||
* @param length 视频长度 单位为毫秒
|
||||
* @param size 视频大小 单位为Byte
|
||||
* @param ahead 空 作用尚不明确
|
||||
* @param vhead 空 作用尚不明确
|
||||
* @param url 视频流url 注:url内容存在转义符 有效时间为120min
|
||||
* @param backupUrl 备用视频流
|
||||
*/
|
||||
@Serializable
|
||||
data class Durl(
|
||||
val order: Int,
|
||||
val length: Int,
|
||||
val size: Int,
|
||||
val ahead: String,
|
||||
val vhead: String,
|
||||
val url: String,
|
||||
@SerialName("backup_url")
|
||||
val backupUrl: List<String> = emptyList()
|
||||
)
|
||||
|
||||
//TODO
|
||||
@Serializable
|
||||
data class Dash(
|
||||
val duration: Int,
|
||||
val minBufferTime: Float,
|
||||
val video: List<DashData> = emptyList(),
|
||||
val audio: List<DashData> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DashData(
|
||||
val id: Int,
|
||||
val baseUrl: String,
|
||||
val backupUrl: List<String> = emptyList(),
|
||||
val mimeType: String,
|
||||
val codecs: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val frameRate: String,
|
||||
val sar: String,
|
||||
val startWithSap: Int,
|
||||
@SerialName("segment_base")
|
||||
val segmentBase: SegmentBase,
|
||||
@SerialName("codecid")
|
||||
val codecId: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SegmentBase(
|
||||
val initialization: String,
|
||||
@SerialName("index_range")
|
||||
val indexRange: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 支持的视频格式
|
||||
*
|
||||
* @param quality 视频清晰度代码
|
||||
* @param format 视频格式
|
||||
* @param newDescription 格式描述
|
||||
* @param displayDesc 格式描述
|
||||
* @param superScript (?)
|
||||
* @param codecs 可用编码格式列表
|
||||
*/
|
||||
@Serializable
|
||||
data class SupportFormat(
|
||||
val quality: Int,
|
||||
val format: String,
|
||||
@SerialName("new_description")
|
||||
val newDescription: String,
|
||||
@SerialName("display_desc")
|
||||
val displayDesc: String,
|
||||
@SerialName("superscript")
|
||||
val superScript: String,
|
||||
val codecs: List<String>? = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class VideoQuality(val qn: Int, val displayName: String) {
|
||||
Q260P(6, "240P 极速"),
|
||||
Q360P(16, "360P 流畅"),
|
||||
Q480P(32, "480P 清晰"),
|
||||
Q720P(64, ""),
|
||||
Q720P60(74, ""),
|
||||
Q1080P(80, ""),
|
||||
Q1080PPlus(112, ""),
|
||||
Q1080P60(116, ""),
|
||||
Q4K(120, ""),
|
||||
HDR(125, ""),
|
||||
Dolby(126, ""),
|
||||
Q8K(127, "")
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package dev.aaa1115910.biliapi.entity.video
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularVideosResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: PopularVideoData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PopularVideoData(
|
||||
val list: List<VideoInfo>,
|
||||
@SerialName("no_more")
|
||||
val noMore: Boolean
|
||||
)
|
@ -0,0 +1,299 @@
|
||||
package dev.aaa1115910.biliapi.entity.video
|
||||
|
||||
import dev.aaa1115910.biliapi.entity.subtitle.Subtitle
|
||||
import dev.aaa1115910.biliapi.entity.user.Staff
|
||||
import dev.aaa1115910.biliapi.entity.user.UserGarb
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 视频详细信息
|
||||
*
|
||||
* @param bvid 稿件bvid
|
||||
* @param aid 稿件avid
|
||||
* @param videos 稿件分P总数 默认为1
|
||||
* @param tid 分区tid
|
||||
* @param tname 子分区名称
|
||||
* @param copyright 视频类型 1:原创 2:转载
|
||||
* @param pic 稿件封面图片url
|
||||
* @param title 稿件标题
|
||||
* @param pubdate 稿件发布时间 秒级时间戳
|
||||
* @param ctime 用户投稿时间 秒级时间戳
|
||||
* @param desc 视频简介
|
||||
* @param state 视频状态
|
||||
* @param duration 稿件总时长(所有分P) 单位为秒
|
||||
* @param forward 撞车视频跳转avid 仅撞车视频存在此字段
|
||||
* @param missionId 稿件参与的活动id
|
||||
* @param redirectUrl 稿重定向url 仅番剧或影视视频存在此字段,用于番剧&影视的av/bv->ep
|
||||
* @param rights 视频属性标志
|
||||
* @param owner 视频UP主信息
|
||||
* @param stat 视频状态数
|
||||
* @param dynamic 视频同步发布的的动态的文字内容
|
||||
* @param cid 视频1P cid
|
||||
* @param dimension 视频1P分辨率
|
||||
* @param premiere 首播状态
|
||||
* @param teenageMode
|
||||
* @param isChargeableSeason
|
||||
* @param isStory
|
||||
* @param noCache
|
||||
* @param pages 视频分P列表
|
||||
* @param subtitle 视频CC字幕信息
|
||||
* @param staff 合作成员列表 非合作视频无此项
|
||||
* @param isSeasonDisplay
|
||||
* @param userGarb 用户装扮信息
|
||||
* @param honorReply
|
||||
* @param likeIcon
|
||||
* @param shortLink
|
||||
* @param shortLinkV2
|
||||
* @param firstFrame
|
||||
* @param pubLocation
|
||||
* @param seasonType
|
||||
* @param isOgv
|
||||
* @param ogvInfo
|
||||
*
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoInfo(
|
||||
val bvid: String,
|
||||
val aid: Int,
|
||||
val videos: Int,
|
||||
val tid: Int,
|
||||
val tname: String,
|
||||
val copyright: Int,
|
||||
val pic: String,
|
||||
val title: String,
|
||||
val pubdate: Int,
|
||||
val ctime: Int,
|
||||
val desc: String,
|
||||
val state: Int,
|
||||
val duration: Int,
|
||||
val forward: Int? = null,
|
||||
@SerialName("mission_id")
|
||||
val missionId: Int? = null,
|
||||
@SerialName("redirect_url")
|
||||
val redirectUrl: String? = null,
|
||||
val rights: VideoRights,
|
||||
val owner: VideoOwner,
|
||||
val stat: VideoStat,
|
||||
val dynamic: String,
|
||||
val cid: Int,
|
||||
val dimension: Dimension,
|
||||
val premiere: Premiere? = null,
|
||||
@SerialName("teenage_mode")
|
||||
val teenageMode: Int = 0,
|
||||
@SerialName("is_chargeable_season")
|
||||
val isChargeableSeason: Boolean = false,
|
||||
@SerialName("is_story")
|
||||
val isStory: Boolean = false,
|
||||
@SerialName("no_cache")
|
||||
val noCache: Boolean = false,
|
||||
val pages: List<VideoPage> = emptyList(),
|
||||
val subtitle: Subtitle? = null,
|
||||
val staff: List<Staff> = emptyList(),
|
||||
@SerialName("is_season_display")
|
||||
val isSeasonDisplay: Boolean = false,
|
||||
@SerialName("user_garb")
|
||||
val userGarb: UserGarb? = null,
|
||||
@SerialName("honor_reply")
|
||||
val honorReply: HonorReply? = null,
|
||||
@SerialName("like_icon")
|
||||
val likeIcon: String? = null,
|
||||
@SerialName("short_link")
|
||||
val shortLink: String? = null,
|
||||
@SerialName("short_link_v2")
|
||||
val shortLinkV2: String? = null,
|
||||
@SerialName("first_frame")
|
||||
val firstFrame: String? = null,
|
||||
@SerialName("pub_location")
|
||||
val pubLocation: String? = null,
|
||||
@SerialName("season_type")
|
||||
val seasonType: Int? = null,
|
||||
@SerialName("is_ogv")
|
||||
val isOgv: Boolean = false,
|
||||
@SerialName("ogv_info")
|
||||
val ogvInfo: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频属性标志
|
||||
*
|
||||
* @param bp 是否允许承包
|
||||
* @param elec 是否支持充电
|
||||
* @param download 是否允许下载
|
||||
* @param movie 是否电影
|
||||
* @param pay 是否PGC付费
|
||||
* @param hd5 是否有高码率
|
||||
* @param noReprint 是否显示“禁止转载”标志
|
||||
* @param autoplay 是否自动播放
|
||||
* @param ugcPay 是否UGC付费
|
||||
* @param isCooperation 是否为联合投稿
|
||||
* @param ugcPayPreview
|
||||
* @param noBackground
|
||||
* @param cleanMode
|
||||
* @param isSteinGate 是否为互动视频
|
||||
* @param is360 是否为全景视频
|
||||
* @param noShare
|
||||
* @param arcPay
|
||||
* @param payFreeWatch
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoRights(
|
||||
val bp: Int,
|
||||
val elec: Int,
|
||||
val download: Int,
|
||||
val movie: Int,
|
||||
val pay: Int,
|
||||
val hd5: Int,
|
||||
@SerialName("no_reprint")
|
||||
val noReprint: Int,
|
||||
val autoplay: Int,
|
||||
@SerialName("ugc_pay")
|
||||
val ugcPay: Int,
|
||||
@SerialName("is_cooperation")
|
||||
val isCooperation: Int,
|
||||
@SerialName("ugc_pay_preview")
|
||||
val ugcPayPreview: Int,
|
||||
@SerialName("no_background")
|
||||
val noBackground: Int,
|
||||
@SerialName("clean_mode")
|
||||
val cleanMode: Int? = null,
|
||||
@SerialName("is_stein_gate")
|
||||
val isSteinGate: Int? = null,
|
||||
@SerialName("is_360")
|
||||
val is360: Int? = null,
|
||||
@SerialName("no_share")
|
||||
val noShare: Int? = null,
|
||||
@SerialName("arc_pay")
|
||||
val arcPay: Int,
|
||||
@SerialName("pay_free_watch")
|
||||
val payFreeWatch: Int? = null
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* 视频作者
|
||||
*
|
||||
* @param mid UP主mid
|
||||
* @param name UP主昵称
|
||||
* @param face UP主头像
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoOwner(
|
||||
val mid: Long,
|
||||
val name: String,
|
||||
val face: String
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* 视频数据
|
||||
*
|
||||
* @param aid 稿件avid
|
||||
* @param view 播放数
|
||||
* @param danmaku 弹幕数
|
||||
* @param reply 评论数
|
||||
* @param favorite 收藏数
|
||||
* @param coin 投币数
|
||||
* @param share 分享数
|
||||
* @param nowRank 当前排名
|
||||
* @param hisRank 历史最高排行
|
||||
* @param like 获赞数
|
||||
* @param dislike 点踩数 恒为0
|
||||
* @param evaluation 视频评分
|
||||
* @param argueMsg 警告/争议提示信息
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoStat(
|
||||
val aid: Int,
|
||||
val view: Int,
|
||||
val danmaku: Int,
|
||||
val reply: Int,
|
||||
val favorite: Int,
|
||||
val coin: Int,
|
||||
val share: Int,
|
||||
@SerialName("now_rank")
|
||||
val nowRank: Int,
|
||||
@SerialName("his_rank")
|
||||
val hisRank: Int,
|
||||
val like: Int,
|
||||
val dislike: Int,
|
||||
val evaluation: String = "",
|
||||
@SerialName("argue_msg")
|
||||
val argueMsg: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* 分辨率
|
||||
*
|
||||
* @param width 当前分P 宽度
|
||||
* @param height 当前分P 高度
|
||||
* @param rotate 是否将宽高对换 0:正常,1:对换
|
||||
*/
|
||||
@Serializable
|
||||
data class Dimension(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val rotate: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Premiere(
|
||||
val state: Int,
|
||||
@SerialName("start_time")
|
||||
val startTime: Long,
|
||||
@SerialName("room_id")
|
||||
val roomId: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 视频分P
|
||||
*
|
||||
* @param cid 分P cid
|
||||
* @param page 分P序号 从1开始
|
||||
* @param from 视频来源 vupload:普通上传(B站)
|
||||
* @param hunan:芒果TV
|
||||
* @param qq:腾讯
|
||||
* @param part 分P标题
|
||||
* @param duration 分P持续时间 单位为秒
|
||||
* @param vid 站外视频vid 仅站外视频有效
|
||||
* @param weblink 站外视频跳转url 仅站外视频有效
|
||||
* @param dimension 当前分P分辨率 部分较老视频无分辨率值
|
||||
*/
|
||||
@Serializable
|
||||
data class VideoPage(
|
||||
val cid: Int,
|
||||
val page: Int,
|
||||
val from: String,
|
||||
val part: String,
|
||||
val duration: Int,
|
||||
val vid: String,
|
||||
val weblink: String,
|
||||
val dimension: Dimension
|
||||
)
|
||||
|
||||
/**
|
||||
* 推荐理由
|
||||
*
|
||||
* @param honor
|
||||
*/
|
||||
@Serializable
|
||||
data class HonorReply(
|
||||
val honor: List<HonorReplyItem> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* 推荐信息
|
||||
*
|
||||
* @param aid 当前稿件aid
|
||||
* @param type 2:第?期每周必看 3:全站排行榜最高第?名 4:热门
|
||||
* @param desc 描述
|
||||
* @param weeklyRecommendNum
|
||||
*/
|
||||
@Serializable
|
||||
data class HonorReplyItem(
|
||||
val aid: Int,
|
||||
val type: Int,
|
||||
val desc: String,
|
||||
@SerialName("weekly_recommend_num")
|
||||
val weeklyRecommendNum: Int
|
||||
)
|
@ -0,0 +1,12 @@
|
||||
package dev.aaa1115910.biliapi.entity.video
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
data class VideoInfoResponse(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ttl: Int,
|
||||
val data: VideoInfo
|
||||
)
|
32
bili-api/src/main/kotlin/dev/aaa1115910/biliapi/util/Zlib.kt
Normal file
32
bili-api/src/main/kotlin/dev/aaa1115910/biliapi/util/Zlib.kt
Normal file
@ -0,0 +1,32 @@
|
||||
package dev.aaa1115910.biliapi.util
|
||||
|
||||
import io.ktor.utils.io.core.use
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.Inflater
|
||||
|
||||
fun ByteArray.zlibCompress(): ByteArray {
|
||||
val output = ByteArray(this.size * 4)
|
||||
val compressor = Deflater().apply {
|
||||
setInput(this@zlibCompress)
|
||||
finish()
|
||||
}
|
||||
val compressedDataLength: Int = compressor.deflate(output)
|
||||
return output.copyOfRange(0, compressedDataLength)
|
||||
}
|
||||
|
||||
fun ByteArray.zlibDecompress(): ByteArray {
|
||||
val inflater = Inflater()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
return outputStream.use {
|
||||
val buffer = ByteArray(1024)
|
||||
inflater.setInput(this)
|
||||
var count = -1
|
||||
while (count != 0) {
|
||||
count = inflater.inflate(buffer)
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
inflater.end()
|
||||
outputStream.toByteArray()
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package dev.aaa1115910.biliapi
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class BiliApiTest {
|
||||
|
||||
@Test
|
||||
fun getPopularVideoData() {
|
||||
assertDoesNotThrow {
|
||||
runBlocking {
|
||||
val response = BiliApi.getPopularVideoData()
|
||||
println(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getVideoInfo() {
|
||||
assertDoesNotThrow {
|
||||
runBlocking {
|
||||
val response = BiliApi.getVideoInfo(av = 170001)
|
||||
println(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getVideoPlayUrl() {
|
||||
assertDoesNotThrow {
|
||||
runBlocking {
|
||||
val response = BiliApi.getVideoPlayUrl(av = 170001, cid = 267714)
|
||||
println(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDanmakuXml() {
|
||||
assertDoesNotThrow {
|
||||
runBlocking {
|
||||
val response = BiliApi.getDanmakuXml(cid = 267714)
|
||||
println(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLiveDanmuHistory() {
|
||||
assertDoesNotThrow {
|
||||
runBlocking {
|
||||
val response = BiliApi.getLiveDanmuHistory(roomId = 22739471)
|
||||
println(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package dev.aaa1115910.biliapi
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class LiveDataWebSocketTest {
|
||||
|
||||
@Test
|
||||
fun connectLiveEvent() {
|
||||
runBlocking {
|
||||
LiveDataWebSocket.connectLiveEvent(5555) {
|
||||
println(it)
|
||||
}
|
||||
for (i in 1..10) {
|
||||
delay(1_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,5 +2,6 @@ plugins {
|
||||
alias(gradleLibs.plugins.android.application) apply false
|
||||
alias(gradleLibs.plugins.android.library) apply false
|
||||
alias(gradleLibs.plugins.kotlin.android) apply false
|
||||
alias(gradleLibs.plugins.kotlin.jvm) apply false
|
||||
alias(gradleLibs.plugins.kotlin.serialization) apply false
|
||||
}
|
@ -6,4 +6,5 @@ kotlin = "1.7.20"
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
@ -23,6 +23,9 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||
|
||||
# https://kotlinlang.org/docs/jvm-test-using-junit.html
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" }
|
||||
|
||||
# https://github.com/Kotlin/kotlinx.serialization
|
||||
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
|
||||
|
@ -20,3 +20,4 @@ dependencyResolutionManagement {
|
||||
}
|
||||
rootProject.name = "BV"
|
||||
include(":app")
|
||||
include(":bili-api")
|
||||
|
Reference in New Issue
Block a user