mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-08 16:07:31 +00:00
feat: add MDBList detail ratings
This commit is contained in:
parent
32243b6363
commit
599e82e7ff
13 changed files with 127 additions and 2 deletions
|
|
@ -37,6 +37,7 @@ import com.fluxa.app.player.TorrentStreamManager
|
|||
import com.fluxa.app.player.TorrentStreamResult
|
||||
import com.fluxa.app.data.repository.CloudStreamCatalogClient
|
||||
import com.fluxa.app.data.repository.HttpEffectExecutor
|
||||
import com.fluxa.app.data.repository.MdblistRatingsClient
|
||||
import com.fluxa.app.data.repository.toStremioType
|
||||
import com.fluxa.app.plugins.PluginManager
|
||||
import com.fluxa.app.plugins.PluginRepositoryManager
|
||||
|
|
@ -116,6 +117,7 @@ class FluxaAndroidHeadlessEnvironment @Inject constructor(
|
|||
internal val profileManager: ProfileManager,
|
||||
internal val externalSyncPushCoordinator: ExternalSyncPushCoordinator,
|
||||
internal val nuvioAccountImportCoordinator: NuvioAccountImportCoordinator,
|
||||
internal val mdblistRatingsClient: MdblistRatingsClient,
|
||||
internal val httpEffectExecutor: HttpEffectExecutor,
|
||||
@param:Named("PluginScraperClient") internal val pluginScraperHttpClient: OkHttpClient
|
||||
) : HeadlessPlatformEnvironment {
|
||||
|
|
@ -294,7 +296,19 @@ class FluxaAndroidHeadlessEnvironment @Inject constructor(
|
|||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return ok(effect, mapOf("watchedVideoIds" to watchedIds.toList(), "similarItems" to similarItems, "trailers" to trailers))
|
||||
val mdblistRatings = profile?.mdblistApiKey
|
||||
?.takeIf(String::isNotBlank)
|
||||
?.let { mdblistRatingsClient.fetch(type, id, it) }
|
||||
.orEmpty()
|
||||
return ok(
|
||||
effect,
|
||||
mapOf(
|
||||
"watchedVideoIds" to watchedIds.toList(),
|
||||
"similarItems" to similarItems,
|
||||
"trailers" to trailers,
|
||||
"mdblistRatings" to mdblistRatings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun prefetchDetailStreams(effect: NativeHeadlessEffect): HeadlessEffectCompletion {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.fluxa.app.data.remote.Video
|
|||
import com.fluxa.app.shared.feature.detail.DetailDataSource
|
||||
import com.fluxa.app.shared.feature.detail.DetailEpisodeUiModel
|
||||
import com.fluxa.app.shared.feature.detail.DetailRequestUiModel
|
||||
import com.fluxa.app.shared.feature.detail.DetailRatingUiModel
|
||||
import com.fluxa.app.shared.feature.detail.DetailStreamUiModel
|
||||
import com.fluxa.app.shared.feature.detail.DetailUiModel
|
||||
import com.fluxa.app.shared.feature.detail.DetailUiState as SharedDetailUiState
|
||||
|
|
@ -55,6 +56,9 @@ class AndroidDetailDataSource(
|
|||
trailerUrl = state.trailerUrl,
|
||||
releaseLabel = detail.releaseInfo.orEmpty(),
|
||||
ratingLabel = detail.imdbRating.orEmpty(),
|
||||
ratings = detail.ratings.orEmpty().mapNotNull { rating ->
|
||||
rating.value?.takeIf(String::isNotBlank)?.let { DetailRatingUiModel(rating.source, it) }
|
||||
},
|
||||
runtimeLabel = detail.runtime,
|
||||
ageRating = detail.ageRating,
|
||||
castNames = detail.cast.orEmpty().mapNotNull { it.name.takeIf { name -> name.isNotBlank() } },
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ class AndroidSettingsDataSource(
|
|||
traktLibraryCount = profile.safeTraktLastWatchlistCount,
|
||||
addonCount = profile.safeLocalAddons.size,
|
||||
tmdbApiKey = profile.tmdbApiKey,
|
||||
mdblistApiKey = profile.mdblistApiKey,
|
||||
tmdbCastImagesEnabled = profile.safeTmdbCastImagesEnabled,
|
||||
tmdbSimilarResultsEnabled = profile.safeTmdbSimilarResultsEnabled,
|
||||
tmdbTrailersEnabled = profile.safeTmdbTrailersEnabled,
|
||||
|
|
@ -408,6 +409,7 @@ class AndroidSettingsDataSource(
|
|||
override suspend fun updateTmdbAccount(value: SettingsAccountUiModel) = update {
|
||||
it.copy(
|
||||
tmdbApiKey = value.tmdbApiKey,
|
||||
mdblistApiKey = value.mdblistApiKey,
|
||||
tmdbCastImagesEnabled = value.tmdbCastImagesEnabled,
|
||||
tmdbSimilarResultsEnabled = value.tmdbSimilarResultsEnabled,
|
||||
tmdbTrailersEnabled = value.tmdbTrailersEnabled,
|
||||
|
|
|
|||
|
|
@ -158,10 +158,15 @@ class DetailViewModel @Inject constructor(
|
|||
).state["detail"] as? Map<*, *>
|
||||
if (generation != detailLoadGeneration) return@launch
|
||||
_uiState.update {
|
||||
val mdblistRatings = gson.fromStateList<MetaRating>(secondary?.get("mdblistRatings"))
|
||||
it.copy(
|
||||
watchedVideoIds = gson.fromStateList(secondary?.get("watchedVideoIds")),
|
||||
similarItems = gson.fromStateList(secondary?.get("similarItems")),
|
||||
trailers = if (it.trailers.isEmpty()) gson.fromStateList(secondary?.get("trailers")) else it.trailers
|
||||
trailers = if (it.trailers.isEmpty()) gson.fromStateList(secondary?.get("trailers")) else it.trailers,
|
||||
detail = it.detail?.copy(
|
||||
ratings = (it.detail.ratings.orEmpty() + mdblistRatings)
|
||||
.distinctBy { rating -> rating.source.lowercase() }
|
||||
)
|
||||
)
|
||||
}
|
||||
maybeAutoPlayDetailTrailer(generation, profile)
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,9 @@
|
|||
"settings.apis": "APIS",
|
||||
"settings.apis_desc": "Configure metadata API access.",
|
||||
"settings.tmdb_api": "TMDB API",
|
||||
"settings.mdblist_api": "MDBList API",
|
||||
"settings.mdblist_api_key": "MDBList API key",
|
||||
"settings.mdblist_api_key_placeholder": "Enter API key",
|
||||
"settings.tmdb_api_configured": "API key saved",
|
||||
"settings.tmdb_api_not_configured": "Add API key",
|
||||
"settings.tmdb_api_key": "TMDB API key",
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,9 @@
|
|||
"settings.apis": "API'LER",
|
||||
"settings.apis_desc": "Metadata API erişimini yapılandır.",
|
||||
"settings.tmdb_api": "TMDB API",
|
||||
"settings.mdblist_api": "MDBList API",
|
||||
"settings.mdblist_api_key": "MDBList API anahtarı",
|
||||
"settings.mdblist_api_key_placeholder": "API anahtarını girin",
|
||||
"settings.tmdb_api_configured": "API key kaydedildi",
|
||||
"settings.tmdb_api_not_configured": "API key ekle",
|
||||
"settings.tmdb_api_key": "TMDB API key",
|
||||
|
|
|
|||
|
|
@ -1368,6 +1368,22 @@ object FluxaCoreNative {
|
|||
return FluxaCoreUniFfi.coreInvokeValue("scrobbleCloseAction", args.toString()).asString
|
||||
}
|
||||
|
||||
fun mdblistMediaInfoUrl(provider: String, mediaType: String, mediaId: String): String {
|
||||
val args = JsonObject().apply {
|
||||
addProperty("provider", provider)
|
||||
addProperty("mediaType", mediaType)
|
||||
addProperty("mediaId", mediaId)
|
||||
addProperty("appendToResponse", "ratings")
|
||||
}
|
||||
return FluxaCoreUniFfi.coreInvokeValue("mdblistMediaInfoUrl", args.toString()).asString
|
||||
}
|
||||
|
||||
fun mdblistMediaRatingsFromResponse(responseJson: String): Map<String, String> {
|
||||
val value = FluxaCoreUniFfi.coreInvokeValue("mdblistMediaRatingsFromResponse", responseJson)
|
||||
if (value.isJsonNull) return emptyMap()
|
||||
return gson.fromJson(value, object : com.google.gson.reflect.TypeToken<Map<String, String>>() {}.type)
|
||||
}
|
||||
|
||||
fun subtitleLanguageDedupKeepIndices(languages: List<String?>, maxPerLanguage: Int = 2): List<Int> {
|
||||
val args = JsonObject().apply {
|
||||
add("languages", gson.toJsonTree(languages))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.fluxa.app.data.repository
|
||||
|
||||
import com.fluxa.app.core.rust.FluxaCoreNative
|
||||
import com.fluxa.app.data.remote.MetaRating
|
||||
import com.fluxa.app.data.remote.StremioService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MdblistRatingsClient @Inject constructor() {
|
||||
suspend fun fetch(contentType: String, contentId: String, apiKey: String): List<MetaRating> = withContext(Dispatchers.IO) {
|
||||
if (apiKey.isBlank()) return@withContext emptyList()
|
||||
val tmdbId = contentId.removePrefix("tmdb:").substringBefore(':')
|
||||
val imdbId = contentId.substringBefore(':')
|
||||
val providerAndId = when {
|
||||
tmdbId.all(Char::isDigit) && tmdbId.isNotBlank() -> "tmdb" to tmdbId
|
||||
imdbId.matches(Regex("(?i)tt\\d+")) -> "imdb" to imdbId
|
||||
else -> return@withContext emptyList()
|
||||
}
|
||||
runCatching {
|
||||
val url = FluxaCoreNative.mdblistMediaInfoUrl(
|
||||
provider = providerAndId.first,
|
||||
mediaType = if (contentType == "series") "show" else "movie",
|
||||
mediaId = providerAndId.second
|
||||
)
|
||||
val request = Request.Builder().url("$url&apikey=$apiKey").get().build()
|
||||
StremioService.sharedClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) return@use emptyList()
|
||||
val body = response.body.string()
|
||||
FluxaCoreNative.mdblistMediaRatingsFromResponse(body)
|
||||
.map { (source, value) -> MetaRating(source, value) }
|
||||
}
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ data class UserProfile(
|
|||
val anilistRefreshToken: String? = null,
|
||||
val anilistTokenExpiresAt: Long? = null,
|
||||
val tmdbApiKey: String? = null,
|
||||
val mdblistApiKey: String? = null,
|
||||
val introDbApiKey: String? = null,
|
||||
val tmdbCastImagesEnabled: Boolean? = true,
|
||||
val tmdbSimilarResultsEnabled: Boolean? = true,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ data class DetailStreamUiModel(
|
|||
val name: String = ""
|
||||
)
|
||||
|
||||
data class DetailRatingUiModel(
|
||||
val source: String,
|
||||
val value: String
|
||||
)
|
||||
|
||||
data class DetailUiModel(
|
||||
val id: String,
|
||||
val type: String,
|
||||
|
|
@ -51,6 +56,7 @@ data class DetailUiModel(
|
|||
val trailerUrl: String? = null,
|
||||
val releaseLabel: String,
|
||||
val ratingLabel: String,
|
||||
val ratings: List<DetailRatingUiModel> = emptyList(),
|
||||
val runtimeLabel: String?,
|
||||
val ageRating: String? = null,
|
||||
val castNames: List<String> = emptyList(),
|
||||
|
|
|
|||
|
|
@ -417,6 +417,27 @@ private fun Hero(content: DetailUiModel, language: String?) {
|
|||
}
|
||||
}
|
||||
}
|
||||
val ratings = content.ratings.ifEmpty {
|
||||
content.ratingLabel.takeIf { it.isNotBlank() }?.let { listOf(DetailRatingUiModel("IMDb", it)) }.orEmpty()
|
||||
}
|
||||
if (ratings.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(ratings, key = { "${it.source}:${it.value}" }) { rating ->
|
||||
Text(
|
||||
text = "${rating.source} ${rating.value}",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier
|
||||
.background(Color.White.copy(alpha = 0.14f), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ data class SettingsAccountUiModel(
|
|||
val traktLibraryCount: Int = 0,
|
||||
val addonCount: Int = 0,
|
||||
val tmdbApiKey: String? = null,
|
||||
val mdblistApiKey: String? = null,
|
||||
val tmdbCastImagesEnabled: Boolean = true,
|
||||
val tmdbSimilarResultsEnabled: Boolean = true,
|
||||
val tmdbTrailersEnabled: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -507,6 +507,17 @@ private fun SettingsAccountContent(
|
|||
}
|
||||
}
|
||||
|
||||
SettingsSectionHeader(AppStrings.t(lang, "settings.mdblist_api"))
|
||||
SettingsGroupCard {
|
||||
SettingsSecretFieldRow(
|
||||
AppStrings.t(lang, "settings.mdblist_api_key"),
|
||||
model.mdblistApiKey.orEmpty(),
|
||||
placeholder = AppStrings.t(lang, "settings.mdblist_api_key_placeholder")
|
||||
) {
|
||||
onAction(SettingsAction.TmdbAccountChanged(model.copy(mdblistApiKey = it)))
|
||||
}
|
||||
}
|
||||
|
||||
selectedProvider?.let { provider ->
|
||||
key(provider) {
|
||||
SettingsAccountSheet(provider, model, lang, onDismiss = { selectedProvider = null }) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue