ref: plugin to use saved tmdb api key for tv show resolution

This commit is contained in:
tapframe 2026-04-19 16:12:16 +05:30
parent 9a0acf7149
commit 023c497fa8
6 changed files with 129 additions and 20 deletions

View file

@ -7,6 +7,7 @@ import com.nuvio.app.features.addons.buildAddonResourceUrl
import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.plugins.PluginRepository import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.plugins.pluginContentId
import com.nuvio.app.features.plugins.PluginRuntimeResult import com.nuvio.app.features.plugins.PluginRuntimeResult
import com.nuvio.app.features.plugins.PluginScraper import com.nuvio.app.features.plugins.PluginScraper
import com.nuvio.app.features.streams.AddonStreamGroup import com.nuvio.app.features.streams.AddonStreamGroup
@ -243,7 +244,11 @@ object PlayerStreamsRepository {
async { async {
PluginRepository.executeScraper( PluginRepository.executeScraper(
scraper = scraper, scraper = scraper,
tmdbId = videoId.toPluginTmdbId(), tmdbId = pluginContentId(
videoId = videoId,
season = season,
episode = episode,
),
mediaType = type, mediaType = type,
season = season, season = season,
episode = episode, episode = episode,
@ -339,11 +344,3 @@ private fun PluginRuntimeResult.toStreamItem(scraper: PluginScraper): StreamItem
}, },
) )
} }
private fun String.toPluginTmdbId(): String {
return when {
startsWith("tmdb:") -> removePrefix("tmdb:").substringBefore(":").ifBlank { this }
startsWith("tmdb/") -> removePrefix("tmdb/").substringBefore('/').ifBlank { this }
else -> this
}
}

View file

@ -0,0 +1,24 @@
package com.nuvio.app.features.plugins
internal fun pluginContentId(
videoId: String,
season: Int?,
episode: Int?,
): String {
val trimmed = videoId.trim()
if (trimmed.isBlank()) return videoId
val withoutPrefix = when {
trimmed.startsWith("tmdb:") -> trimmed.removePrefix("tmdb:")
trimmed.startsWith("tmdb/") -> trimmed.removePrefix("tmdb/")
else -> trimmed
}
val withoutEpisodeSuffix = if (season != null && episode != null) {
withoutPrefix.removeSuffix(":$season:$episode")
} else {
withoutPrefix
}
return withoutEpisodeSuffix.substringBefore('/').ifBlank { trimmed }
}

View file

