This commit is contained in:
Horis 2024-01-29 16:41:51 +08:00
parent 85ea37e76d
commit 51d5c6e505
4 changed files with 140 additions and 118 deletions

View File

@ -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 == "校验超时"

View File

@ -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 {

View File

@ -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.Job
import kotlinx.coroutines.Dispatchers.IO
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() checkJob = lifecycleScope.launch(searchCoroutine) {
checkedIds.clear() flow {
allIds.addAll(ids) for (origin in ids) {
processIndex = 0 appDb.bookSourceDao.getBookSource(origin)?.let {
threadCount = min(allIds.size, threadCount) emit(it)
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, "")
}
}
}
/**
*校验书源
*/
private fun check(source: BookSource) {
execute(
context = searchCoroutine,
start = CoroutineStart.LAZY,
executeContext = IO
) {
Debug.startChecking(source)
var searchWord = CheckSource.keyword
source.ruleSearch?.checkKeyWord?.let {
if (it.isNotBlank()) {
searchWord = 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 suspend fun checkSource(source: BookSource) {
kotlin.runCatching {
Debug.startChecking(source)
source.removeInvalidGroups() source.removeInvalidGroups()
source.bookSourceComment = source.bookSourceComment source.removeErrorComment()
?.split("\n\n")
?.filterNot {
it.startsWith("// Error: ")
}?.joinToString("\n")
//校验搜索书籍 //校验搜索书籍
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() val url = source.exploreKinds().firstOrNull {
var url: String? = null !it.url.isNullOrBlank()
for (ex in exs) { }?.url
url = ex.url
if (!url.isNullOrBlank()) {
break
}
}
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) if (finalCheckMessage.isNotBlank()) {
}.timeout(CheckSource.timeout) throw NoStackTraceException(finalCheckMessage)
.onError(searchCoroutine) { }
when (it) { }.onSuccess {
is TimeoutCancellationException -> source.addGroup("校验超时") Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
is ScriptException, is WrappedException -> source.addGroup("js失效") }.onFailure {
!is NoStackTraceException -> source.addGroup("网站失效") currentCoroutineContext().ensureActive()
} when (it) {
source.bookSourceComment = is TimeoutCancellationException -> source.addGroup("校验超时")
"// Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank()) is ScriptException, is WrappedException -> source.addGroup("js失效")
"" else "\n\n${source.bookSourceComment}" !is NoStackTraceException -> source.addGroup("网站失效")
Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}") }
}.onSuccess(searchCoroutine) { source.addErrorComment(it)
Debug.updateFinalMessage(source.bookSourceUrl, "校验成功") Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
}.onFinally(IO) { }
source.respondTime = Debug.getRespondTime(source.bookSourceUrl) source.respondTime = Debug.getRespondTime(source.bookSourceUrl)
appDb.bookSourceDao.update(source)
onNext(source.bookSourceUrl, source.bookSourceName)
}.start()
} }
/** /**
@ -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) { private fun upNotification() {
synchronized(this) { notificationBuilder.setContentText(notificationMsg)
check() notificationBuilder.setProgress(originSize, finishCount, false)
checkedIds.add(sourceUrl) postEvent(EventBus.CHECK_SOURCE, notificationMsg)
notificationMsg = notificationManager.notify(NotificationId.CheckSourceService, notificationBuilder.build())
getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size)
startForegroundNotification()
if (processIndex > allIds.size + threadCount - 1) {
stopSelf()
}
}
} }
/** /**
@ -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())
} }

View File

@ -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)
}
}