From cdb1cc1a1decba45dd000d3a62c5a2fd251a3a76 Mon Sep 17 00:00:00 2001 From: MUEDSA <7676275+muedsa@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:29:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=88=B1=E5=A5=87?= =?UTF-8?q?=E8=89=BA=E5=BC=B9=E5=B9=95=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/muedsa/tvbox/AppModule.kt | 16 ++ .../danmaku/iqiyi/IqiyiDanmakuProvider.kt | 146 ++++++++++++++++++ .../danmaku/iqiyi/IqiyiSearchApiService.kt | 19 +++ .../tvbox/model/iqiyi/IqiyiAlbumDocInfo.kt | 16 ++ .../model/iqiyi/IqiyiAlbumSearchResult.kt | 10 ++ .../tvbox/model/iqiyi/IqiyiAlbumVideoInfo.kt | 11 ++ .../com/muedsa/tvbox/model/iqiyi/IqiyiResp.kt | 10 ++ .../tvbox/model/iqiyi/IqiyiSearchDocInfo.kt | 11 ++ 8 files changed, 239 insertions(+) create mode 100644 app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiDanmakuProvider.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiSearchApiService.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumDocInfo.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumSearchResult.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumVideoInfo.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiResp.kt create mode 100644 app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiSearchDocInfo.kt diff --git a/app/src/main/java/com/muedsa/tvbox/AppModule.kt b/app/src/main/java/com/muedsa/tvbox/AppModule.kt index 820e4a5..eee8018 100644 --- a/app/src/main/java/com/muedsa/tvbox/AppModule.kt +++ b/app/src/main/java/com/muedsa/tvbox/AppModule.kt @@ -6,6 +6,8 @@ import com.muedsa.tvbox.danmaku.DanmakuService import com.muedsa.tvbox.danmaku.dandanplay.DanDanPlayApiService 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.room.AppDatabase import com.muedsa.tvbox.store.DataStoreRepo import com.muedsa.tvbox.store.PluginPerfStore @@ -21,6 +23,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.Cache +import okhttp3.OkHttpClient import javax.inject.Singleton @Module @@ -96,15 +99,28 @@ internal object AppModule { ) ) + @Provides + @Singleton + fun provideIqiyiDanmakuProvider(okHttpClient: OkHttpClient) = IqiyiDanmakuProvider( + okHttpClient = okHttpClient, + iqiyiSearchApiService = createJsonRetrofit( + baseUrl = "https://search.video.iqiyi.com/", + service = IqiyiSearchApiService::class.java, + okHttpClient = okHttpClient, + ), + ) + @Provides @Singleton fun provideDanmakuService( danDanPlayDanmakuProvider: DanDanPlayDanmakuProvider, + iqiyiDanmakuProvider: IqiyiDanmakuProvider, ) = DanmakuService().also { if (BuildConfig.DANDANPLAY_APP_ID.isNotEmpty() && BuildConfig.DANDANPLAY_APP_SECRET.isNotEmpty() ) { it.register(danDanPlayDanmakuProvider) } + it.register(iqiyiDanmakuProvider) } } \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiDanmakuProvider.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiDanmakuProvider.kt new file mode 100644 index 0000000..db54081 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiDanmakuProvider.kt @@ -0,0 +1,146 @@ +package com.muedsa.tvbox.danmaku.iqiyi + +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.iqiyi.IqiyiAlbumDocInfo +import com.muedsa.tvbox.tool.LenientJson +import com.muedsa.tvbox.tool.feignChrome +import com.muedsa.tvbox.tool.get +import com.muedsa.tvbox.tool.toRequestBuild +import com.muedsa.util.OkHttpCacheInterceptor +import kotlinx.coroutines.delay +import okhttp3.OkHttpClient +import org.jsoup.Jsoup +import org.jsoup.parser.Parser +import timber.log.Timber +import java.util.zip.InflaterInputStream +import kotlin.math.ceil +import kotlin.time.Duration.Companion.milliseconds + +class IqiyiDanmakuProvider( + val okHttpClient: OkHttpClient, + val iqiyiSearchApiService: IqiyiSearchApiService, +) : DanmakuProvider { + + override val name: String = "爱奇艺" + + override suspend fun searchMedia(keyword: String): List { + var resp = iqiyiSearchApiService.searchVideos(key = keyword) + return resp.data?.docInfos + ?.filter { it.score > 0.7f } + ?.map { it.albumDocInfo } + ?.filter { + it.albumId != null && it.siteId == "iqiyi" && it.videoDocType == 1 + && IQIYI_CHANNEL_LIST.contains(it.channel) + }?.map { + DanmakuMedia( + provider = name, + mediaId = it.albumId!!.toString(), + mediaName = "${it.albumTitle}(${it.channel.split(",")[0]})", + publishDate = it.releaseDate, + rating = it.score?.toString(), + episodes = emptyList(), + extendData = LenientJson.encodeToString(it) + ) + } + ?: emptyList() + } + + override suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? { + if (media.extendData.isNullOrBlank()) return null + val album = LenientJson.decodeFromString(media.extendData) + return if(album.videoInfos.all { it.tvId != null && it.timeLength != null }) { + DanmakuMedia( + provider = name, + mediaId = media.mediaId, + mediaName = media.mediaName, + publishDate = media.publishDate, + rating = media.rating, + episodes = album.videoInfos.map { + DanmakuEpisode( + provider = name, + mediaId = media.mediaId, + mediaName = media.mediaName, + episodeId = it.tvId?.toString() ?: media.mediaId, + episodeName = it.itemTitle, + extendData = it.timeLength?.toString() + ) + }, + extendData = media.extendData + ) + } else { + Timber.d(media.extendData) + null + } + } + + @OptIn(ExperimentalStdlibApi::class) + override suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List { + var mat = 1 + val maxMat = ceil(episode.extendData!!.toInt() / 300.0) + val tvId = episode.episodeId.toString() + val s1 = tvId.substring(tvId.length - 4, tvId.length - 2) + val s2 = tvId.substring(tvId.length - 2) + val list = mutableListOf() + while (mat < maxMat) { + val byteStream = + "http://cmts.iqiyi.com/bullet/${s1}/${s2}/${tvId}_300_${mat}.z".toRequestBuild() + .feignChrome() + .header(OkHttpCacheInterceptor.HEADER, "max-age=21600") + .get(okHttpClient = okHttpClient) + .body?.byteStream() + if (byteStream == null) { + break + } + val unzipBytes = InflaterInputStream(byteStream).use { it.readBytes() } + val xml = unzipBytes.toString(Charsets.UTF_8) + val doc = Jsoup.parse(xml, Parser.xmlParser()) + val bulletEls = doc.select("danmu >data >entry >list >bulletInfo") + bulletEls.forEach { + try { + val contentId = it.select("contentId").text().toLong() + val content = it.select("content").text() + val showTime = it.select("showTime").text().toLong() + val color = it.select("color").text().hexToInt() + val scoreLevel = it.select("scoreLevel").text().toInt() + val font = it.select("font").text().toInt() + val contentType = it.select("contentType").text().toInt() + list.add( + DanmakuItemData( + danmakuId = contentId.toLong(), + position = showTime * 1000, + content = content, + mode = when(contentType) { + 100 -> DanmakuItemData.DANMAKU_MODE_CENTER_TOP + 200 -> DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM + else -> DanmakuItemData.DANMAKU_MODE_ROLLING + }, + textSize = font, + textColor = color, + score = scoreLevel, + danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE + ) + ) + } catch (throwable: Throwable) { + Timber.e(throwable, it.outerHtml()) + } + } + mat++ + delay(100.milliseconds) + } + return list + } + + companion object { + val IQIYI_CHANNEL_LIST = listOf( + "电影,1", + "电视剧,2", + "纪录片,3", + "动漫,4", + "音乐,5", + "综艺,6", + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiSearchApiService.kt b/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiSearchApiService.kt new file mode 100644 index 0000000..e28a815 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/danmaku/iqiyi/IqiyiSearchApiService.kt @@ -0,0 +1,19 @@ +package com.muedsa.tvbox.danmaku.iqiyi + +import com.muedsa.tvbox.model.iqiyi.IqiyiAlbumSearchResult +import com.muedsa.tvbox.model.iqiyi.IqiyiResp +import com.muedsa.util.OkHttpCacheInterceptor +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface IqiyiSearchApiService { + @GET("o") + suspend fun searchVideos( + @Query("if") ifP: String = "html5", + @Query("key") key: String, + @Query("pageNum") pageNum: Int = 1, + @Query("pageSize") pageSize: Int = 20, + @Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=3600", + ): IqiyiResp +} \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumDocInfo.kt b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumDocInfo.kt new file mode 100644 index 0000000..6837f93 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumDocInfo.kt @@ -0,0 +1,16 @@ +package com.muedsa.tvbox.model.iqiyi + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class IqiyiAlbumDocInfo( + @SerialName("albumId") val albumId: Long? = null, + @SerialName("albumTitle") val albumTitle: String, + @SerialName("releaseDate") val releaseDate: String, + @SerialName("siteId") val siteId: String, + @SerialName("score") val score: Float? = null, + @SerialName("channel") val channel: String, + @SerialName("videoDocType") val videoDocType: Int, + @SerialName("videoinfos") val videoInfos: List = emptyList(), +) diff --git a/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumSearchResult.kt b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumSearchResult.kt new file mode 100644 index 0000000..0a2a975 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumSearchResult.kt @@ -0,0 +1,10 @@ +package com.muedsa.tvbox.model.iqiyi + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class IqiyiAlbumSearchResult( + @SerialName("code") val code: Int = -1, + @SerialName("docinfos") val docInfos: List = emptyList(), +) \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumVideoInfo.kt b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumVideoInfo.kt new file mode 100644 index 0000000..5df6de5 --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiAlbumVideoInfo.kt @@ -0,0 +1,11 @@ +package com.muedsa.tvbox.model.iqiyi + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class IqiyiAlbumVideoInfo( + @SerialName("tvId") val tvId: Long? = null, + @SerialName("itemTitle") val itemTitle: String, + @SerialName("timeLength") val timeLength: Int? = null, +) diff --git a/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiResp.kt b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiResp.kt new file mode 100644 index 0000000..e4cfb5e --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiResp.kt @@ -0,0 +1,10 @@ +package com.muedsa.tvbox.model.iqiyi + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class IqiyiResp( + @SerialName("code") val code: String = "", + @SerialName("data") val data: T? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiSearchDocInfo.kt b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiSearchDocInfo.kt new file mode 100644 index 0000000..b16f45d --- /dev/null +++ b/app/src/main/java/com/muedsa/tvbox/model/iqiyi/IqiyiSearchDocInfo.kt @@ -0,0 +1,11 @@ +package com.muedsa.tvbox.model.iqiyi + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class IqiyiSearchDocInfo( + @SerialName("doc_id") val docId: String, + @SerialName("score") val score: Float, + @SerialName("albumDocInfo") val albumDocInfo: IqiyiAlbumDocInfo, +)