Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions app/src/main/java/com/muedsa/tvbox/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayAuthInterceptor
import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayDanmakuProvider
import com.muedsa.tvbox.danmaku.iqiyi.IqiyiDanmakuProvider
import com.muedsa.tvbox.danmaku.iqiyi.IqiyiSearchApiService
import com.muedsa.tvbox.danmaku.youku.YoukuApiService
import com.muedsa.tvbox.danmaku.youku.YoukuDanmakuProvider
import com.muedsa.tvbox.room.AppDatabase
import com.muedsa.tvbox.store.DataStoreRepo
import com.muedsa.tvbox.store.PluginPerfStore
Expand Down Expand Up @@ -61,15 +63,19 @@ internal object AppModule {

@Provides
@Singleton
fun provideOkhttpCookieJar(dataStoreRepo: DataStoreRepo) = PluginCookieJar(
saver = SharedCookieSaver(
store = PluginPerfStore(
pluginPackage = BuildConfig.APPLICATION_ID,
pluginDataStore = dataStoreRepo.dataStore,
),
fun provideSharedCookieSaver(dataStoreRepo: DataStoreRepo) = SharedCookieSaver(
store = PluginPerfStore(
pluginPackage = BuildConfig.APPLICATION_ID,
pluginDataStore = dataStoreRepo.dataStore,
),
)

@Provides
@Singleton
fun provideOkhttpCookieJar(cookieSaver: SharedCookieSaver) = PluginCookieJar(
saver = cookieSaver,
)

@Provides
@Singleton
fun provideOkHttpClient(
Expand Down Expand Up @@ -110,17 +116,34 @@ internal object AppModule {
),
)

@Provides
@Singleton
fun provideYoukuDanmakuProvider(
cookieSaver: SharedCookieSaver,
okHttpClient: OkHttpClient,
) = YoukuDanmakuProvider(
cookieSaver = cookieSaver,
okHttpClient = okHttpClient,
youkuApiService = createJsonRetrofit(
baseUrl = "https://openapi.youku.com/",
service = YoukuApiService::class.java,
okHttpClient = okHttpClient,
),
)

@Provides
@Singleton
fun provideDanmakuService(
danDanPlayDanmakuProvider: DanDanPlayDanmakuProvider,
iqiyiDanmakuProvider: IqiyiDanmakuProvider,
youkuDanmakuProvider: YoukuDanmakuProvider
) = DanmakuService().also {
if (BuildConfig.DANDANPLAY_APP_ID.isNotEmpty()
&& BuildConfig.DANDANPLAY_APP_SECRET.isNotEmpty()
) {
it.register(danDanPlayDanmakuProvider)
}
it.register(iqiyiDanmakuProvider)
it.register(youkuDanmakuProvider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.muedsa.tvbox.danmaku.youku


import com.muedsa.tvbox.model.youku.YoukuRpcResp
import com.muedsa.tvbox.model.youku.YoukuSearchResp
import com.muedsa.tvbox.model.youku.YoukuVideos
import com.muedsa.tvbox.tool.ChromeUserAgent
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query

interface YoukuApiService {

@GET("https://search.youku.com/api/search")
suspend fun search(
@Query("keyword") keyword: String,
@Query("userAgent") userAgent: String = ChromeUserAgent,
@Query("site") site: Int = 1,
@Query("categories") categories: Int = 0,
@Query("ftype") fType: Int = 0,
@Query("ob") ob: Int = 0,
@Query("pg") pg: Int = 1,
): YoukuSearchResp

@GET("v2/shows/videos.json")
suspend fun videos(
@Query("client_id") clientId: String = "53e6cc67237fc59a",
@Query("package") clientPackage: String = "com.huawei.hwvplayer.youku",
@Query("ext") ext: String = "show",
@Query("show_id") showId: String,
): YoukuVideos

@POST("https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/")
@FormUrlEncoded
suspend fun danmuList(
@Field("data") data: String,
@Query("jsv") jsv: String = "2.7.0",
@Query("appKey") appKey: String,
@Query("t") t: Long,
@Query("sign") sign: String,
@Query("api") api: String = "mopen.youku.danmu.list",
@Query("v") v: String = "1.0",
@Query("type") type: String = "originaljson",
@Query("dataType") dataType: String = "jsonp",
@Query("timeout") timeout: Int = 20000,
@Query("jsonpIncPrefix") jsonpIncPrefix: String = "utility",
@Header("Referer") referer: String = "https://v.youku.com"
): YoukuRpcResp
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.muedsa.tvbox.danmaku.youku

import com.kuaishou.akdanmaku.data.DanmakuItemData
import com.muedsa.tvbox.danmaku.DanmakuProvider
import com.muedsa.tvbox.model.DanmakuEpisode
import com.muedsa.tvbox.model.DanmakuMedia
import com.muedsa.tvbox.model.youku.YoukuDanmakuProperties
import com.muedsa.tvbox.model.youku.YoukuDanmakuReqMsg
import com.muedsa.tvbox.model.youku.YoukuDanmakuReqSignedMsg
import com.muedsa.tvbox.model.youku.YoukuDanmakuResp
import com.muedsa.tvbox.tool.LenientJson
import com.muedsa.tvbox.tool.SharedCookieSaver
import com.muedsa.tvbox.tool.encodeBase64
import com.muedsa.tvbox.tool.feignChrome
import com.muedsa.tvbox.tool.get
import com.muedsa.tvbox.tool.md5
import com.muedsa.tvbox.tool.toRequestBuild
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
import kotlin.math.ceil
import kotlin.time.Duration.Companion.milliseconds

class YoukuDanmakuProvider(
private val cookieSaver: SharedCookieSaver,
private val okHttpClient: OkHttpClient,
private val youkuApiService: YoukuApiService,
) : DanmakuProvider {

init {
cookieSaver.load().filter { it.domain == "youku.com" || it.domain == "mmstat.com" }
.forEach { cookieSaver.remove(it) }
}

override val name: String = "优酷"

var cna: String? = null
var token: String? = null
var tokenEnc: String? = null

override suspend fun searchMedia(keyword: String): List<DanmakuMedia> {
val resp = youkuApiService.search(keyword = keyword)
return resp.pageComponentList
.mapNotNull { it.commonData }
.filter { it.isYouku == 1 && it.hasYouku == 1 && it.ugcSupply == 0 }
.map {
val feats = it.feature.split(" ")
DanmakuMedia(
provider = name,
mediaId = it.showId,
mediaName = it.titleDTO.displayName,
publishDate = if (feats.size > 2) "${feats[0]} ${feats[1]}" else feats[0],
episodes = emptyList(),
)
}
}

override suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? {
val resp = youkuApiService.videos(showId = media.mediaId)
return resp.videos?.let {
DanmakuMedia(
provider = name,
mediaId = media.mediaId,
mediaName = media.mediaName,
publishDate = media.publishDate,
episodes = it.map {
DanmakuEpisode(
provider = name,
mediaId = media.mediaId,
mediaName = media.mediaName,
episodeId = it.id,
episodeName = it.title,
extendData = it.duration
)
}
)
}
}

override suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List<DanmakuItemData> {
ensureToken()
var mat = 0
val maxMat = ceil(episode.extendData!!.toDouble() / 60).toInt()
val list = mutableListOf<DanmakuItemData>()
while (mat < maxMat) {
val t = System.currentTimeMillis()
val msg = YoukuDanmakuReqMsg(
ctime = t,
guid = cna!!,
vid = episode.episodeId,
mat = mat
)
val msgJson = LenientJson.encodeToString(msg)
val msgEnc = msgJson.toByteArray(Charsets.UTF_8).encodeBase64()
val msgSign = generateMsgSign(msgEnc)
val signedMsg = YoukuDanmakuReqSignedMsg.from(msg = msg, msgEnc = msgEnc, msgSign = msgSign)
val appKey = "24679788"
val data = LenientJson.encodeToString(signedMsg)
val resp = youkuApiService.danmuList(
data = data,
appKey = appKey,
t = t,
sign = generateTokenSign("$t", appKey, data)
)
if (!resp.data?.result.isNullOrBlank()) {
val danmakuResp = LenientJson.decodeFromString<YoukuDanmakuResp>(resp.data.result)
danmakuResp.data?.result?.forEach {
val props = LenientJson.decodeFromString<YoukuDanmakuProperties>(it.properties)
list.add(
DanmakuItemData(
danmakuId = it.id,
position = it.playAt,
content = it.content,
mode = DanmakuItemData.DANMAKU_MODE_ROLLING,
textSize = 25,
textColor = props.color,
score = 9,
danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE
)
)
}
}
mat++
delay(100.milliseconds)
}
return list
}

fun ensureToken() {
var cookies = cookieSaver.load()
cna = cookies.find { it.name == "cna" && (it.domain == "youku.com" || it.domain == "mmstat.com") }?.value
if (cna.isNullOrBlank()) {
"https://log.mmstat.com/eg.js"
.toRequestBuild()
.feignChrome(referer = "https://youku.com/")
.get(okHttpClient)
cookies = cookieSaver.load()
cna = cookies.find { it.name == "cna" && (it.domain == "youku.com" || it.domain == "mmstat.com") }?.value
}

token = cookies.find { it.name == "_m_h5_tk" && it.domain == "youku.com" }?.value
tokenEnc = cookies.find { it.name == "_m_h5_tk_enc" && it.domain == "youku.com" }?.value
if (token.isNullOrBlank() || tokenEnc.isNullOrBlank()) {
"https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788"
.toRequestBuild()
.feignChrome(referer = "https://youku.com/")
.get(okHttpClient)
cookies = cookieSaver.load()
token = cookies.find { it.name == "_m_h5_tk" && it.domain == "youku.com" }?.value
tokenEnc = cookies.find { it.name == "_m_h5_tk_enc" && it.domain == "youku.com" }?.value
}
}

@OptIn(ExperimentalStdlibApi::class)
fun generateMsgSign(msgEnc: String): String =
"${msgEnc}MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr".md5().toHexString()

@OptIn(ExperimentalStdlibApi::class)
fun generateTokenSign(t: String, appKey: String, data: String): String =
listOf(token!!.substring(0, 32), t, appKey, data).joinToString("&").md5().toHexString()
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/model/youku/YoukuDanmaku.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmaku(
@SerialName("id") val id: Long = 0,
@SerialName("content") val content: String,
@SerialName("propertis") val properties: String,
@SerialName("playat") val playAt: Long, //ms
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmakuProperties(
@SerialName("size") val size: Int,
@SerialName("color") val color: Int,
@SerialName("pos") val pos: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmakuReqMsg(
@SerialName("pid") val pid: Int = 0,
@SerialName("ctype") val ctype: Int = 10004,
@SerialName("sver") val sver: String = "3.1.0",
@SerialName("cver") val cver: String = "v1.0",
@SerialName("ctime") val ctime: Long,
@SerialName("guid") val guid: String,
@SerialName("vid") val vid: String,
@SerialName("mat") val mat: Int,
@SerialName("mcount") val mcount: Int = 1,
@SerialName("type") val type: Int = 1,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmakuReqSignedMsg(
@SerialName("pid") val pid: Int = 0,
@SerialName("ctype") val ctype: Int = 10004,
@SerialName("sver") val sver: String = "3.1.0",
@SerialName("cver") val cver: String = "v1.0",
@SerialName("ctime") val ctime: Long,
@SerialName("guid") val guid: String,
@SerialName("vid") val vid: String,
@SerialName("mat") val mat: Int,
@SerialName("mcount") val mcount: Int = 1,
@SerialName("type") val type: Int = 1,
@SerialName("msg") val msg: String,
@SerialName("sign") val sign: String,
) {
companion object {
fun from(
msg: YoukuDanmakuReqMsg,
msgEnc: String,
msgSign: String
): YoukuDanmakuReqSignedMsg = YoukuDanmakuReqSignedMsg(
pid = msg.pid,
ctype = msg.ctype,
sver = msg.sver,
cver = msg.cver,
ctime = msg.ctime,
guid = msg.guid,
vid = msg.vid,
mat = msg.mat,
mcount = msg.mcount,
type = msg.type,
msg = msgEnc,
sign = msgSign,
)
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/model/youku/YoukuDanmakuResp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmakuResp(
// code
// cost
@SerialName("data") val data: YoukuDanmakuResult? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.muedsa.tvbox.model.youku

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class YoukuDanmakuResult(
@SerialName("result") val result: List<YoukuDanmaku> = emptyList(),
)
Loading