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
47 changes: 38 additions & 9 deletions app/src/main/java/com/muedsa/tvbox/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ package com.muedsa.tvbox

import android.content.Context
import androidx.room.Room
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.room.AppDatabase
import com.muedsa.tvbox.service.DanDanPlayApiService
import com.muedsa.tvbox.service.DanDanPlayAuthInterceptor
import com.muedsa.tvbox.store.DataStoreRepo
import com.muedsa.tvbox.tool.createJsonRetrofit
import com.muedsa.tvbox.tool.createOkHttpClient
import com.muedsa.util.AppUtil
import com.muedsa.util.OkHttpCacheInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Cache
import javax.inject.Singleton

@Module
Expand Down Expand Up @@ -44,12 +48,37 @@ internal object AppModule {

@Provides
@Singleton
fun provideDanDanPlayApiService(@ApplicationContext context: Context): DanDanPlayApiService =
createJsonRetrofit(
baseUrl = "https://api.dandanplay.net/api/",
service = DanDanPlayApiService::class.java,
okHttpClient = createOkHttpClient(debug = AppUtil.debuggable(context)) {
addInterceptor(DanDanPlayAuthInterceptor())
}
fun provideOkhttpCache(@ApplicationContext context: Context) = Cache(
directory = context.cacheDir.resolve("http_cache"),
maxSize = 50 * 1024 * 1024,
)

@Provides
@Singleton
fun provideDanDanPlayDanmakuProvider(
@ApplicationContext context: Context,
okHttpCache: Cache,
) = DanDanPlayDanmakuProvider(
danDanPlayApiService = createJsonRetrofit(
baseUrl = "https://api.dandanplay.net/api/",
service = DanDanPlayApiService::class.java,
okHttpClient = createOkHttpClient(debug = AppUtil.debuggable(context)) {
cache(okHttpCache)
addInterceptor(DanDanPlayAuthInterceptor())
addNetworkInterceptor(OkHttpCacheInterceptor())
}
)
)

@Provides
@Singleton
fun provideDanmakuService(
danDanPlayDanmakuProvider: DanDanPlayDanmakuProvider,
) = DanmakuService().also {
if (BuildConfig.DANDANPLAY_APP_ID.isNotEmpty()
&& BuildConfig.DANDANPLAY_APP_SECRET.isNotEmpty()
) {
it.register(danDanPlayDanmakuProvider)
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.muedsa.tvbox.danmaku

import com.kuaishou.akdanmaku.data.DanmakuItemData
import com.muedsa.tvbox.model.DanmakuEpisode
import com.muedsa.tvbox.model.DanmakuMedia

interface DanmakuProvider {

val name: String

suspend fun searchMedia(keyword: String): List<DanmakuMedia>

suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia?

suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List<DanmakuItemData>
}
30 changes: 30 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/danmaku/DanmakuService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.muedsa.tvbox.danmaku

import com.kuaishou.akdanmaku.data.DanmakuItemData
import com.muedsa.tvbox.model.DanmakuEpisode
import com.muedsa.tvbox.model.DanmakuMedia

class DanmakuService {

private val providers: MutableMap<String, DanmakuProvider> = mutableMapOf()

fun register(provider: DanmakuProvider) {
providers[provider.name] = provider
}

fun getProviders(): Set<String> {
return providers.keys
}

suspend fun searchMedia(provider: String, keyword: String): List<DanmakuMedia> {
return providers[provider]?.searchMedia(keyword) ?: emptyList()
}

suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? {
return providers[media.provider]?.getMediaEpisodes(media)
}

suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List<DanmakuItemData> {
return providers[episode.provider]?.getEpisodeDanmakuList(episode) ?: emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.muedsa.tvbox.service
package com.muedsa.tvbox.danmaku.dandanplay

import com.muedsa.tvbox.model.dandanplay.BangumiDetailsResp
import com.muedsa.tvbox.model.dandanplay.BangumiSearch
import com.muedsa.tvbox.model.dandanplay.BangumiSearchResp
import com.muedsa.tvbox.model.dandanplay.EpisodeComments
import com.muedsa.util.OkHttpCacheInterceptor
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query

Expand All @@ -13,20 +15,23 @@ interface DanDanPlayApiService {
@GET("v2/search/anime")
suspend fun searchAnime(
@Query("keyword") keyword: String,
@Query("type") type: String = ""
@Query("type") type: String = "",
@Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=3600",
): BangumiSearchResp<BangumiSearch>

@GET("v2/bangumi/{animeId}")
suspend fun getAnime(
@Path("animeId") animeId: Int
@Path("animeId") animeId: Int,
@Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=21600",
): BangumiDetailsResp

@GET("v2/comment/{episodeId}")
suspend fun getComment(
@Path("episodeId") episodeId: Long,
@Query("from") from: Int = 0,
@Query("withRelated") withRelated: Boolean = false,
@Query("chConvert") chConvert: Int = 0
@Query("chConvert") chConvert: Int = 0,
@Header(OkHttpCacheInterceptor.HEADER) focusedCacheControl: String = "max-age=21600",
): EpisodeComments

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.muedsa.tvbox.service
package com.muedsa.tvbox.danmaku.dandanplay

import com.muedsa.tvbox.BuildConfig
import com.muedsa.tvbox.tool.encodeBase64
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.muedsa.tvbox.danmaku.dandanplay

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 timber.log.Timber

class DanDanPlayDanmakuProvider(
val danDanPlayApiService: DanDanPlayApiService,
) : DanmakuProvider {

override val name: String = "弹弹Play"

override suspend fun searchMedia(keyword: String): List<DanmakuMedia> {
val resp = danDanPlayApiService.searchAnime(keyword)
return if (resp.errorCode == 0) {
resp.animes?.map {
DanmakuMedia(
provider = name,
mediaId = it.animeId.toString(),
mediaName = it.animeTitle,
publishDate = it.startOnlyDate,
rating = it.rating.toString(),
episodes = emptyList(),
)
} ?: emptyList()
} else {
Timber.w("danDanPlayApiService.searchAnime(${keyword}) ${resp.errorMessage}")
emptyList()
}
}

override suspend fun getMediaEpisodes(media: DanmakuMedia): DanmakuMedia? {
val resp = danDanPlayApiService.getAnime(media.mediaId.toInt())
return if (resp.errorCode == 0) {
resp.bangumi?.let {
DanmakuMedia(
provider = name,
mediaId = it.animeId.toString(),
mediaName = it.animeTitle,
publishDate = media.publishDate,
rating = it.rating.toString(),
episodes = it.episodes.map { ep ->
DanmakuEpisode(
provider = name,
mediaId = it.animeId.toString(),
mediaName = it.animeTitle,
episodeId = ep.episodeId.toString(),
episodeName = ep.episodeTitle,
)
},
)
}
} else {
Timber.d("danDanPlayApiService.getAnime(${media.mediaId}) ${resp.errorMessage}")
null
}
}

override suspend fun getEpisodeDanmakuList(episode: DanmakuEpisode): List<DanmakuItemData> {
return danDanPlayApiService.getComment(
episodeId = episode.episodeId.toLong(),
from = 0,
withRelated = true,
chConvert = 1
).comments.map {
val propArr = it.p.split(",")
val pos = (propArr[0].toFloat() * 1000).toLong()
val mode = if (propArr[1] == "1")
DanmakuItemData.DANMAKU_MODE_ROLLING
else if (propArr[1] == "4")
DanmakuItemData.DANMAKU_MODE_CENTER_BOTTOM
else if (propArr[1] == "5")
DanmakuItemData.DANMAKU_MODE_CENTER_TOP
else
DanmakuItemData.DANMAKU_MODE_ROLLING
val colorInt = propArr[2].toInt()
DanmakuItemData(
danmakuId = it.cid,
position = pos,
content = it.m,
mode = mode,
textSize = 25,
textColor = colorInt,
score = 9,
danmakuStyle = DanmakuItemData.DANMAKU_STYLE_NONE
)
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/model/DanmakuEpisode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.muedsa.tvbox.model

import kotlinx.serialization.Serializable

@Serializable
class DanmakuEpisode(
val provider: String,
val mediaId: String,
val mediaName: String,
val episodeId: String,
val episodeName: String,
)
10 changes: 10 additions & 0 deletions app/src/main/java/com/muedsa/tvbox/model/DanmakuMedia.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.muedsa.tvbox.model

class DanmakuMedia(
val provider: String,
val mediaId: String,
val mediaName: String,
val publishDate: String? = null,
val rating: String? = null,
val episodes: List<DanmakuEpisode>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ sealed interface NavigationItems {
val pluginPackage: String,
val mediaId: String,
val episodeId: String,
val danEpisodeId: Long = -1,
val danmakuEpisodeJson: String? = null,
val disableEpisodeProgression: Boolean,
val enableCustomDanmakuList: Boolean,
val enableCustomDanmakuFlow: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,29 @@ import com.muedsa.compose.tv.widget.NoBackground
import com.muedsa.compose.tv.widget.TwoSideWideButton

@Composable
fun AnimeDanmakuSelectBtnWidget(
fun DanmakuMediaSelectorWidget(
enabledDanmakuState: MutableState<Boolean> = remember { mutableStateOf(false) },
mediaDetailScreenViewModel: MediaDetailScreenViewModel
mediaDetailScreenViewModel: MediaDetailScreenViewModel,
) {
val drawerController = useLocalRightSideDrawerController()

OutlinedIconButton(onClick = {
drawerController.pop {
DanmakuSelectorSideWidget(
DanmakuMediaSelectorSideWidget(
enabledDanmakuState = enabledDanmakuState,
mediaDetailScreenViewModel = mediaDetailScreenViewModel
)
}
}) {
Icon(
imageVector = Icons.Outlined.Edit,
contentDescription = "修改弹弹Play匹配剧集"
contentDescription = "修改弹幕匹配剧集"
)
}
}

@Composable
fun DanmakuSelectorSideWidget(
fun DanmakuMediaSelectorSideWidget(
enabledDanmakuState: MutableState<Boolean> = remember { mutableStateOf(false) },
mediaDetailScreenViewModel: MediaDetailScreenViewModel
) {
Expand All @@ -75,10 +75,10 @@ fun DanmakuSelectorSideWidget(
return
}
val mediaDetail = (ui as MediaDetailScreenUiState.Ready).mediaDetail
val danBangumiList = (ui as MediaDetailScreenUiState.Ready).danBangumiList ?: emptyList()
val danBangumiInfo = (ui as MediaDetailScreenUiState.Ready).danBangumiInfo
val danmakuMediaList = (ui as MediaDetailScreenUiState.Ready).danmakuMediaList ?: emptyList()
val danmakuMediaInfo = (ui as MediaDetailScreenUiState.Ready).danmakuMediaInfo

var searchTitle by remember { mutableStateOf(danBangumiInfo?.animeTitle ?: mediaDetail.title) }
var searchTitle by remember { mutableStateOf(danmakuMediaInfo?.mediaName ?: mediaDetail.title) }

val splitTitles = mediaDetail.title.split("\\s+".toRegex())

Expand Down Expand Up @@ -115,19 +115,19 @@ fun DanmakuSelectorSideWidget(
)
}
}
items(items = danBangumiList, key = { it.animeId }) {
items(items = danmakuMediaList, key = { it.mediaName }) {
val interactionSource = remember { MutableInteractionSource() }
TwoSideWideButton(
title = {
Text(
modifier = Modifier.basicMarquee(),
text = "${it.animeTitle} - ${it.typeDescription} - ${it.startOnlyDate}"
text = "${it.mediaName} - ${it.publishDate}"
)
},
onClick = {
drawerController.close()
enabledDanmakuState.value = true
mediaDetailScreenViewModel.changeDanBangumi(it)
mediaDetailScreenViewModel.changeDanmakuMedia(it)
},
interactionSource = interactionSource,
background = {
Expand All @@ -138,7 +138,7 @@ fun DanmakuSelectorSideWidget(
) {
RadioButton(
selected = enabledDanmakuState.value
&& danBangumiInfo?.animeId == it.animeId,
&& danmakuMediaInfo?.mediaId == it.mediaId,
onClick = { },
interactionSource = interactionSource
)
Expand Down Expand Up @@ -199,7 +199,7 @@ fun DanmakuSelectorSideWidget(
) {
Button(
onClick = {
mediaDetailScreenViewModel.searchDanBangumi(searchTitle)
mediaDetailScreenViewModel.searchDanmakuMedia(searchTitle)
isSelectorTab = true
}
) {
Expand All @@ -221,7 +221,7 @@ fun DanmakuSelectorSideWidget(
modifier = Modifier.padding(top = 10.dp),
onClick = {
searchTitle = it
mediaDetailScreenViewModel.searchDanBangumi(searchTitle)
mediaDetailScreenViewModel.searchDanmakuMedia(searchTitle)
isSelectorTab = true
},
title = {
Expand Down
Loading