diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/cloud/CloudLibraryRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/cloud/CloudLibraryRepository.kt index d5f7669d..492b70a3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/cloud/CloudLibraryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/cloud/CloudLibraryRepository.kt @@ -64,6 +64,13 @@ internal class CloudLibraryStore( ?: return CloudLibraryPlaybackResult.MissingCredentials val api = providerApis.firstOrNull { it.provider.id == item.providerId } ?: return CloudLibraryPlaybackResult.Failed() + file.playbackUrl?.takeIf { it.isNotBlank() }?.let { url -> + return CloudLibraryPlaybackResult.Success( + url = url, + filename = file.name.takeIf { it.isNotBlank() }, + videoSizeBytes = file.sizeBytes, + ) + } return api.resolvePlayback( apiKey = credential.apiKey, item = item, @@ -186,7 +193,26 @@ object CloudLibraryRepository { if (!DebridSettingsRepository.snapshot().cloudLibraryEnabled) { return CloudLibraryPlaybackResult.Failed("Cloud library is disabled.") } - return store.resolvePlayback(item, file) + val result = store.resolvePlayback(item, file) + if (result is CloudLibraryPlaybackResult.Success) { + rememberResolvedPlaybackUrl(item = item, file = file, url = result.url) + } + return result + } + + private fun rememberResolvedPlaybackUrl( + item: CloudLibraryItem, + file: CloudLibraryFile, + url: String, + ) { + if (url.isBlank()) return + _uiState.update { current -> + current.withResolvedPlaybackUrl( + item = item, + file = file, + url = url, + ) + } } private fun connectedCloudCredentials(): List = @@ -248,3 +274,35 @@ internal fun CloudLibraryUiState.findPlaybackTargetForProgress( val singleFile = singleItem.playableFiles.singleOrNull() ?: return null return CloudLibraryPlaybackTarget(item = singleItem, file = singleFile) } + +internal fun CloudLibraryUiState.withResolvedPlaybackUrl( + item: CloudLibraryItem, + file: CloudLibraryFile, + url: String, +): CloudLibraryUiState { + val normalizedUrl = url.trim().takeIf { it.isNotBlank() } ?: return this + val targetItemKey = item.stableKey + val targetFileKey = file.stableKey + var didUpdate = false + val updatedProviders = providers.map { providerState -> + if (providerState.providerId != item.providerId) return@map providerState + val updatedItems = providerState.items.map { candidateItem -> + if (candidateItem.stableKey != targetItemKey) return@map candidateItem + val updatedFiles = candidateItem.files.map { candidateFile -> + if (candidateFile.stableKey != targetFileKey) { + candidateFile + } else { + didUpdate = true + candidateFile.copy(playbackUrl = normalizedUrl) + } + } + candidateItem.copy(files = updatedFiles) + } + providerState.copy(items = updatedItems) + } + return if (didUpdate) { + copy(providers = updatedProviders) + } else { + this + } +} diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/cloud/CloudLibraryStoreTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/cloud/CloudLibraryStoreTest.kt index 5610846a..25f25e39 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/cloud/CloudLibraryStoreTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/cloud/CloudLibraryStoreTest.kt @@ -130,6 +130,59 @@ class CloudLibraryStoreTest { assertEquals(item.playableFiles.single(), target.file) } + @Test + fun `resolve playback reuses already resolved file url`() = runBlocking { + val provider = cloudProvider(id = "premiumize", name = "Premiumize") + val api = FakeCloudProviderApi( + provider = provider, + items = emptyList(), + ) + val store = CloudLibraryStore( + credentialsProvider = { + listOf(DebridServiceCredential(provider, "token")) + }, + providerApis = listOf(api), + ) + val item = cloudItem(provider, "ready") + val file = item.playableFiles.single().copy(playbackUrl = "https://cached.example/video.mkv") + + val result = store.resolvePlayback(item = item, file = file) + + assertTrue(result is CloudLibraryPlaybackResult.Success) + assertEquals("https://cached.example/video.mkv", result.url) + assertEquals(0, api.resolvePlaybackCalls) + } + + @Test + fun `resolved playback url is remembered in cloud library state`() { + val provider = cloudProvider(id = "torbox", name = "TorBox") + val item = cloudItem(provider, "29773238") + val file = item.playableFiles.single() + val state = CloudLibraryUiState( + isLoaded = true, + providers = listOf( + CloudLibraryProviderState( + provider = provider, + items = listOf(item), + ), + ), + ) + + val updated = state.withResolvedPlaybackUrl( + item = item, + file = file, + url = "https://resolved.example/movie.mkv", + ) + + val target = assertNotNull( + updated.findPlaybackTargetForProgress( + contentId = item.stableKey, + videoId = item.playbackVideoId(file), + ), + ) + assertEquals("https://resolved.example/movie.mkv", target.file.playbackUrl) + } + @Test fun `provider poster urls are mapped for cloud services`() { assertEquals( @@ -155,6 +208,9 @@ private class FakeCloudProviderApi( override val provider: DebridProvider, private val items: List, ) : CloudLibraryProviderApi { + var resolvePlaybackCalls: Int = 0 + private set + override suspend fun listItems(apiKey: String): Result> = Result.success(items) @@ -162,8 +218,10 @@ private class FakeCloudProviderApi( apiKey: String, item: CloudLibraryItem, file: CloudLibraryFile, - ): CloudLibraryPlaybackResult = - CloudLibraryPlaybackResult.Success(url = "https://example.test/${item.id}/${file.id}") + ): CloudLibraryPlaybackResult { + resolvePlaybackCalls += 1 + return CloudLibraryPlaybackResult.Success(url = "https://example.test/${item.id}/${file.id}") + } } private fun cloudProvider(id: String, name: String): DebridProvider =