From 023c497fa8c5871acd71a7cb2f2a998f2c0cace5 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:12:16 +0530 Subject: [PATCH] ref: plugin to use saved tmdb api key for tv show resolution --- .../player/PlayerStreamsRepository.kt | 15 ++--- .../app/features/plugins/PluginContentIds.kt | 24 ++++++++ .../app/features/streams/StreamsRepository.kt | 15 ++--- .../features/plugins/PluginContentIdsTest.kt | 55 +++++++++++++++++++ .../app/features/plugins/PluginRepository.kt | 21 ++++++- .../features/plugins/PluginsSettingsScreen.kt | 19 ++++++- 6 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/plugins/PluginContentIds.kt create mode 100644 composeApp/src/commonTest/kotlin/com/nuvio/app/features/plugins/PluginContentIdsTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt index e1a8e29f..78f55bdb 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerStreamsRepository.kt @@ -7,6 +7,7 @@ import com.nuvio.app.features.addons.buildAddonResourceUrl import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.details.MetaDetailsRepository 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.PluginScraper import com.nuvio.app.features.streams.AddonStreamGroup @@ -243,7 +244,11 @@ object PlayerStreamsRepository { async { PluginRepository.executeScraper( scraper = scraper, - tmdbId = videoId.toPluginTmdbId(), + tmdbId = pluginContentId( + videoId = videoId, + season = season, + episode = episode, + ), mediaType = type, season = season, 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 - } -} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/plugins/PluginContentIds.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/plugins/PluginContentIds.kt new file mode 100644 index 00000000..446794da --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/plugins/PluginContentIds.kt @@ -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 } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt index 61e04a2b..d9516513 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsRepository.kt @@ -8,6 +8,7 @@ import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.player.PlayerSettingsRepository 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.PluginRepositoryItem import com.nuvio.app.features.plugins.PluginRuntimeResult @@ -285,7 +286,11 @@ object StreamsRepository { launch { val completion = PluginRepository.executeScraper( scraper = scraper, - tmdbId = videoId.toPluginTmdbId(), + tmdbId = pluginContentId( + videoId = videoId, + season = season, + episode = episode, + ), mediaType = type, season = season, episode = episode, @@ -486,14 +491,6 @@ private fun List.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( scraper: PluginScraper, addonName: String = scraper.name, diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/plugins/PluginContentIdsTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/plugins/PluginContentIdsTest.kt new file mode 100644 index 00000000..7b852d18 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/plugins/PluginContentIdsTest.kt @@ -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, + ), + ) + } +} diff --git a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt index aa721abd..32e0562f 100644 --- a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt +++ b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginRepository.kt @@ -4,6 +4,7 @@ import co.touchlab.kermit.Logger import com.nuvio.app.core.network.SupabaseProvider import com.nuvio.app.features.addons.httpGetText 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.query.Order import io.github.jan.supabase.postgrest.rpc @@ -314,10 +315,15 @@ actual object PluginRepository { season: Int?, episode: Int?, ): Result> { + val resolvedTmdbId = resolvePluginTmdbId( + tmdbId = tmdbId, + mediaType = mediaType, + ) + return runCatching { PluginRuntime.executePlugin( code = scraper.code, - tmdbId = tmdbId, + tmdbId = resolvedTmdbId, mediaType = normalizePluginType(mediaType), season = season, 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( manifestUrl: String, previousScrapers: Map, diff --git a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginsSettingsScreen.kt b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginsSettingsScreen.kt index 4838ab51..71b7e4e3 100644 --- a/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginsSettingsScreen.kt +++ b/composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/PluginsSettingsScreen.kt @@ -38,6 +38,7 @@ import com.nuvio.app.core.ui.NuvioInputField import com.nuvio.app.core.ui.NuvioPrimaryButton import com.nuvio.app.core.ui.NuvioSectionLabel import com.nuvio.app.core.ui.NuvioSurfaceCard +import com.nuvio.app.features.tmdb.TmdbSettingsRepository import kotlinx.coroutines.launch @Composable @@ -49,6 +50,10 @@ fun PluginsSettingsPageContent( } val uiState by PluginRepository.uiState.collectAsStateWithLifecycle() + val tmdbSettings by remember { + TmdbSettingsRepository.ensureLoaded() + TmdbSettingsRepository.uiState + }.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() var repositoryUrl by rememberSaveable { mutableStateOf("") } @@ -61,6 +66,7 @@ fun PluginsSettingsPageContent( val sortedRepos = remember(uiState.repositories) { uiState.repositories.sortedBy { it.name.lowercase() } } + val hasTmdbApiKey = tmdbSettings.hasApiKey val repositoryNameByUrl = remember(sortedRepos) { sortedRepos.associate { it.manifestUrl to it.name } } @@ -88,6 +94,17 @@ fun PluginsSettingsPageContent( NuvioInfoBadge( 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)) Row( @@ -355,7 +372,7 @@ fun PluginsSettingsPageContent( Spacer(modifier = Modifier.height(12.dp)) NuvioPrimaryButton( text = if (isTestingThisScraper) "Testing..." else "Test Provider", - enabled = !isTestingThisScraper, + enabled = hasTmdbApiKey && !isTestingThisScraper, onClick = { testingScraperId = scraper.id coroutineScope.launch {