Skip to content

Commit bf7fe4d

Browse files
authored
Merge pull request #61 from muedsa/danmaku_tencent
feat: 添加腾讯视频弹幕源
2 parents 453557e + ecb77c5 commit bf7fe4d

22 files changed

Lines changed: 438 additions & 1 deletion

app/src/main/java/com/muedsa/tvbox/AppModule.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayAuthInterceptor
88
import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayDanmakuProvider
99
import com.muedsa.tvbox.danmaku.iqiyi.IqiyiDanmakuProvider
1010
import com.muedsa.tvbox.danmaku.iqiyi.IqiyiSearchApiService
11+
import com.muedsa.tvbox.danmaku.tencent.TencentDanmakuProvider
12+
import com.muedsa.tvbox.danmaku.tencent.TencentVideoApiService
1113
import com.muedsa.tvbox.danmaku.youku.YoukuApiService
1214
import com.muedsa.tvbox.danmaku.youku.YoukuDanmakuProvider
1315
import com.muedsa.tvbox.room.AppDatabase
@@ -131,12 +133,25 @@ internal object AppModule {
131133
),
132134
)
133135

136+
@Provides
137+
@Singleton
138+
fun provideTencentDanmakuProvider(
139+
okHttpClient: OkHttpClient,
140+
) = TencentDanmakuProvider(
141+
tencentVideoApiService = createJsonRetrofit(
142+
baseUrl = "https://pbaccess.video.qq.com/",
143+
service = TencentVideoApiService::class.java,
144+
okHttpClient = okHttpClient,
145+
),
146+
)
147+
134148
@Provides
135149
@Singleton
136150
fun provideDanmakuService(
137151
danDanPlayDanmakuProvider: DanDanPlayDanmakuProvider,
138152
iqiyiDanmakuProvider: IqiyiDanmakuProvider,
139-
youkuDanmakuProvider: YoukuDanmakuProvider
153+
youkuDanmakuProvider: YoukuDanmakuProvider,
154+
tencentDanmakuProvider: TencentDanmakuProvider,
140155
) = DanmakuService().also {
141156
if (BuildConfig.DANDANPLAY_APP_ID.isNotEmpty()
142157
&& BuildConfig.DANDANPLAY_APP_SECRET.isNotEmpty()
@@ -145,5 +160,6 @@ internal object AppModule {
145160
}
146161
it.register(iqiyiDanmakuProvider)
147162
it.register(youkuDanmakuProvider)
163+
it.register(tencentDanmakuProvider)
148164
}
149165
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.muedsa.tvbox.danmaku.tencent
2+
3+
import com.kuaishou.akdanmaku.data.DanmakuItemData
4+
import com.muedsa.tvbox.danmaku.DanmakuProvider
5+
import com.muedsa.tvbox.model.DanmakuEpisode
6+
import com.muedsa.tvbox.model.DanmakuMedia
7+
import com.muedsa.tvbox.model.tencent.TencentVideoBarrageContentStyle
8+
import com.muedsa.tvbox.model.tencent.TencentVideoInfo
9+
import com.muedsa.tvbox.model.tencent.TencentVideoPageDataReq
10+
import com.muedsa.tvbox.model.tencent.TencentVideoPageParams
11+
import com.muedsa.tvbox.model.tencent.TencentVideoSearchReq
12+
import com.muedsa.tvbox.tool.LenientJson
13+
import kotlinx.coroutines.delay
14+
import timber.log.Timber
15+
import kotlin.time.Duration.Companion.milliseconds
16+
import kotlin.uuid.ExperimentalUuidApi
17+
import kotlin.uuid.Uuid
18+
19+
class TencentDanmakuProvider(
20+
private val tencentVideoApiService: TencentVideoApiService,
21+
) : DanmakuProvider {
22+
23+
override val name: String = "腾讯"
24+
25+
@OptIn(ExperimentalUuidApi::class)
26+
override suspend fun searchMedia(keyword: String): List<DanmakuMedia> {
27+
val req = TencentVideoSearchReq(
28+
query = keyword,
29+
filterValue = "firstTabid=150",
30+
uuid = Uuid.random().toString().uppercase()
31+
)
32+
val resp = tencentVideoApiService.search(req)
33+
return resp.data?.normalList?.itemList?.mapNotNull { it.videoInfo }
34+
?.mapNotNull {
35+
val siteInfo = it.playSites.find {
36+
it.enName == "qq" && it.playSourceType == 1 && !it.episodeInfoList.isEmpty()
37+
}
38+
if (siteInfo != null) {
39+
val firstEpisode = siteInfo.episodeInfoList[0]
40+
DanmakuMedia(
41+
provider = name,
42+
mediaId = firstEpisode.cid,
43+
mediaName = "${it.titleWithoutEm}(${it.typeName})",
44+
publishDate = "${it.year}",
45+
episodes = emptyList(),
46+
extendData = LenientJson.encodeToString(it)
47+
)
48+
} else null
49+
} ?: emptyList()
50+
51+
}
52+
53+
override suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? {
54+
val videoInfo = LenientJson.decodeFromString<TencentVideoInfo>(media.extendData!!)
55+
val firstEpisode = videoInfo.playSites.find {
56+
it.enName == "qq" && it.playSourceType == 1 && !it.episodeInfoList.isEmpty()
57+
}!!.episodeInfoList[0]
58+
val req = TencentVideoPageDataReq(
59+
pageParams = TencentVideoPageParams(
60+
cid = media.mediaId,
61+
vid = firstEpisode.id,
62+
pageId = "vsite_episode_list"
63+
)
64+
)
65+
val resp = tencentVideoApiService.getPageData(req)
66+
val episodes =
67+
resp.data?.moduleListDatas?.get(0)?.moduleDatas?.get(0)?.itemDataLists?.itemDatas?.filter {
68+
it.itemType == "1"
69+
}
70+
if (episodes.isNullOrEmpty()) return null
71+
72+
return DanmakuMedia(
73+
provider = name,
74+
mediaId = media.mediaId,
75+
mediaName = media.mediaName,
76+
publishDate = media.publishDate,
77+
episodes = episodes.mapNotNull {
78+
val cid = it.itemParams?.get("cid")
79+
val vid = it.itemParams?.get("vid")
80+
val playTitle = it.itemParams?.get("play_title")
81+
val duration = it.itemParams?.get("duration")
82+
if (cid != null && vid != null && playTitle != null && duration != null) {
83+
DanmakuEpisode(
84+
provider = name,
85+
mediaId = cid,
86+
mediaName = media.mediaName,
87+
episodeId = vid,
88+
episodeName = playTitle,
89+
extendData = duration,
90+
)
91+
} else null
92+
},
93+
extendData = media.extendData
94+
)
95+
}
96+
97+
@OptIn(ExperimentalStdlibApi::class)
98+
override suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List<DanmakuItemData> {
99+
val duration = episode.extendData!!.toDouble() * 1000
100+
val list = mutableListOf<DanmakuItemData>()
101+
var startMs = 0
102+
var endMs = ONCE_OFFSET
103+
while (startMs < duration) {
104+
try {
105+
val resp = tencentVideoApiService.barrageSegment(
106+
vid = episode.episodeId,
107+
startMs = startMs,
108+
endMs = endMs,
109+
)
110+
resp.barrageList.forEach {
111+
var color = 0xFF_FF_FF
112+
if (!it.contentStyle.isNullOrBlank()) {
113+
LenientJson.decodeFromString<TencentVideoBarrageContentStyle>(it.contentStyle)
114+
.color?.let {
115+
color = it.hexToInt()
116+
}
117+
}
118+
list.add(
119+
DanmakuItemData(
120+
danmakuId = it.id.toLong(),
121+
position = it.timeOffset.toLong(),
122+
content = it.content,
123+
mode = DanmakuItemData.DANMAKU_MODE_ROLLING,
124+
textSize = 25,
125+
textColor = color,
126+
score = 9,
127+
danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE
128+
)
129+
)
130+
}
131+
} catch (throwable: Throwable) {
132+
Timber.e(throwable)
133+
}
134+
startMs = startMs + ONCE_OFFSET
135+
endMs = startMs + ONCE_OFFSET
136+
delay(100.milliseconds)
137+
}
138+
return list
139+
}
140+
141+
companion object {
142+
const val ONCE_OFFSET = 2 * 60 * 1000
143+
}
144+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.muedsa.tvbox.danmaku.tencent
2+
3+
4+
import com.google.common.net.HttpHeaders
5+
import com.muedsa.tvbox.model.tencent.TencentVideoBarrageResp
6+
import com.muedsa.tvbox.model.tencent.TencentVideoPageData
7+
import com.muedsa.tvbox.model.tencent.TencentVideoPageDataReq
8+
import com.muedsa.tvbox.model.tencent.TencentVideoResp
9+
import com.muedsa.tvbox.model.tencent.TencentVideoSearchReq
10+
import com.muedsa.tvbox.model.tencent.TencentVideoSearchResult
11+
import retrofit2.http.Body
12+
import retrofit2.http.GET
13+
import retrofit2.http.Header
14+
import retrofit2.http.POST
15+
import retrofit2.http.Path
16+
import retrofit2.http.Query
17+
18+
interface TencentVideoApiService {
19+
20+
@POST("trpc.videosearch.mobile_search.HttpMobileRecall/MbSearchHttp")
21+
suspend fun search(
22+
@Body req: TencentVideoSearchReq,
23+
@Query("vplatform") videoPlatform: Int = 5,
24+
@Header(HttpHeaders.REFERER) referer: String = "https://m.v.qq.com/"
25+
): TencentVideoResp<TencentVideoSearchResult>
26+
27+
@POST("trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData")
28+
suspend fun getPageData(
29+
@Body req: TencentVideoPageDataReq,
30+
@Query("video_appid") videoAppId: Int = 3000010,
31+
@Query("vplatform") videoPlatform: Int = 2,
32+
@Query("vversion_name") videoVersionName: String = "8.2.98",
33+
@Header(HttpHeaders.REFERER) referer: String = "https://v.qq.com/"
34+
): TencentVideoResp<TencentVideoPageData>
35+
36+
@GET("https://dm.video.qq.com/barrage/segment/{vid}/t/v1/{startMs}/{endMs}")
37+
suspend fun barrageSegment(
38+
@Path("vid") vid: String,
39+
@Path("startMs") startMs: Int,
40+
@Path("endMs") endMs: Int,
41+
@Header(HttpHeaders.REFERER) referer: String = "https://v.qq.com/"
42+
): TencentVideoBarrageResp
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoBarrage(
8+
@SerialName("id") val id: String,
9+
@SerialName("time_offset") val timeOffset: String,
10+
@SerialName("content_style") val contentStyle: String? = null,
11+
@SerialName("content") val content: String,
12+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoBarrageContentStyle(
8+
@SerialName("color") val color: String? = null,
9+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoBarrageResp(
8+
@SerialName("barrage_list") val barrageList: List<TencentVideoBarrage> = emptyList()
9+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoEpisodeInfo(
8+
@SerialName("id") val id: String,
9+
@SerialName("dataType") val dataType: String,
10+
@SerialName("url") val url: String,
11+
@SerialName("title") val title: String,
12+
@SerialName("duration") val duration: String,
13+
) {
14+
@delegate:Transient
15+
val cid: String by lazy {
16+
url.removePrefix("https://v.qq.com/x/cover/").removeSuffix(".html").split("/")[0]
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoInfo(
8+
@SerialName("videoType") val videoType: Int,
9+
@SerialName("typeName") val typeName: String,
10+
@SerialName("title") val title: String,
11+
@SerialName("year") val year: Int,
12+
@SerialName("playSites") val playSites: List<TencentVideoSiteInfo> = emptyList(),
13+
) {
14+
@delegate:Transient
15+
val titleWithoutEm by lazy { title.replace("<em>", "").replace("</em>", "") }
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoPageData(
8+
@SerialName("module_list_datas") val moduleListDatas: List<TencentVideoPageModuleListDatas>? = null,
9+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.muedsa.tvbox.model.tencent
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class TencentVideoPageDataReq(
8+
@SerialName("has_cache") val hasCache: Int = 1,
9+
@SerialName("page_params") val pageParams: TencentVideoPageParams,
10+
)

0 commit comments

Comments
 (0)