Просмотр исходного кода

搜索问题 (#990)

* Filter to searchable book sources before running search

* Track first failure details when all search sources fail

* Align precision matching and parallelize source loading
Kudomaga 6 месяцев назад
Родитель
Сommit
103bdc07d1

+ 49 - 11
app/src/main/java/io/legado/app/domain/usecase/SearchBooksUseCase.kt

@@ -11,6 +11,9 @@ import io.legado.app.model.webBook.WebBook
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.Flow
@@ -81,18 +84,35 @@ class SearchBooksUseCase(
             throw NoStackTraceException("启用书源为空")
         }
 
+        val searchableSources = coroutineScope {
+            sourceParts.map { part ->
+                async(Dispatchers.IO) {
+                    val source = gateway.getBookSource(part.bookSourceUrl) ?: return@async null
+                    if (source.bookSourceType != 0 || source.searchUrl.isNullOrBlank()) {
+                        return@async null
+                    }
+                    SearchableSource(part, source)
+                }
+            }.awaitAll().filterNotNull()
+        }
+        if (searchableSources.isEmpty()) {
+            throw NoStackTraceException("可搜索书源为空")
+        }
+
         val merger = SearchResultMerger(keyword, request.precision)
         val concurrency = request.concurrency.coerceAtLeast(1)
         var hasMore = false
         var processedSources = 0
+        var failedSources = 0
+        var firstFailureMessage: String? = null
 
         emit(SearchRunEvent.Started)
 
-        sourceParts.asFlow()
-            .flatMapMerge(concurrency) { sourcePart ->
+        searchableSources.asFlow()
+            .flatMapMerge(concurrency) { searchableSource ->
                 flow {
                     control.awaitResumed()
-                    emit(searchSource(sourcePart, keyword, request.page, request.precision))
+                    emit(searchSource(searchableSource, keyword, request.page, request.precision))
                 }.flowOn(Dispatchers.IO)
             }
             .collect { result ->
@@ -112,12 +132,16 @@ class SearchBooksUseCase(
                                 removedBookUrls = change.removedBookUrls,
                                 resultCount = merger.count,
                                 processedSources = processedSources,
-                                totalSources = sourceParts.size,
+                                totalSources = searchableSources.size,
                             )
                         )
                     }
 
                     is SourceSearchResult.Failed -> {
+                        failedSources++
+                        if (firstFailureMessage.isNullOrBlank()) {
+                            firstFailureMessage = result.throwable.localizedMessage
+                        }
                         AppLog.put("书源搜索出错\n${result.throwable.localizedMessage}", result.throwable)
                         emit(
                             SearchRunEvent.Progress(
@@ -125,13 +149,18 @@ class SearchBooksUseCase(
                                 removedBookUrls = emptyList(),
                                 resultCount = merger.count,
                                 processedSources = processedSources,
-                                totalSources = sourceParts.size,
+                                totalSources = searchableSources.size,
                             )
                         )
                     }
                 }
             }
 
+        if (merger.count == 0 && failedSources == searchableSources.size) {
+            val error = firstFailureMessage?.takeIf { it.isNotBlank() } ?: "全部书源搜索失败"
+            throw NoStackTraceException(error)
+        }
+
         emit(
             SearchRunEvent.Finished(
                 isEmpty = merger.count == 0,
@@ -141,14 +170,13 @@ class SearchBooksUseCase(
     }.flowOn(Dispatchers.IO)
 
     private suspend fun searchSource(
-        sourcePart: BookSourcePart,
+        searchableSource: SearchableSource,
         keyword: String,
         page: Int,
         precision: Boolean,
     ): SourceSearchResult {
         return try {
-            val source = gateway.getBookSource(sourcePart.bookSourceUrl)
-                ?: return SourceSearchResult.Found(emptyList())
+            val source = searchableSource.source
             val supportsSearchPage = source.supportsSearchPage()
             if (page > 1 && !supportsSearchPage) {
                 return SourceSearchResult.Found(emptyList())
@@ -159,7 +187,9 @@ class SearchBooksUseCase(
                     keyword,
                     page,
                     filter = { name, author ->
-                        !precision || name.contains(keyword) || author.contains(keyword)
+                        !precision ||
+                            name.contains(keyword, ignoreCase = true) ||
+                            author.contains(keyword, ignoreCase = true)
                     }
                 )
             }
@@ -171,6 +201,12 @@ class SearchBooksUseCase(
         }
     }
 
+
+
+    private data class SearchableSource(
+        val part: BookSourcePart,
+        val source: BookSource,
+    )
     private sealed interface SourceSearchResult {
         data class Found(
             val books: List<SearchBook>,
@@ -224,8 +260,10 @@ class SearchBooksUseCase(
 
         private fun classifyBucket(book: SearchBook): LinkedHashMap<SearchBookKey, SearchBook>? {
             return when {
-                book.name == keyword || book.author == keyword -> equalBooks
-                book.name.contains(keyword) || book.author.contains(keyword) -> containsBooks
+                book.name.equals(keyword, ignoreCase = true) ||
+                    book.author.equals(keyword, ignoreCase = true) -> equalBooks
+                book.name.contains(keyword, ignoreCase = true) ||
+                    book.author.contains(keyword, ignoreCase = true) -> containsBooks
                 !precision -> otherBooks
                 else -> null
             }

+ 9 - 3
app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt

@@ -95,7 +95,6 @@ import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
 
 @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
 @Composable
@@ -110,6 +109,7 @@ fun SearchScreen(
     val listState = rememberLazyListState()
     var queryInput by rememberSaveable { mutableStateOf(state.query) }
     var scopeSheetTab by rememberSaveable { mutableStateOf(0) }
+    var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
     val showSuggestionPanel = state.showSuggestions
     val latestQuery by rememberUpdatedState(state.query)
     val scrollBehavior = if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
@@ -142,9 +142,14 @@ fun SearchScreen(
         snapshotFlow { queryInput }
             .distinctUntilChanged()
             .debounce(200)
-            .filter { it != latestQuery }
             .collect { newQuery ->
-                viewModel.onIntent(SearchIntent.UpdateQuery(newQuery))
+                if (ignoreNextDebouncedQuery == newQuery) {
+                    ignoreNextDebouncedQuery = null
+                    return@collect
+                }
+                if (newQuery != latestQuery) {
+                    viewModel.onIntent(SearchIntent.UpdateQuery(newQuery))
+                }
             }
     }
 
@@ -188,6 +193,7 @@ fun SearchScreen(
     val submitSearch: (String) -> Unit = { rawQuery ->
         val normalized = rawQuery.trim()
         if (normalized.isNotBlank()) {
+            ignoreNextDebouncedQuery = normalized
             queryInput = normalized
             if (normalized != state.query) {
                 viewModel.onIntent(SearchIntent.UpdateQuery(normalized))

+ 7 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt

@@ -234,7 +234,13 @@ class SearchViewModel(
     }
 
     private fun updateQuery(query: String, showSuggestions: Boolean) {
-        if (showSuggestions && _uiState.value.isSearching) {
+        val currentState = _uiState.value
+        val isSameQuery = currentState.query == query
+        val sameUiFlag = currentState.showSuggestions == showSuggestions
+        if (isSameQuery && sameUiFlag) {
+            return
+        }
+        if (showSuggestions && currentState.isSearching && !isSameQuery) {
             stopSearch(manualStop = false)
         }
         queryFlow.value = query