|
|
@@ -1,23 +1,34 @@
|
|
|
package io.legado.app.model.webBook
|
|
|
|
|
|
import io.legado.app.constant.AppConst
|
|
|
+import io.legado.app.constant.AppLog
|
|
|
import io.legado.app.constant.PreferKey
|
|
|
import io.legado.app.data.appDb
|
|
|
-import io.legado.app.data.entities.BookSource
|
|
|
+import io.legado.app.data.entities.BookSourcePart
|
|
|
import io.legado.app.data.entities.SearchBook
|
|
|
import io.legado.app.exception.NoStackTraceException
|
|
|
import io.legado.app.help.config.AppConfig
|
|
|
-import io.legado.app.help.coroutine.CompositeCoroutine
|
|
|
import io.legado.app.ui.book.search.SearchScope
|
|
|
import io.legado.app.utils.getPrefBoolean
|
|
|
+import io.legado.app.utils.mapNotNullParallel
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
-import kotlinx.coroutines.CoroutineStart
|
|
|
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
|
|
+import kotlinx.coroutines.Job
|
|
|
import kotlinx.coroutines.asCoroutineDispatcher
|
|
|
+import kotlinx.coroutines.currentCoroutineContext
|
|
|
import kotlinx.coroutines.ensureActive
|
|
|
-import kotlinx.coroutines.isActive
|
|
|
+import kotlinx.coroutines.flow.buffer
|
|
|
+import kotlinx.coroutines.flow.catch
|
|
|
+import kotlinx.coroutines.flow.collect
|
|
|
+import kotlinx.coroutines.flow.flow
|
|
|
+import kotlinx.coroutines.flow.onCompletion
|
|
|
+import kotlinx.coroutines.flow.onEach
|
|
|
+import kotlinx.coroutines.flow.onStart
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import kotlinx.coroutines.withTimeout
|
|
|
import splitties.init.appCtx
|
|
|
import java.util.concurrent.Executors
|
|
|
+import kotlin.coroutines.coroutineContext
|
|
|
import kotlin.math.min
|
|
|
|
|
|
class SearchModel(private val scope: CoroutineScope, private val callBack: CallBack) {
|
|
|
@@ -26,12 +37,10 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
private var mSearchId = 0L
|
|
|
private var searchPage = 1
|
|
|
private var searchKey: String = ""
|
|
|
- private var tasks = CompositeCoroutine()
|
|
|
- private var bookSourceList = arrayListOf<BookSource>()
|
|
|
+ private var bookSourceParts = emptyList<BookSourcePart>()
|
|
|
private var searchBooks = arrayListOf<SearchBook>()
|
|
|
+ private var searchJob: Job? = null
|
|
|
|
|
|
- @Volatile
|
|
|
- private var searchIndex = -1
|
|
|
|
|
|
private fun initSearchPool() {
|
|
|
searchPool?.close()
|
|
|
@@ -40,97 +49,70 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
}
|
|
|
|
|
|
fun search(searchId: Long, key: String) {
|
|
|
- callBack.onSearchStart()
|
|
|
if (searchId != mSearchId) {
|
|
|
if (key.isEmpty()) {
|
|
|
- callBack.onSearchCancel()
|
|
|
return
|
|
|
- } else {
|
|
|
- this.searchKey = key
|
|
|
}
|
|
|
+ searchKey = key
|
|
|
if (mSearchId != 0L) {
|
|
|
close()
|
|
|
}
|
|
|
- initSearchPool()
|
|
|
- mSearchId = searchId
|
|
|
- searchPage = 1
|
|
|
- bookSourceList.clear()
|
|
|
searchBooks.clear()
|
|
|
- callBack.onSearchSuccess(searchBooks)
|
|
|
- bookSourceList.addAll(callBack.getSearchScope().getBookSources())
|
|
|
- if (bookSourceList.isEmpty()) {
|
|
|
+ bookSourceParts = callBack.getSearchScope().getBookSourceParts()
|
|
|
+ if (bookSourceParts.isEmpty()) {
|
|
|
callBack.onSearchCancel(NoStackTraceException("启用书源为空"))
|
|
|
return
|
|
|
}
|
|
|
+ mSearchId = searchId
|
|
|
+ searchPage = 1
|
|
|
+ initSearchPool()
|
|
|
} else {
|
|
|
searchPage++
|
|
|
}
|
|
|
- searchIndex = -1
|
|
|
- for (i in 0 until threadCount) {
|
|
|
- search(searchId)
|
|
|
- }
|
|
|
+ startSearch()
|
|
|
}
|
|
|
|
|
|
- @Synchronized
|
|
|
- private fun search(searchId: Long) {
|
|
|
- if (searchIndex >= bookSourceList.lastIndex) {
|
|
|
- return
|
|
|
- }
|
|
|
- searchIndex++
|
|
|
- val source = bookSourceList.getOrNull(searchIndex) ?: return
|
|
|
- val searchPool = searchPool ?: return
|
|
|
- val task = WebBook.searchBook(
|
|
|
- scope,
|
|
|
- source,
|
|
|
- searchKey,
|
|
|
- searchPage,
|
|
|
- context = searchPool,
|
|
|
- start = CoroutineStart.LAZY,
|
|
|
- executeContext = searchPool
|
|
|
- ).timeout(30000L)
|
|
|
- .onSuccess {
|
|
|
- ensureActive()
|
|
|
- onSuccess(searchId, it)
|
|
|
- }
|
|
|
- .onFinally {
|
|
|
- onFinally(searchId)
|
|
|
- }
|
|
|
- task.start()
|
|
|
- tasks.add(task)
|
|
|
- }
|
|
|
-
|
|
|
- @Synchronized
|
|
|
- private fun onSuccess(searchId: Long, items: ArrayList<SearchBook>) {
|
|
|
- if (searchId == mSearchId) {
|
|
|
- appDb.searchBookDao.insert(*items.toTypedArray())
|
|
|
- val precision = appCtx.getPrefBoolean(PreferKey.precisionSearch)
|
|
|
- mergeItems(scope, items, precision)
|
|
|
- callBack.onSearchSuccess(searchBooks)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Synchronized
|
|
|
- private fun onFinally(searchId: Long) {
|
|
|
- if (searchIndex < bookSourceList.lastIndex) {
|
|
|
- search(searchId)
|
|
|
- } else {
|
|
|
- searchIndex++
|
|
|
- }
|
|
|
- if (searchIndex >= bookSourceList.lastIndex
|
|
|
- + min(bookSourceList.size, threadCount)
|
|
|
- ) {
|
|
|
- callBack.onSearchFinish(searchBooks.isEmpty())
|
|
|
+ private fun startSearch() {
|
|
|
+ val precision = appCtx.getPrefBoolean(PreferKey.precisionSearch)
|
|
|
+ searchJob = scope.launch(searchPool!!) {
|
|
|
+ flow {
|
|
|
+ for (bs in bookSourceParts) {
|
|
|
+ bs.getBookSource()?.let {
|
|
|
+ emit(it)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }.onStart {
|
|
|
+ callBack.onSearchStart()
|
|
|
+ }.mapNotNullParallel(threadCount) {
|
|
|
+ try {
|
|
|
+ withTimeout(30000L) {
|
|
|
+ WebBook.searchBookAwait(it, searchKey, searchPage)
|
|
|
+ }
|
|
|
+ } catch (e: Throwable) {
|
|
|
+ currentCoroutineContext().ensureActive()
|
|
|
+ null
|
|
|
+ }
|
|
|
+ }.buffer(0).onEach { items ->
|
|
|
+ appDb.searchBookDao.insert(*items.toTypedArray())
|
|
|
+ mergeItems(items, precision)
|
|
|
+ currentCoroutineContext().ensureActive()
|
|
|
+ callBack.onSearchSuccess(searchBooks)
|
|
|
+ }.onCompletion {
|
|
|
+ if (it == null) callBack.onSearchFinish(searchBooks.isEmpty())
|
|
|
+ }.catch {
|
|
|
+ AppLog.put("书源搜索出错\n${it.localizedMessage}", it)
|
|
|
+ }.collect()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private fun mergeItems(scope: CoroutineScope, newDataS: List<SearchBook>, precision: Boolean) {
|
|
|
+ private suspend fun mergeItems(newDataS: List<SearchBook>, precision: Boolean) {
|
|
|
if (newDataS.isNotEmpty()) {
|
|
|
val copyData = ArrayList(searchBooks)
|
|
|
val equalData = arrayListOf<SearchBook>()
|
|
|
val containsData = arrayListOf<SearchBook>()
|
|
|
val otherData = arrayListOf<SearchBook>()
|
|
|
copyData.forEach {
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
if (it.name == searchKey || it.author == searchKey) {
|
|
|
equalData.add(it)
|
|
|
} else if (it.name.contains(searchKey) || it.author.contains(searchKey)) {
|
|
|
@@ -140,11 +122,11 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
}
|
|
|
}
|
|
|
newDataS.forEach { nBook ->
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
if (nBook.name == searchKey || nBook.author == searchKey) {
|
|
|
var hasSame = false
|
|
|
equalData.forEach { pBook ->
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
|
|
pBook.addOrigin(nBook.origin)
|
|
|
hasSame = true
|
|
|
@@ -156,7 +138,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
} else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) {
|
|
|
var hasSame = false
|
|
|
containsData.forEach { pBook ->
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
|
|
pBook.addOrigin(nBook.origin)
|
|
|
hasSame = true
|
|
|
@@ -168,7 +150,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
} else if (!precision) {
|
|
|
var hasSame = false
|
|
|
otherData.forEach { pBook ->
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
|
|
pBook.addOrigin(nBook.origin)
|
|
|
hasSame = true
|
|
|
@@ -179,13 +161,13 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
equalData.sortByDescending { it.origins.size }
|
|
|
equalData.addAll(containsData.sortedByDescending { it.origins.size })
|
|
|
if (!precision) {
|
|
|
equalData.addAll(otherData)
|
|
|
}
|
|
|
- if (!scope.isActive) return
|
|
|
+ coroutineContext.ensureActive()
|
|
|
searchBooks = equalData
|
|
|
}
|
|
|
}
|
|
|
@@ -196,7 +178,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
}
|
|
|
|
|
|
fun close() {
|
|
|
- tasks.clear()
|
|
|
+ searchJob?.cancel()
|
|
|
searchPool?.close()
|
|
|
searchPool = null
|
|
|
mSearchId = 0L
|
|
|
@@ -205,9 +187,9 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
|
|
interface CallBack {
|
|
|
fun getSearchScope(): SearchScope
|
|
|
fun onSearchStart()
|
|
|
- fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
|
|
|
+ fun onSearchSuccess(searchBooks: List<SearchBook>)
|
|
|
fun onSearchFinish(isEmpty: Boolean)
|
|
|
- fun onSearchCancel(exception: Exception? = null)
|
|
|
+ fun onSearchCancel(exception: Throwable? = null)
|
|
|
}
|
|
|
|
|
|
}
|