Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

优先选择偏好类型数据源,不需要等其他源查询完毕 #1002

Merged
merged 10 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,20 @@ interface MediaFetchSession {
* 注意, 即使 [hasCompletedOrDisabled] 现在为 `true`, 它也可能在未来因为数据源重试, 或者 [request] 变更而变为 `false`.
* 因此该 flow 永远不会完结.
*/
val hasCompleted: Flow<Boolean>
val hasCompleted: Flow<CompletedConditions>
}

/**
* 启动所有 [MediaSource] 的查询, 挂起当前协程, 直到所有 [MediaSource] 都查询完成.
*
* 支持 cancellation.
*/
suspend fun MediaFetchSession.awaitCompletion() {
suspend fun MediaFetchSession.awaitCompletion(
onHasCompletedChanged: suspend (completedConditions: CompletedConditions) -> Boolean = { it.allCompleted() }
) {
cancellableCoroutineScope {
cumulativeResults.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
hasCompleted.first { it }
hasCompleted.first { onHasCompletedChanged(it) }
cancelScope()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] 的综合查询工具.
Expand Down Expand Up @@ -338,10 +341,27 @@ class MediaSourceMediaFetcher(
}

override val hasCompleted = if (mediaSourceResults.isEmpty()) {
flowOf(true)
flowOf(CompletedConditions.AllCompleted)
} else {
combine(mediaSourceResults.map { it.state }) { states ->
states.all { it is MediaSourceFetchState.Completed || it is MediaSourceFetchState.Disabled }
val map = MediaSourceKind.entries.map { kind ->
Him188 marked this conversation as resolved.
Show resolved Hide resolved
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
else -> false
NieR4ever marked this conversation as resolved.
Show resolved Hide resolved
}
}.onStart {
if (stateList.isEmpty()) emit(kind to null)
}
}

combine(map) { pairs ->
NieR4ever marked this conversation as resolved.
Show resolved Hide resolved
CompletedConditions(
ImmutableEnumMap<MediaSourceKind, _> { kind ->
pairs.find { it.first == kind }?.second
},
)
}.flowOn(flowContext)
}
}
Expand All @@ -358,3 +378,26 @@ class MediaSourceMediaFetcher(
private const val ENABLE_WATCHDOG = false
}
}

@JvmInline
value class CompletedConditions(
NieR4ever marked this conversation as resolved.
Show resolved Hide resolved
val values: EnumMap<MediaSourceKind, Boolean?>
NieR4ever marked this conversation as resolved.
Show resolved Hide resolved
) {
fun allCompleted() = values.values.all { it ?: true }

operator fun get(kind: MediaSourceKind): Boolean? = try {
values[kind]
} catch (e: NoSuchElementException) {
null
}

fun copy(
NieR4ever marked this conversation as resolved.
Show resolved Hide resolved
values: EnumMap<MediaSourceKind, Boolean?> = this.values,
): CompletedConditions = CompletedConditions(values)

companion object {
val AllCompleted = CompletedConditions(
ImmutableEnumMap { true },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -28,9 +30,16 @@ value class MediaSelectorAutoSelect(
*
* 返回成功选择的 [Media] 对象. 当用户已经手动选择过一个别的 [Media], 或者没有可选的 [Media] 时返回 `null`.
*/
suspend fun awaitCompletedAndSelectDefault(mediaFetchSession: MediaFetchSession): Media? {
suspend fun awaitCompletedAndSelectDefault(
mediaFetchSession: MediaFetchSession,
preferKind: Flow<MediaSourceKind?> = flowOf(null)
): Media? {
// 等全部加载完成
mediaFetchSession.awaitCompletion()
mediaFetchSession.awaitCompletion { completedConditions ->
return@awaitCompletion preferKind.first()?.let {
completedConditions[it]
} ?: completedConditions.allCompleted()
}
if (mediaSelector.selected.value == null) {
val selected = mediaSelector.trySelectDefault()
return selected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -386,7 +390,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,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class MediaFetcherTest {
assertEquals(1, session.mediaSourceResults.size)
val res = session.mediaSourceResults.first()
assertIs<MediaSourceFetchState.Idle>(res.state.value)
assertEquals(false, session.hasCompleted.first())
assertEquals(false, session.hasCompleted.first().allCompleted())
}

///////////////////////////////////////////////////////////////////////////
Expand All @@ -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
Expand All @@ -113,7 +113,7 @@ class MediaFetcherTest {
assertEquals(1, session.mediaSourceResults.size)
val res = session.mediaSourceResults.first()
assertIs<MediaSourceFetchState.Idle>(res.state.value)
assertEquals(false, session.hasCompleted.first())
assertEquals(false, session.hasCompleted.first().allCompleted())
}

@Test
Expand All @@ -125,7 +125,7 @@ class MediaFetcherTest {
assertEquals(2, session.mediaSourceResults.size)
assertIs<MediaSourceFetchState.Disabled>(session.mediaSourceResults.first().state.value)
assertIs<MediaSourceFetchState.Disabled>(session.mediaSourceResults.toList()[1].state.value)
assertEquals(true, session.hasCompleted.first())
assertEquals(true, session.hasCompleted.first().allCompleted())
}

@Test
Expand All @@ -137,7 +137,7 @@ class MediaFetcherTest {
assertEquals(2, session.mediaSourceResults.size)
assertIs<MediaSourceFetchState.Disabled>(session.mediaSourceResults.first().state.value)
assertIs<MediaSourceFetchState.Idle>(session.mediaSourceResults[1].state.value)
assertEquals(false, session.hasCompleted.first())
assertEquals(false, session.hasCompleted.first().allCompleted())
}

///////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -396,7 +396,7 @@ class MediaFetcherTest {
assertIs<MediaSourceFetchState.Disabled>(session.mediaSourceResults.first().state.value)
assertIs<MediaSourceFetchState.Disabled>(session.mediaSourceResults[1].state.value)
assertEquals(0, session.awaitCompletedResults().size)
assertEquals(true, session.hasCompleted.first())
assertEquals(true, session.hasCompleted.first().allCompleted())
}

///////////////////////////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ 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 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
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.topic.Resolution
import me.him188.ani.datasources.api.topic.SubtitleLanguage.ChineseSimplified
Expand Down
Loading
Loading