From 874428449a90da15c6a9d45e679ced90d3208cb9 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Tue, 24 Sep 2024 14:23:56 +0800 Subject: [PATCH] auto select --- .../source/media/fetch/MediaFetchSession.kt | 8 +-- .../data/source/media/fetch/MediaFetcher.kt | 54 ++++++++++++++++++- .../source/media/selector/MediaSelector.kt | 5 +- .../media/selector/MediaSelectorAutoSelect.kt | 11 +++- .../ui/subject/episode/EpisodeViewModel.kt | 2 +- .../source/media/fetch/MediaFetcherTest.kt | 12 ++--- .../media/framework/TestMediaSelector.kt | 5 +- 7 files changed, 82 insertions(+), 15 deletions(-) diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetchSession.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetchSession.kt index 1699345b24..e8f9b0ae58 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetchSession.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetchSession.kt @@ -76,7 +76,7 @@ interface MediaFetchSession { * 注意, 即使 [hasCompletedOrDisabled] 现在为 `true`, 它也可能在未来因为数据源重试, 或者 [request] 变更而变为 `false`. * 因此该 flow 永远不会完结. */ - val hasCompleted: Flow + val hasCompleted: Flow } /** @@ -84,10 +84,12 @@ interface MediaFetchSession { * * 支持 cancellation. */ -suspend fun MediaFetchSession.awaitCompletion() { +suspend fun MediaFetchSession.awaitCompletion( + onHasCompletedChanged: suspend (completedCondition: CompletedCondition) -> Boolean = { it.allCompleted } +) { cancellableCoroutineScope { cumulativeResults.shareIn(this, started = SharingStarted.Eagerly, replay = 1) - hasCompleted.first { it } + hasCompleted.first { onHasCompletedChanged(it) } cancelScope() } } diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetcher.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetcher.kt index b413fdb740..925cd92cac 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetcher.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/fetch/MediaFetcher.kt @@ -338,10 +338,43 @@ class MediaSourceMediaFetcher( } override val hasCompleted = if (mediaSourceResults.isEmpty()) { - flowOf(true) + flowOf(CompletedCondition.AllCompleted) } else { - combine(mediaSourceResults.map { it.state }) { states -> + val webStates = mediaSourceResults.filter { it.kind == MediaSourceKind.WEB } + .map { it.state } + val bitTorrentStates = mediaSourceResults.filter { it.kind == MediaSourceKind.BitTorrent } + .map { it.state } + val localCacheStates = mediaSourceResults.filter { it.kind == MediaSourceKind.LocalCache } + .map { it.state } + + val webCompleted = combine(webStates) { states -> + states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + }.onStart { + if (webStates.isEmpty()) emit(false) + } + val btCompleted = combine(bitTorrentStates) { states -> + states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + }.onStart { + if (bitTorrentStates.isEmpty()) emit(false) + } + val localCacheCompleted = combine(localCacheStates) { states -> + states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + }.onStart { + if (localCacheStates.isEmpty()) emit(false) + } + val allCompleted = combine(mediaSourceResults.map { it.state }) { states -> states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + } + + combine( + webCompleted, btCompleted, localCacheCompleted, allCompleted, + ) { web, bt, local, all -> + CompletedCondition( + webCompleted = web, + btCompleted = bt, + localCacheCompleted = local, + allCompleted = all, + ) }.flowOn(flowContext) } } @@ -358,3 +391,20 @@ class MediaSourceMediaFetcher( private const val ENABLE_WATCHDOG = false } } + +class CompletedCondition( + val webCompleted: Boolean, + val btCompleted: Boolean, + val localCacheCompleted: Boolean, + val allCompleted: Boolean, +) { + + companion object { + val AllCompleted = CompletedCondition( + webCompleted = true, + btCompleted = true, + localCacheCompleted = true, + allCompleted = true, + ) + } +} diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelector.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelector.kt index 1c5d353701..eedfd18a54 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelector.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelector.kt @@ -14,8 +14,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn -import me.him188.ani.app.data.models.preference.MediaSelectorSettings import me.him188.ani.app.data.models.preference.MediaPreference +import me.him188.ani.app.data.models.preference.MediaSelectorSettings import me.him188.ani.datasources.api.Media import me.him188.ani.datasources.api.source.MediaSourceKind import me.him188.ani.datasources.api.source.MediaSourceLocation @@ -45,6 +45,7 @@ interface MediaSelector { val subtitleLanguageId: MediaPreferenceItem val mediaSourceId: MediaPreferenceItem + val preferKind: Flow /** * 经过 [alliance], [resolution] 等[偏好][MediaPreference]筛选后的列表. */ @@ -302,6 +303,8 @@ class DefaultMediaSelector( getFromPreference = { it.mediaSourceId }, ) + override val preferKind: Flow = mediaSelectorSettings.map { it.preferKind } + /** * 当前会话中的生效偏好 */ diff --git a/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelectorAutoSelect.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelectorAutoSelect.kt index 88477ca7ce..d0aeeb80f6 100644 --- a/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelectorAutoSelect.kt +++ b/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelectorAutoSelect.kt @@ -30,7 +30,16 @@ value class MediaSelectorAutoSelect( */ suspend fun awaitCompletedAndSelectDefault(mediaFetchSession: MediaFetchSession): Media? { // 等全部加载完成 - mediaFetchSession.awaitCompletion() + mediaFetchSession.awaitCompletion { completedCondition -> + if (completedCondition.allCompleted) return@awaitCompletion true + return@awaitCompletion mediaSelector.preferKind.first()?.let { + when (it) { + MediaSourceKind.WEB -> completedCondition.webCompleted + MediaSourceKind.BitTorrent -> completedCondition.btCompleted + MediaSourceKind.LocalCache -> completedCondition.localCacheCompleted + } + } ?: completedCondition.allCompleted + } if (mediaSelector.selected.value == null) { val selected = mediaSelector.trySelectDefault() return selected diff --git a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt index a67ba32bfe..901c633376 100644 --- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt +++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt @@ -386,7 +386,7 @@ private class EpisodeViewModelImpl( private val playerLauncher: PlayerLauncher = PlayerLauncher( mediaSelector, videoSourceResolver, playerState, mediaSourceInfoProvider, episodeInfo, - mediaFetchSession.flatMapLatest { it.hasCompleted }.map { !it }, + mediaFetchSession.flatMapLatest { it.hasCompleted }.map { !it.allCompleted }, backgroundScope.coroutineContext, ) diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/fetch/MediaFetcherTest.kt b/app/shared/src/desktopTest/kotlin/data/source/media/fetch/MediaFetcherTest.kt index 98db131c2e..1d83c30888 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/fetch/MediaFetcherTest.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/fetch/MediaFetcherTest.kt @@ -93,7 +93,7 @@ class MediaFetcherTest { assertEquals(1, session.mediaSourceResults.size) val res = session.mediaSourceResults.first() assertIs(res.state.value) - assertEquals(false, session.hasCompleted.first()) + assertEquals(false, session.hasCompleted.first().allCompleted) } /////////////////////////////////////////////////////////////////////////// @@ -104,7 +104,7 @@ class MediaFetcherTest { fun `hasCompleted is initially true if no source`() = runTest { val session = createFetcher().newSession(request1) assertEquals(0, session.mediaSourceResults.size) - assertEquals(true, session.hasCompleted.first()) + assertEquals(true, session.hasCompleted.first().allCompleted) } @Test @@ -113,7 +113,7 @@ class MediaFetcherTest { assertEquals(1, session.mediaSourceResults.size) val res = session.mediaSourceResults.first() assertIs(res.state.value) - assertEquals(false, session.hasCompleted.first()) + assertEquals(false, session.hasCompleted.first().allCompleted) } @Test @@ -125,7 +125,7 @@ class MediaFetcherTest { assertEquals(2, session.mediaSourceResults.size) assertIs(session.mediaSourceResults.first().state.value) assertIs(session.mediaSourceResults.toList()[1].state.value) - assertEquals(true, session.hasCompleted.first()) + assertEquals(true, session.hasCompleted.first().allCompleted) } @Test @@ -137,7 +137,7 @@ class MediaFetcherTest { assertEquals(2, session.mediaSourceResults.size) assertIs(session.mediaSourceResults.first().state.value) assertIs(session.mediaSourceResults[1].state.value) - assertEquals(false, session.hasCompleted.first()) + assertEquals(false, session.hasCompleted.first().allCompleted) } /////////////////////////////////////////////////////////////////////////// @@ -396,7 +396,7 @@ class MediaFetcherTest { assertIs(session.mediaSourceResults.first().state.value) assertIs(session.mediaSourceResults[1].state.value) assertEquals(0, session.awaitCompletedResults().size) - assertEquals(true, session.hasCompleted.first()) + assertEquals(true, session.hasCompleted.first().allCompleted) } /////////////////////////////////////////////////////////////////////////// diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/framework/TestMediaSelector.kt b/app/shared/src/desktopTest/kotlin/data/source/media/framework/TestMediaSelector.kt index f579b9d9eb..af2c98ee49 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/framework/TestMediaSelector.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/framework/TestMediaSelector.kt @@ -5,6 +5,8 @@ package me.him188.ani.app.data.source.media.framework import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import me.him188.ani.app.data.models.preference.MediaPreference import me.him188.ani.app.data.source.media.selector.DefaultMediaSelector import me.him188.ani.app.data.source.media.selector.MediaPreferenceItem import me.him188.ani.app.data.source.media.selector.MediaSelector @@ -12,8 +14,8 @@ import me.him188.ani.app.data.source.media.selector.MediaSelectorEvents import me.him188.ani.app.data.source.media.selector.MutableMediaSelectorEvents import me.him188.ani.app.data.source.media.selector.OptionalPreference import me.him188.ani.app.data.source.media.selector.orElse -import me.him188.ani.app.data.models.preference.MediaPreference import me.him188.ani.datasources.api.Media +import me.him188.ani.datasources.api.source.MediaSourceKind import me.him188.ani.datasources.api.topic.Resolution import me.him188.ani.datasources.api.topic.SubtitleLanguage.ChineseSimplified import me.him188.ani.datasources.api.topic.SubtitleLanguage.ChineseTraditional @@ -57,6 +59,7 @@ open class TestMediaSelector( final override val resolution: TestMediaPreferenceItem = TestMediaPreferenceItem() final override val subtitleLanguageId: TestMediaPreferenceItem = TestMediaPreferenceItem() final override val mediaSourceId: TestMediaPreferenceItem = TestMediaPreferenceItem() + final override val preferKind: Flow = flowOf(MediaSourceKind.WEB) private val mergedPreference = combine( defaultPreference,