This commit is contained in:
Horis
2024-01-31 18:13:20 +08:00
parent 49fde7164d
commit db5fdf2603
4 changed files with 187 additions and 97 deletions
@@ -10,6 +10,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.ArchiveUtils import io.legado.app.utils.ArchiveUtils
@@ -24,12 +25,13 @@ import io.legado.app.utils.exists
import io.legado.app.utils.externalFiles import io.legado.app.utils.externalFiles
import io.legado.app.utils.getFile import io.legado.app.utils.getFile
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.onEachParallel
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.JaccardSimilarity import org.apache.commons.text.similarity.JaccardSimilarity
import splitties.init.appCtx import splitties.init.appCtx
@@ -151,19 +153,16 @@ object BookHelp {
bookChapter: BookChapter, bookChapter: BookChapter,
content: String content: String
) = coroutineScope { ) = coroutineScope {
val awaitList = arrayListOf<Deferred<Unit>>() flow {
val matcher = AppPattern.imgPattern.matcher(content) val matcher = AppPattern.imgPattern.matcher(content)
while (matcher.find()) { while (matcher.find()) {
matcher.group(1)?.let { src -> val src = matcher.group(1) ?: continue
val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src) val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src)
awaitList.add(async { emit(mSrc)
saveImage(bookSource, book, mSrc, bookChapter)
})
} }
} }.onEachParallel(AppConfig.threadCount) { mSrc ->
awaitList.forEach { saveImage(bookSource, book, mSrc, bookChapter)
it.await() }.collect()
}
} }
suspend fun saveImage( suspend fun saveImage(
@@ -11,13 +11,15 @@ import io.legado.app.data.entities.rule.TocRule
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.exception.TocEmptyException import io.legado.app.exception.TocEmptyException
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.config.AppConfig
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.isTrue import io.legado.app.utils.isTrue
import io.legado.app.utils.mapAsync
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
@@ -87,23 +89,23 @@ object BookChapterList {
"◇并发解析目录,总页数:${chapterData.second.size}" "◇并发解析目录,总页数:${chapterData.second.size}"
) )
withContext(IO) { withContext(IO) {
val asyncArray = Array(chapterData.second.size) { flow {
async(IO) { for (urlStr in chapterData.second) {
val urlStr = chapterData.second[it] emit(urlStr)
val res = AnalyzeUrl(
mUrl = urlStr,
source = bookSource,
ruleData = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponseAwait() //控制并发访问
analyzeChapterList(
book, urlStr, res.url,
res.body!!, tocRule, listRule, bookSource, false
).first
} }
} }.mapAsync(AppConfig.threadCount) { urlStr ->
asyncArray.forEach { coroutine -> val res = AnalyzeUrl(
chapterList.addAll(coroutine.await()) mUrl = urlStr,
source = bookSource,
ruleData = book,
headerMapF = bookSource.getHeaderMap()
).getStrResponseAwait() //控制并发访问
analyzeChapterList(
book, urlStr, res.url,
res.body!!, tocRule, listRule, bookSource, false
).first
}.collect {
chapterList.addAll(it)
} }
} }
} }
@@ -142,7 +144,8 @@ object BookChapterList {
} }
} }
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules() val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
book.latestChapterTitle = list.last().getDisplayTitle(replaceRules, book.getUseReplaceRule()) book.latestChapterTitle =
list.last().getDisplayTitle(replaceRules, book.getUseReplaceRule())
book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() } book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
.getDisplayTitle(replaceRules, book.getUseReplaceRule()) .getDisplayTitle(replaceRules, book.getUseReplaceRule())
if (book.totalChapterNum < list.size) { if (book.totalChapterNum < list.size) {
@@ -42,6 +42,8 @@ import io.legado.app.utils.activityPendingIntent
import io.legado.app.utils.cnCompare import io.legado.app.utils.cnCompare
import io.legado.app.utils.createFolderIfNotExist import io.legado.app.utils.createFolderIfNotExist
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.mapAsync
import io.legado.app.utils.mapAsyncIndexed
import io.legado.app.utils.outputStream import io.legado.app.utils.outputStream
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.readBytes import io.legado.app.utils.readBytes
@@ -49,20 +51,14 @@ import io.legado.app.utils.readText
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 io.legado.app.utils.writeBytes import io.legado.app.utils.writeBytes
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ag2s.epublib.domain.Author import me.ag2s.epublib.domain.Author
import me.ag2s.epublib.domain.Date import me.ag2s.epublib.domain.Date
@@ -327,23 +323,19 @@ class ExportBookService : BaseService() {
val threads = if (AppConfig.parallelExportBook) { val threads = if (AppConfig.parallelExportBook) {
AppConst.MAX_THREAD AppConst.MAX_THREAD
} else { } else {
0 1
} }
flow { flow {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
val task = async(Default, start = CoroutineStart.LAZY) { emit(chapter)
getExportData(book, chapter, contentProcessor, useReplace)
}
emit(task)
}
}.onEach { it.start() }
.buffer(threads)
.map { it.await() }
.collectIndexed { index, result ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
append.invoke(result.first, result.second)
} }
}.mapAsync(threads) { chapter ->
getExportData(book, chapter, contentProcessor, useReplace)
}.collectIndexed { index, result ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
append.invoke(result.first, result.second)
}
} }
@@ -641,56 +633,52 @@ class ExportBookService : BaseService() {
val threads = if (AppConfig.parallelExportBook) { val threads = if (AppConfig.parallelExportBook) {
AppConst.MAX_THREAD AppConst.MAX_THREAD
} else { } else {
0 1
} }
flow { flow {
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
val task = async(Default, start = CoroutineStart.LAZY) { emit(chapter)
val content = BookHelp.getContent(book, chapter)
val (contentFix, resources) = fixPic(
book,
content ?: if (chapter.isVolume) "" else "null",
chapter
)
// 不导出vip标识
chapter.isVip = false
val content1 = contentProcessor
.getContent(
book,
chapter,
contentFix,
includeTitle = false,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
val title = chapter.run {
// 不导出vip标识
isVip = false
getDisplayTitle(
contentProcessor.getTitleReplaceRules(),
useReplace = useReplace
)
}
val chapterResource = ResourceUtil.createChapterResource(
title.replace("\uD83D\uDD12", ""),
content1,
contentModel,
"Text/chapter_${index}.html"
)
ExportChapter(title, chapterResource, resources)
}
emit(task)
} }
}.onEach { it.start() } }.mapAsyncIndexed(threads) { index, chapter ->
.buffer(threads) val content = BookHelp.getContent(book, chapter)
.map { it.await() } val (contentFix, resources) = fixPic(
.collectIndexed { index, exportChapter -> book,
postEvent(EventBus.EXPORT_BOOK, book.bookUrl) content ?: if (chapter.isVolume) "" else "null",
exportProgress[book.bookUrl] = index chapter
epubBook.resources.addAll(exportChapter.resources) )
epubBook.addSection(exportChapter.title, exportChapter.chapterResource) // 不导出vip标识
chapter.isVip = false
val content1 = contentProcessor
.getContent(
book,
chapter,
contentFix,
includeTitle = false,
useReplace = useReplace,
chineseConvert = false,
reSegment = false
).toString()
val title = chapter.run {
// 不导出vip标识
isVip = false
getDisplayTitle(
contentProcessor.getTitleReplaceRules(),
useReplace = useReplace
)
} }
val chapterResource = ResourceUtil.createChapterResource(
title.replace("\uD83D\uDD12", ""),
content1,
contentModel,
"Text/chapter_${index}.html"
)
ExportChapter(title, chapterResource, resources)
}.collectIndexed { index, exportChapter ->
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
exportProgress[book.bookUrl] = index
epubBook.resources.addAll(exportChapter.resources)
epubBook.addSection(exportChapter.title, exportChapter.chapterResource)
}
} }
data class ExportChapter( data class ExportChapter(
@@ -1,17 +1,22 @@
package io.legado.app.utils package io.legado.app.utils
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Semaphore
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
inline fun <T> Flow<T>.onEachParallel( inline fun <T> Flow<T>.onEachParallel(
concurrency: Int, concurrency: Int,
crossinline action: suspend (T) -> Unit crossinline action: suspend (T) -> Unit
): Flow<T> = flatMapMerge(concurrency) { value -> ): Flow<T> = flatMapMerge(concurrency) { value ->
return@flatMapMerge flow { flow {
action(value) action(value)
emit(value) emit(value)
} }
@@ -37,3 +42,98 @@ inline fun <T> Flow<T>.onEachIndexed(
emit(value) emit(value)
} }
} }
inline fun <T, R> Flow<T>.mapIndexed(
crossinline action: suspend (index: Int, T) -> R,
): Flow<R> = flow {
var index = 0
collect { value ->
emit(action(index++, value))
}
}
inline fun <T, R> Flow<T>.mapAsync(
concurrency: Int,
crossinline transform: suspend (T) -> R
): Flow<R> = if (concurrency == 1) {
map { transform(it) }
} else {
Semaphore(concurrency).let { semaphore ->
channelFlow {
collect {
semaphore.acquire()
send(async { transform(it) })
}
}.map {
it.await()
}.onEach { semaphore.release() }
}
}
inline fun <T, R> Flow<T>.mapAsyncIndexed(
concurrency: Int,
crossinline transform: suspend (index: Int, T) -> R
): Flow<R> = if (concurrency == 1) {
mapIndexed { index, value ->
transform(index, value)
}
} else {
Semaphore(concurrency).let { semaphore ->
channelFlow {
var index = 0
collect {
semaphore.acquire()
val i = index++
send(async { transform(i, it) })
}
}.map {
it.await()
}.onEach { semaphore.release() }
}
}
inline fun <T> Flow<T>.onEachAsync(
concurrency: Int,
crossinline action: suspend (T) -> Unit
): Flow<T> = if (concurrency == 1) {
onEach { action(it) }
} else {
Semaphore(concurrency).let { semaphore ->
channelFlow {
collect {
semaphore.acquire()
send(async {
action(it)
it
})
}
}.map {
it.await()
}.onEach { semaphore.release() }
}
}
inline fun <T> Flow<T>.onEachAsyncIndexed(
concurrency: Int,
crossinline action: suspend (index: Int, T) -> Unit
): Flow<T> = if (concurrency == 1) {
onEachIndexed { index, value ->
action(index, value)
}
} else {
Semaphore(concurrency).let { semaphore ->
channelFlow {
var index = 0
collect {
semaphore.acquire()
val i = index++
send(async {
action(i, it)
it
})
}
}.map {
it.await()
}.onEach { semaphore.release() }
}
}