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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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,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 ->
Comment on lines 343 to +346
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目前这样改会为那些原本总是期待所有数据源加载完成的代码增加计算量 (计算是否完成并构造 EnumMap 等), 导致性能变差

这是可以避免的, 我觉得可能要做一个另外的属性来支持中间状态

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
Comment on lines +350 to +352
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为什么要有 null? 不能用 false

}
}.onStart {
if (stateList.isEmpty()) emit(kind to null)
}
}

combine(map) { pairs ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

目前两个 combine 的算法会导致启动数倍多的协程

用一个 combine 就足够解决问题了, 让 combine 的 lambda 返回 ImmutableEnumMap<MSK, Boolean>

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(
Comment on lines +382 to +383
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@JvmInline
value class CompletedConditions(
class CompletedConditions(

这个东西看起来会用在被 box 的地方, 那其实不需要 value

val values: EnumMap<MediaSourceKind, Boolean?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it private

) {
fun allCompleted() = values.values.all { it ?: true }

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

fun copy(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

未使用, 没必要有

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