优化
This commit is contained in:
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user