@ -8,6 +8,7 @@ import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.plugins.PluginRepository import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.plugins.pluginContentId
import com.nuvio.app.features.plugins.PluginsUiState import com.nuvio.app.features.plugins.PluginsUiState
import com.nuvio.app.features.plugins.PluginRepositoryItem import com.nuvio.app.features.plugins.PluginRepositoryItem
import com.nuvio.app.features.plugins.PluginRuntimeResult import com.nuvio.app.features.plugins.PluginRuntimeResult
@ -285,7 +286,11 @@ object StreamsRepository {
launch { launch {
val completion = PluginRepository.executeScraper( val completion = PluginRepository.executeScraper(
scraper = scraper, scraper = scraper,
tmdbId = videoId.toPluginTmdbId(), tmdbId = pluginContentId(
videoId = videoId,
season = season,
episode = episode,
),
mediaType = type, mediaType = type,
season = season, season = season,
episode = episode, episode = episode,
@ -486,14 +491,6 @@ private fun List<AddonStreamGroup>.toEmptyStateReason(anyLoading: Boolean): Stre
} }
} }
private fun String.toPluginTmdbId(): String {
return when {
startsWith("tmdb:") -> removePrefix("tmdb:").substringBefore(":").ifBlank { this }
startsWith("tmdb/") -> removePrefix("tmdb/").substringBefore('/').ifBlank { this }
else -> this
}
}
private fun PluginRuntimeResult.toStreamItem( private fun PluginRuntimeResult.toStreamItem(
scraper: PluginScraper, scraper: PluginScraper,
addonName: String = scraper.name, addonName: String = scraper.name,

View file

@ -0,0 +1,55 @@
package com.nuvio.app.features.plugins
import kotlin.test.Test
import kotlin.test.assertEquals
class PluginContentIdsTest {
@Test
fun `series playback id strips season episode suffix`() {
assertEquals(
"tt2575988",
pluginContentId(
videoId = "tt2575988:5:8",
season = 5,
episode = 8,
),
)
}
@Test
fun `tmdb prefixed series playback id strips prefix and suffix`() {
assertEquals(
"12345",
pluginContentId(
videoId = "tmdb:12345:2:6",
season = 2,
episode = 6,
),
)
}
@Test
fun `movie id stays unchanged`() {
assertEquals(
"tt0133093",
pluginContentId(
videoId = "tt0133093",
season = null,
episode = null,
),
)
}
@Test
fun `slash prefixed tmdb id keeps base content id`() {
assertEquals(
"999",
pluginContentId(
videoId = "tmdb/999/1/2",
season = 1,
episode = 2,
),
)
}
}

View file

@ -4,6 +4,7 @@ import co.touchlab.kermit.Logger
import com.nuvio.app.core.network.SupabaseProvider import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.profiles.ProfileRepository import com.nuvio.app.features.profiles.ProfileRepository
import com.nuvio.app.features.tmdb.TmdbService
import io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.query.Order import io.github.jan.supabase.postgrest.query.Order
import io.github.jan.supabase.postgrest.rpc import io.github.jan.supabase.postgrest.rpc
@ -314,10 +315,15 @@ actual object PluginRepository {
season: Int?, season: Int?,
episode: Int?, episode: Int?,
): Result<List<PluginRuntimeResult>> { ): Result<List<PluginRuntimeResult>> {
val resolvedTmdbId = resolvePluginTmdbId(
tmdbId = tmdbId,
mediaType = mediaType,
)
return runCatching { return runCatching {
PluginRuntime.executePlugin( PluginRuntime.executePlugin(
code = scraper.code, code = scraper.code,
tmdbId = tmdbId, tmdbId = resolvedTmdbId,
mediaType = normalizePluginType(mediaType), mediaType = normalizePluginType(mediaType),
season = season, season = season,
episode = episode, episode = episode,
@ -327,6 +333,19 @@ actual object PluginRepository {
} }
} }
private suspend fun resolvePluginTmdbId(
tmdbId: String,
mediaType: String,
): String {
val trimmed = tmdbId.trim()
if (trimmed.isBlank()) return tmdbId
return TmdbService.ensureTmdbId(
videoId = trimmed,
mediaType = mediaType,
) ?: trimmed
}
private suspend fun fetchRepositoryData( private suspend fun fetchRepositoryData(
manifestUrl: String, manifestUrl: String,
previousScrapers: Map<String, PluginScraper>, previousScrapers: Map<String, PluginScraper>,

View file

@ -38,6 +38,7 @@ import com.nuvio.app.core.ui.NuvioInputField
import com.nuvio.app.core.ui.NuvioPrimaryButton import com.nuvio.app.core.ui.NuvioPrimaryButton
import com.nuvio.app.core.ui.NuvioSectionLabel import com.nuvio.app.core.ui.NuvioSectionLabel
import com.nuvio.app.core.ui.NuvioSurfaceCard import com.nuvio.app.core.ui.NuvioSurfaceCard
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -49,6 +50,10 @@ fun PluginsSettingsPageContent(
} }
val uiState by PluginRepository.uiState.collectAsStateWithLifecycle() val uiState by PluginRepository.uiState.collectAsStateWithLifecycle()
val tmdbSettings by remember {
TmdbSettingsRepository.ensureLoaded()
TmdbSettingsRepository.uiState
}.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var repositoryUrl by rememberSaveable { mutableStateOf("") } var repositoryUrl by rememberSaveable { mutableStateOf("") }
@ -61,6 +66,7 @@ fun PluginsSettingsPageContent(
val sortedRepos = remember(uiState.repositories) { val sortedRepos = remember(uiState.repositories) {
uiState.repositories.sortedBy { it.name.lowercase() } uiState.repositories.sortedBy { it.name.lowercase() }
} }
val hasTmdbApiKey = tmdbSettings.hasApiKey
val repositoryNameByUrl = remember(sortedRepos) { val repositoryNameByUrl = remember(sortedRepos) {
sortedRepos.associate { it.manifestUrl to it.name } sortedRepos.associate { it.manifestUrl to it.name }
} }
@ -88,6 +94,17 @@ fun PluginsSettingsPageContent(
NuvioInfoBadge( NuvioInfoBadge(
text = if (uiState.pluginsEnabled) "Plugins enabled" else "Plugins disabled", text = if (uiState.pluginsEnabled) "Plugins enabled" else "Plugins disabled",
) )
NuvioInfoBadge(
text = if (hasTmdbApiKey) "TMDB API key set" else "TMDB API key missing",
)
}
if (!hasTmdbApiKey) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Plugin providers require a TMDB API key. Set it on the TMDB screen or plugin providers will not work correctly.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Row( Row(
@ -355,7 +372,7 @@ fun PluginsSettingsPageContent(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NuvioPrimaryButton( NuvioPrimaryButton(
text = if (isTestingThisScraper) "Testing..." else "Test Provider", text = if (isTestingThisScraper) "Testing..." else "Test Provider",
enabled = !isTestingThisScraper, enabled = hasTmdbApiKey && !isTestingThisScraper,
onClick = { onClick = {
testingScraperId = scraper.id testingScraperId = scraper.id
coroutineScope.launch { coroutineScope.launch {