Horis 2 ani în urmă
părinte
comite
e26c191eaf

+ 23 - 0
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -191,6 +191,29 @@ data class BookSource(
         removeGroup(getInvalidGroupNames())
         removeGroup(getInvalidGroupNames())
     }
     }
 
 
+    fun removeErrorComment() {
+        bookSourceComment = bookSourceComment
+            ?.split("\n\n")
+            ?.filterNot {
+                it.startsWith("// Error: ")
+            }?.joinToString("\n")
+    }
+
+    fun addErrorComment(e: Throwable) {
+        bookSourceComment =
+            "// Error: ${e.localizedMessage}" + if (bookSourceComment.isNullOrBlank())
+                "" else "\n\n${bookSourceComment}"
+    }
+
+    fun getCheckKeyword(default: String): String {
+        ruleSearch?.checkKeyWord?.let {
+            if (it.isNotBlank()) {
+                return it
+            }
+        }
+        return default
+    }
+
     fun getInvalidGroupNames(): String {
     fun getInvalidGroupNames(): String {
         return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter {
         return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter {
             "失效" in it || it == "校验超时"
             "失效" in it || it == "校验超时"

+ 1 - 0
app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt

@@ -70,6 +70,7 @@ data class ReplaceRule(
         return id.hashCode()
         return id.hashCode()
     }
     }
 
 
+    @delegate:Transient
     @delegate:Ignore
     @delegate:Ignore
     @IgnoredOnParcel
     @IgnoredOnParcel
     val regex: Regex by lazy {
     val regex: Regex by lazy {

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

@@ -25,16 +25,23 @@ import io.legado.app.model.Debug
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.utils.activityPendingIntent
 import io.legado.app.utils.activityPendingIntent
+import io.legado.app.utils.onEachParallel
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import org.mozilla.javascript.WrappedException
 import org.mozilla.javascript.WrappedException
 import splitties.init.appCtx
 import splitties.init.appCtx
+import splitties.systemservices.notificationManager
 import java.util.concurrent.Executors
 import java.util.concurrent.Executors
 import kotlin.math.min
 import kotlin.math.min
 
 
@@ -45,10 +52,10 @@ class CheckSourceService : BaseService() {
     private var threadCount = AppConfig.threadCount
     private var threadCount = AppConfig.threadCount
     private var searchCoroutine =
     private var searchCoroutine =
         Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
         Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
-    private val allIds = ArrayList<String>()
-    private val checkedIds = ArrayList<String>()
-    private var processIndex = 0
     private var notificationMsg = appCtx.getString(R.string.service_starting)
     private var notificationMsg = appCtx.getString(R.string.service_starting)
+    private var checkJob: Job? = null
+    private var originSize = 0
+    private var finishCount = 0
 
 
     private val notificationBuilder by lazy {
     private val notificationBuilder by lazy {
         NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
         NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
@@ -86,64 +93,48 @@ class CheckSourceService : BaseService() {
     }
     }
 
 
     private fun check(ids: List<String>) {
     private fun check(ids: List<String>) {
-        if (allIds.isNotEmpty()) {
+        if (checkJob?.isActive == true) {
             toastOnUi("已有书源在校验,等完成后再试")
             toastOnUi("已有书源在校验,等完成后再试")
             return
             return
         }
         }
-        allIds.clear()
-        checkedIds.clear()
-        allIds.addAll(ids)
-        processIndex = 0
-        threadCount = min(allIds.size, threadCount)
-        notificationMsg = getString(R.string.progress_show, "", 0, allIds.size)
-        startForegroundNotification()
-        for (i in 0 until threadCount) {
-            check()
-        }
-    }
-
-    /**
-     * 检测
-     */
-    private fun check() {
-        val index = processIndex
-        synchronized(this) {
-            processIndex++
-        }
-        lifecycleScope.launch(IO) {
-            if (index < allIds.size) {
-                val sourceUrl = allIds[index]
-                appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source ->
-                    check(source)
-                } ?: onNext(sourceUrl, "")
+        checkJob = lifecycleScope.launch(searchCoroutine) {
+            flow {
+                for (origin in ids) {
+                    appDb.bookSourceDao.getBookSource(origin)?.let {
+                        emit(it)
+                    }
+                }
+            }.onStart {
+                originSize = ids.size
+                finishCount = 0
+                notificationMsg = getString(R.string.progress_show, "", 0, originSize)
+                upNotification()
+            }.onEachParallel(threadCount) {
+                checkSource(it)
+            }.onCompletion {
+                stopSelf()
+            }.buffer(0).collect {
+                finishCount++
+                notificationMsg = getString(
+                    R.string.progress_show,
+                    it.bookSourceName,
+                    finishCount,
+                    originSize
+                )
+                upNotification()
+                appDb.bookSourceDao.update(it)
             }
             }
         }
         }
     }
     }
 
 
-    /**
-     *校验书源
-     */
-    private fun check(source: BookSource) {
-        execute(
-            context = searchCoroutine,
-            start = CoroutineStart.LAZY,
-            executeContext = IO
-        ) {
+    private suspend fun checkSource(source: BookSource) {
+        kotlin.runCatching {
             Debug.startChecking(source)
             Debug.startChecking(source)
-            var searchWord = CheckSource.keyword
-            source.ruleSearch?.checkKeyWord?.let {
-                if (it.isNotBlank()) {
-                    searchWord = it
-                }
-            }
             source.removeInvalidGroups()
             source.removeInvalidGroups()
-            source.bookSourceComment = source.bookSourceComment
-                ?.split("\n\n")
-                ?.filterNot {
-                    it.startsWith("// Error: ")
-                }?.joinToString("\n")
+            source.removeErrorComment()
             //校验搜索书籍
             //校验搜索书籍
             if (CheckSource.checkSearch) {
             if (CheckSource.checkSearch) {
+                val searchWord = source.getCheckKeyword(CheckSource.keyword)
                 if (!source.searchUrl.isNullOrBlank()) {
                 if (!source.searchUrl.isNullOrBlank()) {
                     source.removeGroup("搜索链接规则为空")
                     source.removeGroup("搜索链接规则为空")
                     val searchBooks = WebBook.searchBookAwait(source, searchWord)
                     val searchBooks = WebBook.searchBookAwait(source, searchWord)
@@ -159,14 +150,9 @@ class CheckSourceService : BaseService() {
             }
             }
             //校验发现书籍
             //校验发现书籍
             if (CheckSource.checkDiscovery && !source.exploreUrl.isNullOrBlank()) {
             if (CheckSource.checkDiscovery && !source.exploreUrl.isNullOrBlank()) {
-                val exs = source.exploreKinds()
-                var url: String? = null
-                for (ex in exs) {
-                    url = ex.url
-                    if (!url.isNullOrBlank()) {
-                        break
-                    }
-                }
+                val url = source.exploreKinds().firstOrNull {
+                    !it.url.isNullOrBlank()
+                }?.url
                 if (url.isNullOrBlank()) {
                 if (url.isNullOrBlank()) {
                     source.addGroup("发现规则为空")
                     source.addGroup("发现规则为空")
                 } else {
                 } else {
@@ -181,25 +167,22 @@ class CheckSourceService : BaseService() {
                 }
                 }
             }
             }
             val finalCheckMessage = source.getInvalidGroupNames()
             val finalCheckMessage = source.getInvalidGroupNames()
-            if (finalCheckMessage.isNotBlank()) throw NoStackTraceException(finalCheckMessage)
-        }.timeout(CheckSource.timeout)
-            .onError(searchCoroutine) {
-                when (it) {
-                    is TimeoutCancellationException -> source.addGroup("校验超时")
-                    is ScriptException, is WrappedException -> source.addGroup("js失效")
-                    !is NoStackTraceException -> source.addGroup("网站失效")
-                }
-                source.bookSourceComment =
-                    "// Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank())
-                        "" else "\n\n${source.bookSourceComment}"
-                Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
-            }.onSuccess(searchCoroutine) {
-                Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
-            }.onFinally(IO) {
-                source.respondTime = Debug.getRespondTime(source.bookSourceUrl)
-                appDb.bookSourceDao.update(source)
-                onNext(source.bookSourceUrl, source.bookSourceName)
-            }.start()
+            if (finalCheckMessage.isNotBlank()) {
+                throw NoStackTraceException(finalCheckMessage)
+            }
+        }.onSuccess {
+            Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
+        }.onFailure {
+            currentCoroutineContext().ensureActive()
+            when (it) {
+                is TimeoutCancellationException -> source.addGroup("校验超时")
+                is ScriptException, is WrappedException -> source.addGroup("js失效")
+                !is NoStackTraceException -> source.addGroup("网站失效")
+            }
+            source.addErrorComment(it)
+            Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
+        }
+        source.respondTime = Debug.getRespondTime(source.bookSourceUrl)
     }
     }
 
 
     /**
     /**
@@ -207,31 +190,33 @@ class CheckSourceService : BaseService() {
      */
      */
     private suspend fun checkBook(book: Book, source: BookSource, isSearchBook: Boolean = true) {
     private suspend fun checkBook(book: Book, source: BookSource, isSearchBook: Boolean = true) {
         kotlin.runCatching {
         kotlin.runCatching {
-            var mBook = book
+            if (!CheckSource.checkInfo) {
+                return
+            }
             //校验详情
             //校验详情
-            if (CheckSource.checkInfo) {
-                if (mBook.tocUrl.isBlank()) {
-                    mBook = WebBook.getBookInfoAwait(source, mBook)
-                }
-                //校验目录
-                if (CheckSource.checkCategory &&
-                    source.bookSourceType != BookSourceType.file
-                ) {
-                    val toc = WebBook.getChapterListAwait(source, mBook).getOrThrow()
-                        .filter { !(it.isVolume && it.url.startsWith(it.title)) }
-                    val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
-                    //校验正文
-                    if (CheckSource.checkContent) {
-                        WebBook.getContentAwait(
-                            bookSource = source,
-                            book = mBook,
-                            bookChapter = toc.first(),
-                            nextChapterUrl = nextChapterUrl,
-                            needSave = false
-                        )
-                    }
-                }
+            if (book.tocUrl.isBlank()) {
+                WebBook.getBookInfoAwait(source, book)
             }
             }
+            if (!CheckSource.checkCategory || source.bookSourceType == BookSourceType.file) {
+                return
+            }
+            //校验目录
+            val toc = WebBook.getChapterListAwait(source, book).getOrThrow().asSequence()
+                .filter { !(it.isVolume && it.url.startsWith(it.title)) }
+                .take(2)
+                .toList()
+            val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
+            if (!CheckSource.checkContent) {
+                return
+            }
+            //校验正文
+            WebBook.getContentAwait(
+                bookSource = source,
+                book = book,
+                bookChapter = toc.first(),
+                nextChapterUrl = nextChapterUrl,
+                needSave = false
+            )
         }.onFailure {
         }.onFailure {
             val bookType = if (isSearchBook) "搜索" else "发现"
             val bookType = if (isSearchBook) "搜索" else "发现"
             when (it) {
             when (it) {
@@ -246,17 +231,11 @@ class CheckSourceService : BaseService() {
         }
         }
     }
     }
 
 
-    private fun onNext(sourceUrl: String, sourceName: String) {
-        synchronized(this) {
-            check()
-            checkedIds.add(sourceUrl)
-            notificationMsg =
-                getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size)
-            startForegroundNotification()
-            if (processIndex > allIds.size + threadCount - 1) {
-                stopSelf()
-            }
-        }
+    private fun upNotification() {
+        notificationBuilder.setContentText(notificationMsg)
+        notificationBuilder.setProgress(originSize, finishCount, false)
+        postEvent(EventBus.CHECK_SOURCE, notificationMsg)
+        notificationManager.notify(NotificationId.CheckSourceService, notificationBuilder.build())
     }
     }
 
 
     /**
     /**
@@ -264,7 +243,7 @@ class CheckSourceService : BaseService() {
      */
      */
     override fun startForegroundNotification() {
     override fun startForegroundNotification() {
         notificationBuilder.setContentText(notificationMsg)
         notificationBuilder.setContentText(notificationMsg)
-        notificationBuilder.setProgress(allIds.size, checkedIds.size, false)
+        notificationBuilder.setProgress(originSize, finishCount, false)
         postEvent(EventBus.CHECK_SOURCE, notificationMsg)
         postEvent(EventBus.CHECK_SOURCE, notificationMsg)
         startForeground(NotificationId.CheckSourceService, notificationBuilder.build())
         startForeground(NotificationId.CheckSourceService, notificationBuilder.build())
     }
     }

+ 19 - 0
app/src/main/java/io/legado/app/utils/FlowExtensions.kt

@@ -0,0 +1,19 @@
+package io.legado.app.utils
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.DEFAULT_CONCURRENCY
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapMerge
+import kotlinx.coroutines.flow.flow
+
+@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
+inline fun <T> Flow<T>.onEachParallel(
+    concurrency: Int = DEFAULT_CONCURRENCY,
+    crossinline action: suspend (T) -> Unit
+): Flow<T> = flatMapMerge(concurrency) { value ->
+    return@flatMapMerge flow {
+        action(value)
+        emit(value)
+    }
+}