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 01/10] 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, From 14ba6db553b594d8199d40bf6f5c194661f619c7 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Wed, 25 Sep 2024 15:08:50 +0800 Subject: [PATCH 02/10] optimize something --- .../data/source/media/fetch/MediaFetcher.kt | 30 ++++++++----------- .../media/selector/MediaSelectorAutoSelect.kt | 1 - 2 files changed, 12 insertions(+), 19 deletions(-) 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 925cd92cac..30eaf268b4 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 @@ -348,32 +348,28 @@ class MediaSourceMediaFetcher( .map { it.state } val webCompleted = combine(webStates) { states -> - states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } }.onStart { - if (webStates.isEmpty()) emit(false) + if (webStates.isEmpty()) emit(null) } val btCompleted = combine(bitTorrentStates) { states -> - states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } }.onStart { - if (bitTorrentStates.isEmpty()) emit(false) + if (bitTorrentStates.isEmpty()) emit(null) } val localCacheCompleted = combine(localCacheStates) { states -> - states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + 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 } + if (localCacheStates.isEmpty()) emit(null) } combine( - webCompleted, btCompleted, localCacheCompleted, allCompleted, - ) { web, bt, local, all -> + webCompleted, btCompleted, localCacheCompleted, + ) { web, bt, local -> CompletedCondition( webCompleted = web, btCompleted = bt, localCacheCompleted = local, - allCompleted = all, ) }.flowOn(flowContext) } @@ -393,18 +389,16 @@ class MediaSourceMediaFetcher( } class CompletedCondition( - val webCompleted: Boolean, - val btCompleted: Boolean, - val localCacheCompleted: Boolean, - val allCompleted: Boolean, + val webCompleted: Boolean?, + val btCompleted: Boolean?, + val localCacheCompleted: Boolean?, ) { - + val allCompleted: Boolean get() = webCompleted ?: true && btCompleted ?: true && localCacheCompleted ?: true 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/MediaSelectorAutoSelect.kt b/app/shared/app-data/src/commonMain/kotlin/data/source/media/selector/MediaSelectorAutoSelect.kt index d0aeeb80f6..cd029c0465 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 @@ -31,7 +31,6 @@ value class MediaSelectorAutoSelect( suspend fun awaitCompletedAndSelectDefault(mediaFetchSession: MediaFetchSession): Media? { // 等全部加载完成 mediaFetchSession.awaitCompletion { completedCondition -> - if (completedCondition.allCompleted) return@awaitCompletion true return@awaitCompletion mediaSelector.preferKind.first()?.let { when (it) { MediaSourceKind.WEB -> completedCondition.webCompleted From 91c3cfbf7aa74e819dc7fd235f3e23a8b699f072 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Wed, 25 Sep 2024 15:24:15 +0800 Subject: [PATCH 03/10] add argument preferKind --- .../kotlin/data/source/media/selector/MediaSelector.kt | 3 --- .../source/media/selector/MediaSelectorAutoSelect.kt | 9 +++++++-- .../kotlin/ui/subject/episode/EpisodeViewModel.kt | 6 +++++- 3 files changed, 12 insertions(+), 6 deletions(-) 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 eedfd18a54..97d057df2f 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 @@ -45,7 +45,6 @@ interface MediaSelector { val subtitleLanguageId: MediaPreferenceItem val mediaSourceId: MediaPreferenceItem - val preferKind: Flow /** * 经过 [alliance], [resolution] 等[偏好][MediaPreference]筛选后的列表. */ @@ -303,8 +302,6 @@ 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 cd029c0465..f7fad67978 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 @@ -1,8 +1,10 @@ package me.him188.ani.app.data.source.media.selector +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.takeWhile import me.him188.ani.app.data.source.media.fetch.MediaFetchSession import me.him188.ani.app.data.source.media.fetch.awaitCompletion @@ -28,10 +30,13 @@ value class MediaSelectorAutoSelect( * * 返回成功选择的 [Media] 对象. 当用户已经手动选择过一个别的 [Media], 或者没有可选的 [Media] 时返回 `null`. */ - suspend fun awaitCompletedAndSelectDefault(mediaFetchSession: MediaFetchSession): Media? { + suspend fun awaitCompletedAndSelectDefault( + mediaFetchSession: MediaFetchSession, + preferKind: Flow = flowOf() + ): Media? { // 等全部加载完成 mediaFetchSession.awaitCompletion { completedCondition -> - return@awaitCompletion mediaSelector.preferKind.first()?.let { + return@awaitCompletion preferKind.first()?.let { when (it) { MediaSourceKind.WEB -> completedCondition.webCompleted MediaSourceKind.BitTorrent -> completedCondition.btCompleted 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 901c633376..45ccbab81c 100644 --- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt +++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt @@ -296,10 +296,14 @@ private class EpisodeViewModelImpl( ) .apply { autoSelect.run { + launchInBackground { mediaFetchSession.collectLatest { awaitSwitchEpisodeCompleted() - awaitCompletedAndSelectDefault(it) + awaitCompletedAndSelectDefault( + it, + settingsRepository.mediaSelectorSettings.flow.map { it.preferKind }, + ) } } launchInBackground { From bb31dcd0fed0b8ba875831947660a08e454e304b Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Wed, 25 Sep 2024 22:39:10 +0800 Subject: [PATCH 04/10] use value class --- .../source/media/fetch/MediaFetchSession.kt | 4 +- .../data/source/media/fetch/MediaFetcher.kt | 67 ++++++++----------- .../media/selector/MediaSelectorAutoSelect.kt | 10 +-- .../ui/subject/episode/EpisodeViewModel.kt | 2 +- .../source/media/fetch/MediaFetcherTest.kt | 12 ++-- .../media/framework/TestMediaSelector.kt | 3 - 6 files changed, 41 insertions(+), 57 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 e8f9b0ae58..dcb8bda37c 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 } /** @@ -85,7 +85,7 @@ interface MediaFetchSession { * 支持 cancellation. */ suspend fun MediaFetchSession.awaitCompletion( - onHasCompletedChanged: suspend (completedCondition: CompletedCondition) -> Boolean = { it.allCompleted } + onHasCompletedChanged: suspend (completedConditions: CompletedConditions) -> Boolean = { it.allCompleted() } ) { cancellableCoroutineScope { cumulativeResults.shareIn(this, started = SharingStarted.Eagerly, replay = 1) 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 30eaf268b4..c03633c372 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 @@ -47,8 +47,11 @@ import me.him188.ani.utils.coroutines.cancellableCoroutineScope import me.him188.ani.utils.logging.error import me.him188.ani.utils.logging.info import me.him188.ani.utils.logging.logger +import me.him188.ani.utils.platform.collections.EnumMap +import me.him188.ani.utils.platform.collections.ImmutableEnumMap import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.jvm.JvmInline /** * [MediaFetcher], 为支持从多个 [MediaSource] 并行获取 [Media] 的综合查询工具. @@ -338,38 +341,22 @@ class MediaSourceMediaFetcher( } override val hasCompleted = if (mediaSourceResults.isEmpty()) { - flowOf(CompletedCondition.AllCompleted) + flowOf(CompletedConditions.AllCompleted) } else { - 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(null) - } - val btCompleted = combine(bitTorrentStates) { states -> - states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } - }.onStart { - if (bitTorrentStates.isEmpty()) emit(null) - } - val localCacheCompleted = combine(localCacheStates) { states -> - states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } - }.onStart { - if (localCacheStates.isEmpty()) emit(null) + val map = MediaSourceKind.entries.map { kind -> + val stateList = mediaSourceResults.filter { it.kind == kind }.map { it.state } + combine(stateList) { states -> + kind to states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + }.onStart { + if (stateList.isEmpty()) emit(kind to null) + } } - combine( - webCompleted, btCompleted, localCacheCompleted, - ) { web, bt, local -> - CompletedCondition( - webCompleted = web, - btCompleted = bt, - localCacheCompleted = local, + combine(map) { pairs -> + CompletedConditions( + ImmutableEnumMap { kind -> + pairs.find { it.first == kind }?.second + }, ) }.flowOn(flowContext) } @@ -388,17 +375,21 @@ class MediaSourceMediaFetcher( } } -class CompletedCondition( - val webCompleted: Boolean?, - val btCompleted: Boolean?, - val localCacheCompleted: Boolean?, +@JvmInline +value class CompletedConditions( + val values: EnumMap ) { - val allCompleted: Boolean get() = webCompleted ?: true && btCompleted ?: true && localCacheCompleted ?: true + fun allCompleted() = values.values.all { it ?: true } + + operator fun get(kind: MediaSourceKind): Boolean? = values[kind] + + fun copy( + values: EnumMap = this.values, + ): CompletedConditions = CompletedConditions(values) + companion object { - val AllCompleted = CompletedCondition( - webCompleted = true, - btCompleted = true, - localCacheCompleted = true, + val AllCompleted = CompletedConditions( + ImmutableEnumMap { true }, ) } } 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 f7fad67978..a049b1c7ac 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 @@ -35,14 +35,10 @@ value class MediaSelectorAutoSelect( preferKind: Flow = flowOf() ): Media? { // 等全部加载完成 - mediaFetchSession.awaitCompletion { completedCondition -> + mediaFetchSession.awaitCompletion { completedConditions -> return@awaitCompletion preferKind.first()?.let { - when (it) { - MediaSourceKind.WEB -> completedCondition.webCompleted - MediaSourceKind.BitTorrent -> completedCondition.btCompleted - MediaSourceKind.LocalCache -> completedCondition.localCacheCompleted - } - } ?: completedCondition.allCompleted + completedConditions[it] + } ?: completedConditions.allCompleted() } if (mediaSelector.selected.value == null) { val selected = mediaSelector.trySelectDefault() 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 45ccbab81c..934f17c459 100644 --- a/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt +++ b/app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodeViewModel.kt @@ -390,7 +390,7 @@ private class EpisodeViewModelImpl( private val playerLauncher: PlayerLauncher = PlayerLauncher( mediaSelector, videoSourceResolver, playerState, mediaSourceInfoProvider, episodeInfo, - mediaFetchSession.flatMapLatest { it.hasCompleted }.map { !it.allCompleted }, + 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 1d83c30888..4bb7a3aef5 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().allCompleted) + 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().allCompleted) + 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().allCompleted) + 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().allCompleted) + 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().allCompleted) + 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().allCompleted) + 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 af2c98ee49..54fdb548b5 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,7 +5,6 @@ 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 @@ -15,7 +14,6 @@ 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.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 @@ -59,7 +57,6 @@ 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, From 98f3411f9ad705afe08639be68d9ef1845a168fb Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Thu, 26 Sep 2024 00:20:34 +0800 Subject: [PATCH 05/10] fix bug --- .../data/source/media/selector/MediaSelectorAutoSelect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a049b1c7ac..068904c7f0 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 @@ -32,7 +32,7 @@ value class MediaSelectorAutoSelect( */ suspend fun awaitCompletedAndSelectDefault( mediaFetchSession: MediaFetchSession, - preferKind: Flow = flowOf() + preferKind: Flow = flowOf(null) ): Media? { // 等全部加载完成 mediaFetchSession.awaitCompletion { completedConditions -> From 358eec8daceed21871265742cbd9ff330b97bf2b Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Fri, 27 Sep 2024 20:15:34 +0800 Subject: [PATCH 06/10] fix bug and add test --- .../data/source/media/fetch/MediaFetcher.kt | 12 +- .../selector/MediaSelectorAutoSelectTest.kt | 157 +++++++++++++++++- 2 files changed, 166 insertions(+), 3 deletions(-) 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 c03633c372..52e8b70043 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 @@ -346,7 +346,11 @@ class MediaSourceMediaFetcher( val map = MediaSourceKind.entries.map { kind -> val stateList = mediaSourceResults.filter { it.kind == kind }.map { it.state } combine(stateList) { states -> - kind to states?.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } + kind to when { + states.all { it is MediaSourceFetchState.Disabled } -> null + states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } -> true + else -> false + } }.onStart { if (stateList.isEmpty()) emit(kind to null) } @@ -381,7 +385,11 @@ value class CompletedConditions( ) { fun allCompleted() = values.values.all { it ?: true } - operator fun get(kind: MediaSourceKind): Boolean? = values[kind] + operator fun get(kind: MediaSourceKind): Boolean? = try { + values[kind] + } catch (e: NoSuchElementException) { + null + } fun copy( values: EnumMap = this.values, diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt index b992099b2e..980cd62502 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt @@ -1,16 +1,20 @@ package me.him188.ani.app.data.source.media.selector +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runTest +import me.him188.ani.app.data.models.preference.MediaPreference import me.him188.ani.app.data.models.preference.MediaSelectorSettings import me.him188.ani.app.data.source.media.SOURCE_DMHY import me.him188.ani.app.data.source.media.TestMediaList import me.him188.ani.app.data.source.media.createTestDefaultMedia +import me.him188.ani.app.data.source.media.fetch.MediaFetchSession import me.him188.ani.app.data.source.media.fetch.MediaFetcherConfig +import me.him188.ani.app.data.source.media.fetch.MediaSourceFetchState import me.him188.ani.app.data.source.media.fetch.MediaSourceMediaFetcher import me.him188.ani.app.data.source.media.instance.createTestMediaSourceInstance -import me.him188.ani.app.data.models.preference.MediaPreference import me.him188.ani.datasources.api.DefaultMedia import me.him188.ani.datasources.api.EpisodeSort import me.him188.ani.datasources.api.MediaProperties @@ -30,6 +34,7 @@ import me.him188.ani.datasources.api.topic.SubtitleLanguage.ChineseTraditional import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -118,6 +123,64 @@ class MediaSelectorAutoSelectTest { private val autoSelect get() = selector.autoSelect + private fun mediaFetchSessionWithCompletableDeferred( + btEnabled: Boolean = true, + webEnabled: Boolean = true, + preferKind: MediaSourceKind? = MediaSourceKind.BitTorrent, + beforeBTFetch: suspend CompletableDeferred.() -> Unit = {}, + afterBTFetch: suspend CompletableDeferred.() -> Unit = {}, + beforeWebFetch: suspend CompletableDeferred.() -> Unit = {}, + afterWebFetch: suspend CompletableDeferred.() -> Unit = {}, + ): MediaFetchSession { + val mediaList = mutableListOf() + if (webEnabled) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }) + if (btEnabled) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.BitTorrent) }) + this.mediaList.value = mediaList + mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = preferKind) + + val completableDeferred = CompletableDeferred() + val mediaFetcher = MediaSourceMediaFetcher( + configProvider = { MediaFetcherConfig.Default }, + mediaSources = listOf( + createTestMediaSourceInstance( + isEnabled = btEnabled, + source = TestHttpMediaSource( + fetch = { + beforeBTFetch(completableDeferred) + SinglePagePagedSource { + mediaList.filter { it.kind == MediaSourceKind.BitTorrent } + .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() + }.also { afterBTFetch(completableDeferred) } + }, + ), + ), + createTestMediaSourceInstance( + isEnabled = webEnabled, + source = TestHttpMediaSource( + kind = MediaSourceKind.WEB, + fetch = { + beforeWebFetch(completableDeferred) + SinglePagePagedSource { + mediaList.filter { it.kind == MediaSourceKind.WEB } + .map { MediaMatch(it.copy(kind = MediaSourceKind.WEB), MatchKind.EXACT) }.asFlow() + }.also { afterWebFetch(completableDeferred) } + }, + ), + ), + ), + flowContext = EmptyCoroutineContext, + ) + return mediaFetcher.newSession( + MediaFetchRequest( + subjectId = "1", + episodeId = "1", + subjectNames = setOf("孤独摇滚"), + episodeSort = EpisodeSort(1), + episodeName = "test", + ), + ) + } + /////////////////////////////////////////////////////////////////////////// // awaitCompletedAndSelectDefault /////////////////////////////////////////////////////////////////////////// @@ -128,6 +191,98 @@ class MediaSelectorAutoSelectTest { assertNotNull(selected) } + @Test + fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt done`() = runTest { + val session = mediaFetchSessionWithCompletableDeferred( + beforeWebFetch = { + await() + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.BitTorrent, selected.kind) + } + + @Test + fun `awaitCompletedAndSelectDefault selects one when prefer bt and web done`() = runTest { + val session = mediaFetchSessionWithCompletableDeferred( + beforeBTFetch = { + await() + }, + afterWebFetch = { + complete(Unit) + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.BitTorrent, selected.kind) + } + + @Test + fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt media source disable`() = runTest { + val session = mediaFetchSessionWithCompletableDeferred( + btEnabled = false, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + val btRes = session.mediaSourceResults[0] + assertIs(btRes.state.value) + assertNotNull(selected) + assertEquals(MediaSourceKind.WEB, selected.kind) + } + + @Test + fun `awaitCompletedAndSelectDefault selects one when no prefer and bt done`() = runTest { + val session = mediaFetchSessionWithCompletableDeferred( + preferKind = null, + afterBTFetch = { + complete(Unit) + }, + beforeWebFetch = { + await() + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session) + assertNotNull(selected) + } + + @Test + fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt media source no results`() = runTest { + mediaList.value = TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }.toMutableList() + mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = MediaSourceKind.BitTorrent) + val mediaFetcher = MediaSourceMediaFetcher( + configProvider = { MediaFetcherConfig.Default }, + mediaSources = listOf( + createTestMediaSourceInstance( + source = TestHttpMediaSource(), + ), + createTestMediaSourceInstance( + source = TestHttpMediaSource( + kind = MediaSourceKind.WEB, + fetch = { + SinglePagePagedSource { + mediaList.value.filter { it.kind == MediaSourceKind.WEB } + .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() + } + }, + ), + ), + ), + flowContext = EmptyCoroutineContext, + ) + val session = mediaFetcher.newSession( + MediaFetchRequest( + subjectId = "1", + episodeId = "1", + subjectNames = setOf("孤独摇滚"), + episodeSort = EpisodeSort(1), + episodeName = "test", + ), + ) + + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.WEB, selected.kind) + } @Test fun `awaitCompletedAndSelectDefault twice does not select`() = runTest { val selected = autoSelect.awaitCompletedAndSelectDefault(mediaFetchSession()) From ff2ff6891cbc874e03017e43a46e3731596085e6 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Sun, 29 Sep 2024 16:36:44 +0800 Subject: [PATCH 07/10] optimize something --- .../data/source/media/fetch/MediaFetcher.kt | 10 +- .../selector/MediaSelectorAutoSelectTest.kt | 233 ++++++++++-------- 2 files changed, 126 insertions(+), 117 deletions(-) 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 52e8b70043..7688a0ef7b 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 @@ -51,7 +51,6 @@ import me.him188.ani.utils.platform.collections.EnumMap import me.him188.ani.utils.platform.collections.ImmutableEnumMap import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.jvm.JvmInline /** * [MediaFetcher], 为支持从多个 [MediaSource] 并行获取 [Media] 的综合查询工具. @@ -379,9 +378,8 @@ class MediaSourceMediaFetcher( } } -@JvmInline -value class CompletedConditions( - val values: EnumMap +class CompletedConditions( + private val values: EnumMap ) { fun allCompleted() = values.values.all { it ?: true } @@ -391,10 +389,6 @@ value class CompletedConditions( null } - fun copy( - values: EnumMap = this.values, - ): CompletedConditions = CompletedConditions(values) - companion object { val AllCompleted = CompletedConditions( ImmutableEnumMap { true }, diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt index 980cd62502..ed2f69eaa6 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt @@ -123,47 +123,48 @@ class MediaSelectorAutoSelectTest { private val autoSelect get() = selector.autoSelect - private fun mediaFetchSessionWithCompletableDeferred( - btEnabled: Boolean = true, - webEnabled: Boolean = true, - preferKind: MediaSourceKind? = MediaSourceKind.BitTorrent, - beforeBTFetch: suspend CompletableDeferred.() -> Unit = {}, - afterBTFetch: suspend CompletableDeferred.() -> Unit = {}, - beforeWebFetch: suspend CompletableDeferred.() -> Unit = {}, - afterWebFetch: suspend CompletableDeferred.() -> Unit = {}, + private suspend fun mediaFetchSessionWithFetchHook( + addBtSources: Boolean, + addWebSources: Boolean, + preferKind: MediaSourceKind?, + beforeBtFetch: suspend () -> Unit = {}, + afterBtFetch: suspend () -> Unit = {}, + beforeWebFetch: suspend () -> Unit = {}, + afterWebFetch: suspend () -> Unit = {}, ): MediaFetchSession { val mediaList = mutableListOf() - if (webEnabled) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }) - if (btEnabled) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.BitTorrent) }) + // 至少保持一个 local cache 类型 + mediaList.addAll(TestMediaList.take(1).map { it.copy(kind = MediaSourceKind.LocalCache) }) + if (addWebSources) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }) + if (addBtSources) mediaList.addAll(TestMediaList.map { it.copy(kind = MediaSourceKind.BitTorrent) }) this.mediaList.value = mediaList mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = preferKind) - val completableDeferred = CompletableDeferred() val mediaFetcher = MediaSourceMediaFetcher( configProvider = { MediaFetcherConfig.Default }, mediaSources = listOf( createTestMediaSourceInstance( - isEnabled = btEnabled, + isEnabled = addBtSources, source = TestHttpMediaSource( fetch = { - beforeBTFetch(completableDeferred) + beforeBtFetch() SinglePagePagedSource { mediaList.filter { it.kind == MediaSourceKind.BitTorrent } .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() - }.also { afterBTFetch(completableDeferred) } + }.also { afterBtFetch() } }, ), ), createTestMediaSourceInstance( - isEnabled = webEnabled, + isEnabled = addWebSources, source = TestHttpMediaSource( kind = MediaSourceKind.WEB, fetch = { - beforeWebFetch(completableDeferred) + beforeWebFetch() SinglePagePagedSource { mediaList.filter { it.kind == MediaSourceKind.WEB } .map { MediaMatch(it.copy(kind = MediaSourceKind.WEB), MatchKind.EXACT) }.asFlow() - }.also { afterWebFetch(completableDeferred) } + }.also { afterWebFetch() } }, ), ), @@ -191,98 +192,6 @@ class MediaSelectorAutoSelectTest { assertNotNull(selected) } - @Test - fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt done`() = runTest { - val session = mediaFetchSessionWithCompletableDeferred( - beforeWebFetch = { - await() - }, - ) - val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) - assertNotNull(selected) - assertEquals(MediaSourceKind.BitTorrent, selected.kind) - } - - @Test - fun `awaitCompletedAndSelectDefault selects one when prefer bt and web done`() = runTest { - val session = mediaFetchSessionWithCompletableDeferred( - beforeBTFetch = { - await() - }, - afterWebFetch = { - complete(Unit) - }, - ) - val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) - assertNotNull(selected) - assertEquals(MediaSourceKind.BitTorrent, selected.kind) - } - - @Test - fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt media source disable`() = runTest { - val session = mediaFetchSessionWithCompletableDeferred( - btEnabled = false, - ) - val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) - val btRes = session.mediaSourceResults[0] - assertIs(btRes.state.value) - assertNotNull(selected) - assertEquals(MediaSourceKind.WEB, selected.kind) - } - - @Test - fun `awaitCompletedAndSelectDefault selects one when no prefer and bt done`() = runTest { - val session = mediaFetchSessionWithCompletableDeferred( - preferKind = null, - afterBTFetch = { - complete(Unit) - }, - beforeWebFetch = { - await() - }, - ) - val selected = autoSelect.awaitCompletedAndSelectDefault(session) - assertNotNull(selected) - } - - @Test - fun `awaitCompletedAndSelectDefault selects one when prefer bt and bt media source no results`() = runTest { - mediaList.value = TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }.toMutableList() - mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = MediaSourceKind.BitTorrent) - val mediaFetcher = MediaSourceMediaFetcher( - configProvider = { MediaFetcherConfig.Default }, - mediaSources = listOf( - createTestMediaSourceInstance( - source = TestHttpMediaSource(), - ), - createTestMediaSourceInstance( - source = TestHttpMediaSource( - kind = MediaSourceKind.WEB, - fetch = { - SinglePagePagedSource { - mediaList.value.filter { it.kind == MediaSourceKind.WEB } - .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() - } - }, - ), - ), - ), - flowContext = EmptyCoroutineContext, - ) - val session = mediaFetcher.newSession( - MediaFetchRequest( - subjectId = "1", - episodeId = "1", - subjectNames = setOf("孤独摇滚"), - episodeSort = EpisodeSort(1), - episodeName = "test", - ), - ) - - val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) - assertNotNull(selected) - assertEquals(MediaSourceKind.WEB, selected.kind) - } @Test fun `awaitCompletedAndSelectDefault twice does not select`() = runTest { val selected = autoSelect.awaitCompletedAndSelectDefault(mediaFetchSession()) @@ -367,4 +276,110 @@ class MediaSelectorAutoSelectTest { val isSuccess = autoSelect.selectCached(mediaFetchSession(), 1) assertNull(isSuccess) } + + @Test + fun `priority select preferred data sources when prefer bt and bt done`() = runTest { + val completableDeferred = CompletableDeferred() + val session = mediaFetchSessionWithFetchHook( + addBtSources = true, + addWebSources = true, + preferKind = MediaSourceKind.BitTorrent, + beforeWebFetch = { + completableDeferred.await() + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.BitTorrent, selected.kind) + } + + @Test + fun `priority select preferred data sources when prefer bt and web done`() = runTest { + val completableDeferred = CompletableDeferred() + val session = mediaFetchSessionWithFetchHook( + addBtSources = true, + addWebSources = true, + preferKind = MediaSourceKind.BitTorrent, + beforeBtFetch = { + completableDeferred.await() + }, + afterWebFetch = { + completableDeferred.complete(Unit) + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.BitTorrent, selected.kind) + } + + @Test + fun `priority select preferred data sources when prefer bt and bt media source disable`() = runTest { + val session = mediaFetchSessionWithFetchHook( + addBtSources = false, + addWebSources = true, + preferKind = MediaSourceKind.BitTorrent, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + val btRes = session.mediaSourceResults[0] + assertIs(btRes.state.value) + assertNotNull(selected) + assertEquals(MediaSourceKind.WEB, selected.kind) + } + + @Test + fun `priority select preferred data sources when no prefer and bt done`() = runTest { + val completableDeferred = CompletableDeferred() + val session = mediaFetchSessionWithFetchHook( + addBtSources = true, + addWebSources = true, + preferKind = null, + afterBtFetch = { + completableDeferred.complete(Unit) + }, + beforeWebFetch = { + completableDeferred.await() + }, + ) + val selected = autoSelect.awaitCompletedAndSelectDefault(session) + assertNotNull(selected) + } + + @Test + fun `priority select preferred data sources when prefer bt and bt media source no results`() = runTest { + mediaList.value = TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }.toMutableList() + mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = MediaSourceKind.BitTorrent) + val mediaFetcher = MediaSourceMediaFetcher( + configProvider = { MediaFetcherConfig.Default }, + mediaSources = listOf( + createTestMediaSourceInstance( + source = TestHttpMediaSource(), + ), + createTestMediaSourceInstance( + source = TestHttpMediaSource( + kind = MediaSourceKind.WEB, + fetch = { + SinglePagePagedSource { + mediaList.value.filter { it.kind == MediaSourceKind.WEB } + .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() + } + }, + ), + ), + ), + flowContext = EmptyCoroutineContext, + ) + val session = mediaFetcher.newSession( + MediaFetchRequest( + subjectId = "1", + episodeId = "1", + subjectNames = setOf("孤独摇滚"), + episodeSort = EpisodeSort(1), + episodeName = "test", + ), + ) + + val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) + assertNotNull(selected) + assertEquals(MediaSourceKind.WEB, selected.kind) + } } From 2c8b82be8cc80b0d7d08f7d7c3fd32e015551ff1 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Sun, 29 Sep 2024 21:49:01 +0800 Subject: [PATCH 08/10] optimize something --- .../data/source/media/fetch/MediaFetcher.kt | 20 ++++++++----------- .../selector/MediaSelectorAutoSelectTest.kt | 2 -- 2 files changed, 8 insertions(+), 14 deletions(-) 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 7688a0ef7b..7f9a221828 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 @@ -342,23 +342,19 @@ class MediaSourceMediaFetcher( override val hasCompleted = if (mediaSourceResults.isEmpty()) { flowOf(CompletedConditions.AllCompleted) } else { - val map = MediaSourceKind.entries.map { kind -> - val stateList = mediaSourceResults.filter { it.kind == kind }.map { it.state } - combine(stateList) { states -> - kind to when { - states.all { it is MediaSourceFetchState.Disabled } -> null - states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled } -> true + combine(mediaSourceResults.map { it.state }) { + val pairs = MediaSourceKind.entries.associateWith { kind -> + val stateList = mediaSourceResults.filter { it.kind == kind }.map { it.state } + when { + // 该类型数据源全部禁用时返回 null,如果返回 false 会导致 awaitCompletion 无法结束 + stateList.all { it.value is MediaSourceFetchState.Disabled } -> null + stateList.all { it.value is MediaSourceFetchState.Completed || it.value is MediaSourceFetchState.Disabled } -> true else -> false } - }.onStart { - if (stateList.isEmpty()) emit(kind to null) } - } - - combine(map) { pairs -> CompletedConditions( ImmutableEnumMap { kind -> - pairs.find { it.first == kind }?.second + pairs[kind] }, ) }.flowOn(flowContext) diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt index ed2f69eaa6..ba987a7e35 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt @@ -169,7 +169,6 @@ class MediaSelectorAutoSelectTest { ), ), ), - flowContext = EmptyCoroutineContext, ) return mediaFetcher.newSession( MediaFetchRequest( @@ -366,7 +365,6 @@ class MediaSelectorAutoSelectTest { ), ), ), - flowContext = EmptyCoroutineContext, ) val session = mediaFetcher.newSession( MediaFetchRequest( From ef59524a58a9d2b838d33b65115ff7fbc0b27589 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Sun, 29 Sep 2024 22:19:22 +0800 Subject: [PATCH 09/10] optimize something --- .../selector/MediaSelectorAutoSelectTest.kt | 90 +++++++++++-------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt index ba987a7e35..3755d6984e 100644 --- a/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt +++ b/app/shared/src/desktopTest/kotlin/data/source/media/selector/MediaSelectorAutoSelectTest.kt @@ -19,6 +19,7 @@ import me.him188.ani.datasources.api.DefaultMedia import me.him188.ani.datasources.api.EpisodeSort import me.him188.ani.datasources.api.MediaProperties import me.him188.ani.datasources.api.paging.SinglePagePagedSource +import me.him188.ani.datasources.api.paging.SizedSource import me.him188.ani.datasources.api.source.MatchKind import me.him188.ani.datasources.api.source.MediaFetchRequest import me.him188.ani.datasources.api.source.MediaMatch @@ -123,14 +124,25 @@ class MediaSelectorAutoSelectTest { private val autoSelect get() = selector.autoSelect + /** + * 创建一个具有一个 bt 源 和一个 web 源的 [MediaFetchSession] + * @param addBtSources 添加 bt 类型的资源信息 + * @param addWebSources 添加 web 类型的资源信息 + * @param btEnabled 启用 bt 数据源 + * @param webEnabled 启用 web 数据源 + */ private suspend fun mediaFetchSessionWithFetchHook( addBtSources: Boolean, addWebSources: Boolean, + btEnabled: Boolean, + webEnabled: Boolean, preferKind: MediaSourceKind?, beforeBtFetch: suspend () -> Unit = {}, afterBtFetch: suspend () -> Unit = {}, beforeWebFetch: suspend () -> Unit = {}, afterWebFetch: suspend () -> Unit = {}, + btFetch: (suspend (MediaFetchRequest) -> SizedSource)? = null, + webFetch: (suspend (MediaFetchRequest) -> SizedSource)? = null, ): MediaFetchSession { val mediaList = mutableListOf() // 至少保持一个 local cache 类型 @@ -144,27 +156,30 @@ class MediaSelectorAutoSelectTest { configProvider = { MediaFetcherConfig.Default }, mediaSources = listOf( createTestMediaSourceInstance( - isEnabled = addBtSources, + isEnabled = btEnabled, source = TestHttpMediaSource( fetch = { beforeBtFetch() - SinglePagePagedSource { - mediaList.filter { it.kind == MediaSourceKind.BitTorrent } - .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() - }.also { afterBtFetch() } + btFetch?.invoke(it).also { afterBtFetch() } + ?: SinglePagePagedSource { + mediaList.filter { it.kind == MediaSourceKind.BitTorrent } + .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() + }.also { afterBtFetch() } }, ), ), createTestMediaSourceInstance( - isEnabled = addWebSources, + isEnabled = webEnabled, source = TestHttpMediaSource( kind = MediaSourceKind.WEB, fetch = { beforeWebFetch() - SinglePagePagedSource { - mediaList.filter { it.kind == MediaSourceKind.WEB } - .map { MediaMatch(it.copy(kind = MediaSourceKind.WEB), MatchKind.EXACT) }.asFlow() - }.also { afterWebFetch() } + webFetch?.invoke(it).also { afterWebFetch() } + ?: SinglePagePagedSource { + mediaList.filter { it.kind == MediaSourceKind.WEB } + .map { MediaMatch(it.copy(kind = MediaSourceKind.WEB), MatchKind.EXACT) } + .asFlow() + }.also { afterWebFetch() } }, ), ), @@ -282,6 +297,8 @@ class MediaSelectorAutoSelectTest { val session = mediaFetchSessionWithFetchHook( addBtSources = true, addWebSources = true, + btEnabled = true, + webEnabled = true, preferKind = MediaSourceKind.BitTorrent, beforeWebFetch = { completableDeferred.await() @@ -298,6 +315,8 @@ class MediaSelectorAutoSelectTest { val session = mediaFetchSessionWithFetchHook( addBtSources = true, addWebSources = true, + btEnabled = true, + webEnabled = true, preferKind = MediaSourceKind.BitTorrent, beforeBtFetch = { completableDeferred.await() @@ -316,6 +335,8 @@ class MediaSelectorAutoSelectTest { val session = mediaFetchSessionWithFetchHook( addBtSources = false, addWebSources = true, + btEnabled = false, + webEnabled = true, preferKind = MediaSourceKind.BitTorrent, ) val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) @@ -331,6 +352,8 @@ class MediaSelectorAutoSelectTest { val session = mediaFetchSessionWithFetchHook( addBtSources = true, addWebSources = true, + btEnabled = true, + webEnabled = true, preferKind = null, afterBtFetch = { completableDeferred.complete(Unit) @@ -345,35 +368,24 @@ class MediaSelectorAutoSelectTest { @Test fun `priority select preferred data sources when prefer bt and bt media source no results`() = runTest { - mediaList.value = TestMediaList.map { it.copy(kind = MediaSourceKind.WEB) }.toMutableList() - mediaSelectorSettings.value = MediaSelectorSettings.Default.copy(preferKind = MediaSourceKind.BitTorrent) - val mediaFetcher = MediaSourceMediaFetcher( - configProvider = { MediaFetcherConfig.Default }, - mediaSources = listOf( - createTestMediaSourceInstance( - source = TestHttpMediaSource(), - ), - createTestMediaSourceInstance( - source = TestHttpMediaSource( - kind = MediaSourceKind.WEB, - fetch = { - SinglePagePagedSource { - mediaList.value.filter { it.kind == MediaSourceKind.WEB } - .map { MediaMatch(it, MatchKind.EXACT) }.asFlow() - } - }, - ), - ), - ), - ) - val session = mediaFetcher.newSession( - MediaFetchRequest( - subjectId = "1", - episodeId = "1", - subjectNames = setOf("孤独摇滚"), - episodeSort = EpisodeSort(1), - episodeName = "test", - ), + val completableDeferred = CompletableDeferred() + val session = mediaFetchSessionWithFetchHook( + addBtSources = false, + addWebSources = true, + btEnabled = true, + webEnabled = true, + preferKind = MediaSourceKind.BitTorrent, + afterBtFetch = { + completableDeferred.complete(Unit) + }, + beforeWebFetch = { + completableDeferred.await() + }, + btFetch = { + SinglePagePagedSource { + emptyList().asFlow() + } + }, ) val selected = autoSelect.awaitCompletedAndSelectDefault(session, mediaSelectorSettings.map { it.preferKind }) From 0a5f9e36a46e3a40cd6c6c4a352009ea627eccb0 Mon Sep 17 00:00:00 2001 From: Nier4ever <20170127nwl@gmail.com> Date: Sun, 29 Sep 2024 22:37:56 +0800 Subject: [PATCH 10/10] optimize something --- .../kotlin/data/source/media/fetch/MediaFetcher.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7f9a221828..bf993640e2 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 @@ -343,12 +343,12 @@ class MediaSourceMediaFetcher( flowOf(CompletedConditions.AllCompleted) } else { combine(mediaSourceResults.map { it.state }) { - val pairs = MediaSourceKind.entries.associateWith { kind -> - val stateList = mediaSourceResults.filter { it.kind == kind }.map { it.state } + val pairs = mediaSourceResults.groupBy { it.kind }.mapValues { results -> + val states = results.value.map { it.state } when { // 该类型数据源全部禁用时返回 null,如果返回 false 会导致 awaitCompletion 无法结束 - stateList.all { it.value is MediaSourceFetchState.Disabled } -> null - stateList.all { it.value is MediaSourceFetchState.Completed || it.value is MediaSourceFetchState.Disabled } -> true + states.all { it.value is MediaSourceFetchState.Disabled } -> null + states.all { it.value is MediaSourceFetchState.Completed || it.value is MediaSourceFetchState.Disabled } -> true else -> false } }