diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index c9a13b78..e1d1ca30 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -2325,8 +2325,10 @@ private fun MainAppContent( ) if (!forceInternal && (forceExternal || playerSettings.externalPlayerEnabled)) { - coroutineScope.launch { openExternalPlayback(playerLaunch) } - StreamsRepository.cancelLoading() + streamRouteScope.launch { + openExternalPlayback(playerLaunch) + StreamsRepository.cancelLoading() + } return } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ExternalPlayerLaunchCoordinator.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ExternalPlayerLaunchCoordinator.kt index ea9b3e4e..4fe5ff08 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ExternalPlayerLaunchCoordinator.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ExternalPlayerLaunchCoordinator.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.addJsonObject import kotlinx.serialization.json.buildJsonArray @@ -33,35 +34,47 @@ suspend fun prepareExternalPlayerLaunch( preferredLanguage: String, secondaryLanguage: String?, onOverlayMessage: (String?) -> Unit, -): ExternalPlayerPlaybackRequest { +): ExternalPlayerPlaybackRequest = coroutineScope { var result = request - if (forwardSubtitles && !preferredLanguage.equals(SubtitleLanguageOption.NONE, ignoreCase = true)) { - onOverlayMessage("Loading subtitles from addons...") + val subtitlesDeferred = if (forwardSubtitles && !preferredLanguage.equals(SubtitleLanguageOption.NONE, ignoreCase = true)) { + async { + onOverlayMessage("Loading subtitles from addons...") - val subtitles = SubtitleForwarder.fetchForExternalPlayer( - type = type, - videoId = videoId, - preferredLanguage = preferredLanguage, - secondaryLanguage = secondaryLanguage, - ) + val subtitles = SubtitleForwarder.fetchForExternalPlayer( + type = type, + videoId = videoId, + preferredLanguage = preferredLanguage, + secondaryLanguage = secondaryLanguage, + ) - if (subtitles != null) { - onOverlayMessage("Downloading subtitles...") - val cachedSubtitles = SubtitleCacheProvider.cacheForExternalPlayer(subtitles) - // Fallback: use original URLs if caching fails - result = result.copy(subtitles = cachedSubtitles ?: subtitles) + if (subtitles != null) { + onOverlayMessage("Downloading subtitles...") + val cachedSubtitles = SubtitleCacheProvider.cacheForExternalPlayer(subtitles) + // Fallback: use original URLs if caching fails + cachedSubtitles ?: subtitles + } else { + null + } } + } else { + null } - if (sendSkipSegments) { - val skipSegmentsJson = resolveSkipSegmentsJson(videoId, request.season, request.episode) - if (skipSegmentsJson != null) { - result = result.copy(skipSegmentsJson = skipSegmentsJson) - } + val skipSegmentsDeferred = if (sendSkipSegments) { + async { resolveSkipSegmentsJson(videoId, request.season, request.episode) } + } else { + null } - return result + subtitlesDeferred?.await()?.let { subtitles -> + result = result.copy(subtitles = subtitles) + } + skipSegmentsDeferred?.await()?.let { skipSegmentsJson -> + result = result.copy(skipSegmentsJson = skipSegmentsJson) + } + + return@coroutineScope result } /** diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt index 39a53296..d99dc978 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt @@ -1,6 +1,8 @@ package com.nuvio.app.features.player.skip import com.nuvio.app.features.player.PlayerSettingsRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope object SkipIntroRepository { @@ -17,48 +19,64 @@ object SkipIntroRepository { season: Int, episode: Int, requireSkipIntroEnabled: Boolean = true, - ): List { - if (imdbId == null) return emptyList() + ): List = coroutineScope { + if (imdbId == null) return@coroutineScope emptyList() val settings = PlayerSettingsRepository.uiState.value - if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList() + if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return@coroutineScope emptyList() val cacheKey = "$imdbId:$season:$episode" - cache[cacheKey]?.let { return it } + cache[cacheKey]?.let { return@coroutineScope it } - val introDb = if (introDbConfigured) fetchFromIntroDb(imdbId, season, episode) else emptyList() - val entries = resolveImdbEntries(imdbId) - val animeSkip = fetchAnimeSkipForEntries(entries, season, episode) + val introDbDeferred = async { + if (introDbConfigured) fetchFromIntroDb(imdbId, season, episode) else emptyList() + } + val entriesDeferred = async { resolveImdbEntries(imdbId) } + val entries = entriesDeferred.await() + val animeSkipDeferred = async { fetchAnimeSkipForEntries(entries, season, episode) } val malId = entries.getOrNull(season - 1)?.myanimelist?.toString() ?: entries.firstOrNull()?.myanimelist?.toString() - val aniSkip = if (malId != null) fetchFromAniSkip(malId, episode) else emptyList() + val aniSkipDeferred = async { + if (malId != null) fetchFromAniSkip(malId, episode) else emptyList() + } - return mergeByPriority(introDb, animeSkip, aniSkip).also { cache[cacheKey] = it } + return@coroutineScope mergeByPriority( + introDbDeferred.await(), + animeSkipDeferred.await(), + aniSkipDeferred.await(), + ).also { cache[cacheKey] = it } } suspend fun getSkipIntervalsForMal( malId: String, episode: Int, requireSkipIntroEnabled: Boolean = true, - ): List { + ): List = coroutineScope { val settings = PlayerSettingsRepository.uiState.value - if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList() + if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return@coroutineScope emptyList() val cacheKey = "mal:$malId:$episode" - cache[cacheKey]?.let { return it } + cache[cacheKey]?.let { return@coroutineScope it } - val aniSkip = fetchFromAniSkip(malId, episode) + val aniSkipDeferred = async { fetchFromAniSkip(malId, episode) } - val imdbId = try { - SkipIntroApi.resolveMalToImdb(malId)?.imdb - } catch (_: Exception) { null } + val imdbIdDeferred = async { + try { + SkipIntroApi.resolveMalToImdb(malId)?.imdb + } catch (_: Exception) { null } + } var introDb = emptyList() var animeSkip = emptyList() + val imdbId = imdbIdDeferred.await() if (imdbId != null) { val entries = resolveImdbEntries(imdbId) val season = entries.indexOfFirst { it.myanimelist == malId.toIntOrNull() } + 1 - if (introDbConfigured) introDb = fetchFromIntroDb(imdbId, season, episode) - animeSkip = fetchAnimeSkipForEntries(entries, season, episode) + val introDbDeferred = async { + if (introDbConfigured) fetchFromIntroDb(imdbId, season, episode) else emptyList() + } + val animeSkipDeferred = async { fetchAnimeSkipForEntries(entries, season, episode) } + introDb = introDbDeferred.await() + animeSkip = animeSkipDeferred.await() } else { val anilistId = try { SkipIntroApi.resolveMalToAnilist(malId)?.anilist?.toString() @@ -66,36 +84,46 @@ object SkipIntroRepository { if (anilistId != null) animeSkip = fetchFromAnimeSkip(anilistId, episode, season = null) } - return mergeByPriority(introDb, animeSkip, aniSkip).also { cache[cacheKey] = it } + return@coroutineScope mergeByPriority(introDb, animeSkip, aniSkipDeferred.await()).also { cache[cacheKey] = it } } suspend fun getSkipIntervalsForKitsu( kitsuId: String, episode: Int, requireSkipIntroEnabled: Boolean = true, - ): List { + ): List = coroutineScope { val settings = PlayerSettingsRepository.uiState.value - if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList() + if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return@coroutineScope emptyList() val cacheKey = "kitsu:$kitsuId:$episode" - cache[cacheKey]?.let { return it } + cache[cacheKey]?.let { return@coroutineScope it } - val malId = try { - SkipIntroApi.resolveKitsuToMal(kitsuId)?.myanimelist?.toString() - } catch (_: Exception) { null } - val aniSkip = if (malId != null) fetchFromAniSkip(malId, episode) else emptyList() - - val imdbId = try { - SkipIntroApi.resolveKitsuToImdb(kitsuId)?.imdb - } catch (_: Exception) { null } + val malIdDeferred = async { + try { + SkipIntroApi.resolveKitsuToMal(kitsuId)?.myanimelist?.toString() + } catch (_: Exception) { null } + } + val imdbIdDeferred = async { + try { + SkipIntroApi.resolveKitsuToImdb(kitsuId)?.imdb + } catch (_: Exception) { null } + } + val aniSkipDeferred = async { + malIdDeferred.await()?.let { fetchFromAniSkip(it, episode) } ?: emptyList() + } var introDb = emptyList() var animeSkip = emptyList() + val imdbId = imdbIdDeferred.await() if (imdbId != null) { val entries = resolveImdbEntries(imdbId) val season = entries.indexOfFirst { it.kitsu == kitsuId.toIntOrNull() } + 1 - if (introDbConfigured) introDb = fetchFromIntroDb(imdbId, season, episode) - animeSkip = fetchAnimeSkipForEntries(entries, season, episode) + val introDbDeferred = async { + if (introDbConfigured) fetchFromIntroDb(imdbId, season, episode) else emptyList() + } + val animeSkipDeferred = async { fetchAnimeSkipForEntries(entries, season, episode) } + introDb = introDbDeferred.await() + animeSkip = animeSkipDeferred.await() } else { val anilistId = try { SkipIntroApi.resolveKitsuToAnilist(kitsuId)?.anilist?.toString() @@ -103,7 +131,7 @@ object SkipIntroRepository { if (anilistId != null) animeSkip = fetchFromAnimeSkip(anilistId, episode, season = null) } - return mergeByPriority(introDb, animeSkip, aniSkip).also { cache[cacheKey] = it } + return@coroutineScope mergeByPriority(introDb, animeSkip, aniSkipDeferred.await()).also { cache[cacheKey] = it } } /**