Horis 2 лет назад
Родитель
Сommit
0ef03dba9a

+ 1 - 0
app/src/main/java/io/legado/app/constant/BookSourceType.kt

@@ -2,6 +2,7 @@ package io.legado.app.constant
 
 
 import androidx.annotation.IntDef
 import androidx.annotation.IntDef
 
 
+@Suppress("ConstPropertyName")
 object BookSourceType {
 object BookSourceType {
 
 
     const val default = 0           // 0 文本
     const val default = 0           // 0 文本

+ 6 - 2
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -250,8 +250,12 @@ interface BookSourceDao {
     @get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
     @get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
     val allLogin: List<BookSource>
     val allLogin: List<BookSource>
 
 
-    @get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
-    val allTextEnabled: List<BookSource>
+    @get:Query(
+        """select  bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder"""
+    )
+    val allTextEnabledPart: List<BookSourcePart>
 
 
     @get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
     @get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
     val allGroupsUnProcessed: List<String>
     val allGroupsUnProcessed: List<String>

+ 9 - 0
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -479,6 +479,15 @@ object BookHelp {
         }
         }
     }
     }
 
 
+    fun getDurChapter(
+        oldBook: Book,
+        newChapterList: List<BookChapter>
+    ): Int {
+        return oldBook.run {
+            getDurChapter(durChapterIndex, durChapterTitle, newChapterList, totalChapterNum)
+        }
+    }
+
     private val chapterNamePattern1 by lazy {
     private val chapterNamePattern1 by lazy {
         Pattern.compile(".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话]")
         Pattern.compile(".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话]")
     }
     }

+ 5 - 11
app/src/main/java/io/legado/app/model/webBook/SearchModel.kt

