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 all 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,6 +47,8 @@ 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

Expand Down Expand Up @@ -338,10 +340,23 @@ 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 }
combine(mediaSourceResults.map { it.state }) {
val pairs = mediaSourceResults.groupBy { it.kind }.mapValues { results ->
val states = results.value.map { it.state }
when {
// 该类型数据源全部禁用时返回 null,如果返回 false 会导致 awaitCompletion 无法结束
states.all { it.value is MediaSourceFetchState.Disabled } -> null
states.all { it.value is MediaSourceFetchState.Completed || it.value is MediaSourceFetchState.Disabled } -> true
else -> false
}
}
CompletedConditions(
ImmutableEnumMap<MediaSourceKind, _> { kind ->
pairs[kind]
},
)
}.flowOn(flowContext)
}
}
Expand All @@ -358,3 +373,21 @@ class MediaSourceMediaFetcher(
private const val ENABLE_WATCHDOG = false
}
}

class CompletedConditions(
private val values: EnumMap<MediaSourceKind, Boolean?>
) {
fun allCompleted() = values.values.all { it ?: true }

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

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