mirror of
https://github.com/gedoor/legado.git
synced 2024-07-17 00:58:29 +08:00
优化
This commit is contained in:
parent
85ea37e76d
commit
51d5c6e505
@ -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 == "校验超时"
|
||||||
|
@ -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 {
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
19
app/src/main/java/io/legado/app/utils/FlowExtensions.kt
Normal file
19
app/src/main/java/io/legado/app/utils/FlowExtensions.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user