@@ -10,14 +10,13 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.getPrefBoolean
-import io.legado.app.utils.mapNotNullParallel
+import io.legado.app.utils.mapParallelSafe
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExecutorCoroutineDispatcher
 import kotlinx.coroutines.ExecutorCoroutineDispatcher
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.flow.buffer
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flow
@@ -83,16 +82,11 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
                 }
                 }
             }.onStart {
             }.onStart {
                 callBack.onSearchStart()
                 callBack.onSearchStart()
-            }.mapNotNullParallel(threadCount) {
-                try {
-                    withTimeout(30000L) {
-                        WebBook.searchBookAwait(it, searchKey, searchPage)
-                    }
-                } catch (e: Throwable) {
-                    currentCoroutineContext().ensureActive()
-                    null
+            }.mapParallelSafe(threadCount) {
+                withTimeout(30000L) {
+                    WebBook.searchBookAwait(it, searchKey, searchPage)
                 }
                 }
-            }.buffer(0).onEach { items ->
+            }.onEach { items ->
                 appDb.searchBookDao.insert(*items.toTypedArray())
                 appDb.searchBookDao.insert(*items.toTypedArray())
                 mergeItems(items, precision)
                 mergeItems(items, precision)
                 currentCoroutineContext().ensureActive()
                 currentCoroutineContext().ensureActive()

+ 1 - 1
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -114,7 +114,7 @@ class CheckSourceService : BaseService() {
                 upNotification()
                 upNotification()
             }.onEachParallel(threadCount) {
             }.onEachParallel(threadCount) {
                 checkSource(it)
                 checkSource(it)
-            }.buffer(0).onEach {
+            }.onEach {
                 finishCount++
                 finishCount++
                 notificationMsg = getString(
                 notificationMsg = getString(
                     R.string.progress_show,
                     R.string.progress_show,

+ 45 - 31
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -34,10 +34,19 @@ import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.utils.DocumentUtils
 import io.legado.app.utils.DocumentUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.isContentScheme
+import io.legado.app.utils.mapParallelSafe
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.toStringArray
 import io.legado.app.utils.toStringArray
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
+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.onEmpty
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.take
 import java.io.File
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileInputStream
 import java.io.FileNotFoundException
 import java.io.FileNotFoundException
@@ -266,39 +275,44 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     private fun autoChangeSource(name: String, author: String) {
     private fun autoChangeSource(name: String, author: String) {
         if (!AppConfig.autoChangeSource) return
         if (!AppConfig.autoChangeSource) return
         execute {
         execute {
-            val sources = appDb.bookSourceDao.allTextEnabled
-            sources.forEach { source ->
-                WebBook.preciseSearchAwait(this, source, name, author).getOrNull()?.let { book ->
-                    if (book.tocUrl.isEmpty()) {
-                        WebBook.getBookInfoAwait(source, book)
-                    }
-                    val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
-                    val chapter = toc.getOrElse(book.durChapterIndex) {
-                        toc.last()
-                    }
-                    val nextChapter = toc.getOrElse(chapter.index) {
-                        toc.first()
-                    }
-                    kotlin.runCatching {
-                        WebBook.getContentAwait(
-                            bookSource = source,
-                            book = book,
-                            bookChapter = chapter,
-                            nextChapterUrl = nextChapter.url
-                        )
-                        changeTo(book, toc)
-                        return@execute
+            val sources = appDb.bookSourceDao.allTextEnabledPart
+            flow {
+                for (source in sources) {
+                    source.getBookSource()?.let {
+                        emit(it)
                     }
                     }
                 }
                 }
-            }
-            throw NoStackTraceException("没有合适书源")
-        }.onStart {
-            ReadBook.upMsg(context.getString(R.string.source_auto_changing))
-        }.onError {
-            AppLog.put("自动换源失败\n${it.localizedMessage}", it)
-            context.toastOnUi("自动换源失败\n${it.localizedMessage}")
-        }.onFinally {
-            ReadBook.upMsg(null)
+            }.onStart {
+                ReadBook.upMsg(context.getString(R.string.source_auto_changing))
+            }.mapParallelSafe(AppConfig.threadCount) { source ->
+                val book = WebBook.preciseSearchAwait(this, source, name, author).getOrThrow()
+                if (book.tocUrl.isEmpty()) {
+                    WebBook.getBookInfoAwait(source, book)
+                }
+                val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
+                val chapter = toc.getOrElse(book.durChapterIndex) {
+                    toc.last()
+                }
+                val nextChapter = toc.getOrElse(chapter.index) {
+                    toc.first()
+                }
+                WebBook.getContentAwait(
+                    bookSource = source,
+                    book = book,
+                    bookChapter = chapter,
+                    nextChapterUrl = nextChapter.url
+                )
+                book to toc
+            }.take(1).onEach { (book, toc) ->
+                changeTo(book, toc)
+            }.onEmpty {
+                throw NoStackTraceException("没有合适书源")
+            }.onCompletion {
+                ReadBook.upMsg(null)
+            }.catch {
+                AppLog.put("自动换源失败\n${it.localizedMessage}", it)
+                context.toastOnUi("自动换源失败\n${it.localizedMessage}")
+            }.collect()
         }
         }
     }
     }
 
 

+ 54 - 6
app/src/main/java/io/legado/app/utils/FlowExtensions.kt

@@ -2,7 +2,11 @@ package io.legado.app.utils
 
 
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.async
 import kotlinx.coroutines.async
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.buffer
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flatMapMerge
 import kotlinx.coroutines.flow.flatMapMerge
@@ -20,13 +24,57 @@ inline fun <T> Flow<T>.onEachParallel(
         action(value)
         action(value)
         emit(value)
         emit(value)
     }
     }
-}
+}.buffer(0)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+inline fun <T> Flow<T>.onEachParallelSafe(
+    concurrency: Int,
+    crossinline action: suspend (T) -> Unit
+): Flow<T> = flatMapMerge(concurrency) { value ->
+    flow {
+        try {
+            action(value)
+        } catch (e: Throwable) {
+            currentCoroutineContext().ensureActive()
+        }
+        emit(value)
+    }
+}.buffer(0)
 
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 inline fun <T, R> Flow<T>.mapParallel(
 inline fun <T, R> Flow<T>.mapParallel(
     concurrency: Int,
     concurrency: Int,
     crossinline transform: suspend (T) -> R,
     crossinline transform: suspend (T) -> R,
-): Flow<R> = flatMapMerge(concurrency) { value -> flow { emit(transform(value)) } }
+): Flow<R> = flatMapMerge(concurrency) { value -> flow { emit(transform(value)) } }.buffer(0)
+
+
+@OptIn(ExperimentalCoroutinesApi::class)
+inline fun <T, R> Flow<T>.mapParallelSafe(
+    concurrency: Int,
+    crossinline transform: suspend (T) -> R,
+): Flow<R> = flatMapMerge(concurrency) { value ->
+    flow {
+        try {
+            emit(transform(value))
+        } catch (e: Throwable) {
+            currentCoroutineContext().ensureActive()
+        }
+    }
+}.buffer(0)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+inline fun <T, R> Flow<T>.transformParallelSafe(
+    concurrency: Int,
+    crossinline transform: suspend FlowCollector<R>.(T) -> R,
+): Flow<R> = flatMapMerge(concurrency) { value ->
+    flow {
+        try {
+            transform(value)
+        } catch (e: Throwable) {
+            currentCoroutineContext().ensureActive()
+        }
+    }
+}.buffer(0)
 
 
 inline fun <T, R> Flow<T>.mapNotNullParallel(
 inline fun <T, R> Flow<T>.mapNotNullParallel(
     concurrency: Int,
     concurrency: Int,
@@ -67,7 +115,7 @@ inline fun <T, R> Flow<T>.mapAsync(
         }.map {
         }.map {
             it.await()
             it.await()
         }.onEach { semaphore.release() }
         }.onEach { semaphore.release() }
-    }
+    }.buffer(0)
 }
 }
 
 
 inline fun <T, R> Flow<T>.mapAsyncIndexed(
 inline fun <T, R> Flow<T>.mapAsyncIndexed(
@@ -89,7 +137,7 @@ inline fun <T, R> Flow<T>.mapAsyncIndexed(
         }.map {
         }.map {
             it.await()
             it.await()
         }.onEach { semaphore.release() }
         }.onEach { semaphore.release() }
-    }
+    }.buffer(0)
 }
 }
 
 
 inline fun <T> Flow<T>.onEachAsync(
 inline fun <T> Flow<T>.onEachAsync(
@@ -110,7 +158,7 @@ inline fun <T> Flow<T>.onEachAsync(
         }.map {
         }.map {
             it.await()
             it.await()
         }.onEach { semaphore.release() }
         }.onEach { semaphore.release() }
-    }
+    }.buffer(0)
 }
 }
 
 
 inline fun <T> Flow<T>.onEachAsyncIndexed(
 inline fun <T> Flow<T>.onEachAsyncIndexed(
@@ -135,5 +183,5 @@ inline fun <T> Flow<T>.onEachAsyncIndexed(
         }.map {
         }.map {
             it.await()
             it.await()
         }.onEach { semaphore.release() }
         }.onEach { semaphore.release() }
-    }
+    }.buffer(0)
 }
 }