Rename
This commit is contained in:
1 parent
b86d90021c
commit
6c0757c91b
972 files changed
+6885
-7321
No files matched your search
@@ -0,0 +1,288 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.script.ScriptBindings
|
||||
import com.script.rhino.RhinoScriptEngine
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookChapter
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.data.entities.rule.TocRule
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.exception.TocEmptyException
|
||||
import io.legato.kazusa.help.book.ContentProcessor
|
||||
import io.legato.kazusa.help.book.simulatedTotalChapterNum
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.utils.isTrue
|
||||
import io.legato.kazusa.utils.mapAsync
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.mozilla.javascript.Context
|
||||
import splitties.init.appCtx
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* 获取目录
|
||||
*/
|
||||
object BookChapterList {
|
||||
|
||||
suspend fun analyzeChapterList(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
body: String?
|
||||
): List<BookChapter> {
|
||||
body ?: throw NoStackTraceException(
|
||||
appCtx.getString(R.string.error_get_web_content, baseUrl)
|
||||
)
|
||||
val chapterList = ArrayList<BookChapter>()
|
||||
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
|
||||
Debug.log(bookSource.bookSourceUrl, body, state = 30)
|
||||
val tocRule = bookSource.getTocRule()
|
||||
val nextUrlList = arrayListOf(redirectUrl)
|
||||
var reverse = false
|
||||
var listRule = tocRule.chapterList ?: ""
|
||||
if (listRule.startsWith("-")) {
|
||||
reverse = true
|
||||
listRule = listRule.substring(1)
|
||||
}
|
||||
if (listRule.startsWith("+")) {
|
||||
listRule = listRule.substring(1)
|
||||
}
|
||||
var chapterData =
|
||||
analyzeChapterList(
|
||||
book, baseUrl, redirectUrl, body,
|
||||
tocRule, listRule, bookSource, log = true
|
||||
)
|
||||
chapterList.addAll(chapterData.first)
|
||||
when (chapterData.second.size) {
|
||||
0 -> Unit
|
||||
1 -> {
|
||||
var nextUrl = chapterData.second[0]
|
||||
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
|
||||
nextUrlList.add(nextUrl)
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = nextUrl,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
val res = analyzeUrl.getStrResponseAwait() //控制并发访问
|
||||
res.body?.let { nextBody ->
|
||||
chapterData = analyzeChapterList(
|
||||
book, nextUrl, nextUrl,
|
||||
nextBody, tocRule, listRule, bookSource
|
||||
)
|
||||
nextUrl = chapterData.second.firstOrNull() ?: ""
|
||||
chapterList.addAll(chapterData.first)
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"◇并发解析目录,总页数:${chapterData.second.size}"
|
||||
)
|
||||
flow {
|
||||
for (urlStr in chapterData.second) {
|
||||
emit(urlStr)
|
||||
}
|
||||
}.mapAsync(AppConfig.threadCount) { urlStr ->
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = urlStr,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
val res = analyzeUrl.getStrResponseAwait() //控制并发访问
|
||||
analyzeChapterList(
|
||||
book, urlStr, res.url,
|
||||
res.body!!, tocRule, listRule, bookSource, false
|
||||
).first
|
||||
}.collect {
|
||||
chapterList.addAll(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chapterList.isEmpty()) {
|
||||
throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
|
||||
}
|
||||
if (!reverse) {
|
||||
chapterList.reverse()
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
//去重
|
||||
val lh = LinkedHashSet(chapterList)
|
||||
val list = ArrayList(lh)
|
||||
if (!book.getReverseToc()) {
|
||||
list.reverse()
|
||||
}
|
||||
Debug.log(book.origin, "◇目录总数:${list.size}")
|
||||
coroutineContext.ensureActive()
|
||||
list.forEachIndexed { index, bookChapter ->
|
||||
bookChapter.index = index
|
||||
}
|
||||
val formatJs = tocRule.formatJs
|
||||
if (!formatJs.isNullOrBlank()) {
|
||||
Context.enter().use {
|
||||
val bindings = ScriptBindings()
|
||||
bindings["gInt"] = 0
|
||||
list.forEachIndexed { index, bookChapter ->
|
||||
bindings["index"] = index + 1
|
||||
bindings["chapter"] = bookChapter
|
||||
bindings["title"] = bookChapter.title
|
||||
RhinoScriptEngine.runCatching {
|
||||
eval(formatJs, bindings)?.toString()?.let {
|
||||
bookChapter.title = it
|
||||
}
|
||||
}.onFailure {
|
||||
Debug.log(book.origin, "格式化标题出错, ${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val replaceRules = ContentProcessor.get(book).getTitleReplaceRules()
|
||||
book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
|
||||
.getDisplayTitle(replaceRules, book.getUseReplaceRule())
|
||||
if (book.totalChapterNum < list.size) {
|
||||
book.lastCheckCount = list.size - book.totalChapterNum
|
||||
book.latestChapterTime = System.currentTimeMillis()
|
||||
}
|
||||
book.lastCheckTime = System.currentTimeMillis()
|
||||
book.totalChapterNum = list.size
|
||||
book.latestChapterTitle =
|
||||
list.getOrElse(book.simulatedTotalChapterNum() - 1) { list.last() }
|
||||
.getDisplayTitle(replaceRules, book.getUseReplaceRule())
|
||||
coroutineContext.ensureActive()
|
||||
getWordCount(list, book)
|
||||
return list
|
||||
}
|
||||
|
||||
private suspend fun analyzeChapterList(
|
||||
book: Book,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
body: String,
|
||||
tocRule: TocRule,
|
||||
listRule: String,
|
||||
bookSource: BookSource,
|
||||
getNextUrl: Boolean = true,
|
||||
log: Boolean = false
|
||||
): Pair<List<BookChapter>, List<String>> {
|
||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||
analyzeRule.setContent(body).setBaseUrl(baseUrl)
|
||||
analyzeRule.setRedirectUrl(redirectUrl)
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
//获取目录列表
|
||||
val chapterList = arrayListOf<BookChapter>()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log)
|
||||
val elements = analyzeRule.getElements(listRule)
|
||||
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log)
|
||||
//获取下一页链接
|
||||
val nextUrlList = arrayListOf<String>()
|
||||
val nextTocRule = tocRule.nextTocUrl
|
||||
if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log)
|
||||
analyzeRule.getStringList(nextTocRule, isUrl = true)?.let {
|
||||
for (item in it) {
|
||||
if (item != redirectUrl) {
|
||||
nextUrlList.add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"└" + TextUtils.join(",\n", nextUrlList),
|
||||
log
|
||||
)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
if (elements.isNotEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log)
|
||||
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)
|
||||
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)
|
||||
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)
|
||||
val payRule = analyzeRule.splitSourceRule(tocRule.isPay)
|
||||
val upTimeRule = analyzeRule.splitSourceRule(tocRule.updateTime)
|
||||
val isVolumeRule = analyzeRule.splitSourceRule(tocRule.isVolume)
|
||||
elements.forEachIndexed { index, item ->
|
||||
coroutineContext.ensureActive()
|
||||
analyzeRule.setContent(item)
|
||||
val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = redirectUrl)
|
||||
analyzeRule.setChapter(bookChapter)
|
||||
bookChapter.title = analyzeRule.getString(nameRule)
|
||||
bookChapter.url = analyzeRule.getString(urlRule)
|
||||
bookChapter.tag = analyzeRule.getString(upTimeRule)
|
||||
val isVolume = analyzeRule.getString(isVolumeRule)
|
||||
bookChapter.isVolume = false
|
||||
if (isVolume.isTrue()) {
|
||||
bookChapter.isVolume = true
|
||||
}
|
||||
if (bookChapter.url.isEmpty()) {
|
||||
if (bookChapter.isVolume) {
|
||||
bookChapter.url = bookChapter.title + index
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"⇒一级目录${index}未获取到url,使用标题替代"
|
||||
)
|
||||
} else {
|
||||
bookChapter.url = baseUrl
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"⇒目录${index}未获取到url,使用baseUrl替代"
|
||||
)
|
||||
}
|
||||
}
|
||||
if (bookChapter.title.isNotEmpty()) {
|
||||
val isVip = analyzeRule.getString(vipRule)
|
||||
val isPay = analyzeRule.getString(payRule)
|
||||
if (isVip.isTrue()) {
|
||||
bookChapter.isVip = true
|
||||
}
|
||||
if (isPay.isTrue()) {
|
||||
bookChapter.isPay = true
|
||||
}
|
||||
chapterList.add(bookChapter)
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└目录列表解析完成", log)
|
||||
if (chapterList.isEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "◇章节列表为空", log)
|
||||
} else {
|
||||
Debug.log(bookSource.bookSourceUrl, "≡首章信息", log)
|
||||
Debug.log(bookSource.bookSourceUrl, "◇章节名称:${chapterList[0].title}", log)
|
||||
Debug.log(bookSource.bookSourceUrl, "◇章节链接:${chapterList[0].url}", log)
|
||||
Debug.log(bookSource.bookSourceUrl, "◇章节信息:${chapterList[0].tag}", log)
|
||||
Debug.log(bookSource.bookSourceUrl, "◇是否VIP:${chapterList[0].isVip}", log)
|
||||
Debug.log(bookSource.bookSourceUrl, "◇是否购买:${chapterList[0].isPay}", log)
|
||||
}
|
||||
}
|
||||
return Pair(chapterList, nextUrlList)
|
||||
}
|
||||
|
||||
private fun getWordCount(list: ArrayList<BookChapter>, book: Book) {
|
||||
if (!AppConfig.tocCountWords) {
|
||||
return
|
||||
}
|
||||
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
||||
if (chapterList.isNotEmpty()) {
|
||||
val map = chapterList.associateBy({ it.getFileName() }, { it.wordCount })
|
||||
for (bookChapter in list) {
|
||||
val wordCount = map[bookChapter.getFileName()]
|
||||
if (wordCount != null) {
|
||||
bookChapter.wordCount = wordCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppPattern
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookChapter
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.data.entities.rule.ContentRule
|
||||
import io.legato.kazusa.exception.ContentEmptyException
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.book.BookHelp
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.utils.HtmlFormatter
|
||||
import io.legato.kazusa.utils.NetworkUtils
|
||||
import io.legato.kazusa.utils.mapAsync
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
import splitties.init.appCtx
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* 获取正文
|
||||
*/
|
||||
object BookContent {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun analyzeContent(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
body: String?,
|
||||
nextChapterUrl: String?,
|
||||
needSave: Boolean = true
|
||||
): String {
|
||||
body ?: throw NoStackTraceException(
|
||||
appCtx.getString(R.string.error_get_web_content, baseUrl)
|
||||
)
|
||||
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
|
||||
Debug.log(bookSource.bookSourceUrl, body, state = 40)
|
||||
val mNextChapterUrl = if (nextChapterUrl.isNullOrEmpty()) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url
|
||||
?: appDb.bookChapterDao.getChapter(book.bookUrl, 0)?.url
|
||||
} else {
|
||||
nextChapterUrl
|
||||
}
|
||||
val contentList = arrayListOf<String>()
|
||||
val nextUrlList = arrayListOf(redirectUrl)
|
||||
val contentRule = bookSource.getContentRule()
|
||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||
analyzeRule.setContent(body, baseUrl)
|
||||
analyzeRule.setRedirectUrl(redirectUrl)
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
analyzeRule.setChapter(bookChapter)
|
||||
analyzeRule.setNextChapterUrl(mNextChapterUrl)
|
||||
coroutineContext.ensureActive()
|
||||
val titleRule = contentRule.title
|
||||
if (!titleRule.isNullOrBlank()) {
|
||||
val title = analyzeRule.runCatching {
|
||||
getString(titleRule)
|
||||
}.onFailure {
|
||||
Debug.log(bookSource.bookSourceUrl, "获取标题出错, ${it.localizedMessage}")
|
||||
}.getOrNull()
|
||||
if (!title.isNullOrBlank()) {
|
||||
bookChapter.title = title
|
||||
bookChapter.titleMD5 = null
|
||||
appDb.bookChapterDao.update(bookChapter)
|
||||
}
|
||||
}
|
||||
var contentData = analyzeContent(
|
||||
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
|
||||
)
|
||||
contentList.add(contentData.first)
|
||||
if (contentData.second.size == 1) {
|
||||
var nextUrl = contentData.second[0]
|
||||
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
|
||||
if (!mNextChapterUrl.isNullOrEmpty()
|
||||
&& NetworkUtils.getAbsoluteURL(redirectUrl, nextUrl)
|
||||
== NetworkUtils.getAbsoluteURL(redirectUrl, mNextChapterUrl)
|
||||
) break
|
||||
nextUrlList.add(nextUrl)
|
||||
coroutineContext.ensureActive()
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = nextUrl,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
val res = analyzeUrl.getStrResponseAwait() //控制并发访问
|
||||
res.body?.let { nextBody ->
|
||||
contentData = analyzeContent(
|
||||
book, nextUrl, res.url, nextBody, contentRule,
|
||||
bookChapter, bookSource, mNextChapterUrl,
|
||||
printLog = false
|
||||
)
|
||||
nextUrl =
|
||||
if (contentData.second.isNotEmpty()) contentData.second[0] else ""
|
||||
contentList.add(contentData.first)
|
||||
Debug.log(bookSource.bookSourceUrl, "第${contentList.size}页完成")
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
|
||||
} else if (contentData.second.size > 1) {
|
||||
Debug.log(bookSource.bookSourceUrl, "◇并发解析正文,总页数:${contentData.second.size}")
|
||||
flow {
|
||||
for (urlStr in contentData.second) {
|
||||
emit(urlStr)
|
||||
}
|
||||
}.mapAsync(AppConfig.threadCount) { urlStr ->
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = urlStr,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
val res = analyzeUrl.getStrResponseAwait() //控制并发访问
|
||||
analyzeContent(
|
||||
book, urlStr, res.url, res.body!!, contentRule,
|
||||
bookChapter, bookSource, mNextChapterUrl,
|
||||
getNextPageUrl = false,
|
||||
printLog = false
|
||||
).first
|
||||
}.collect {
|
||||
coroutineContext.ensureActive()
|
||||
contentList.add(it)
|
||||
}
|
||||
}
|
||||
var contentStr = contentList.joinToString("\n")
|
||||
//全文替换
|
||||
val replaceRegex = contentRule.replaceRegex
|
||||
if (!replaceRegex.isNullOrEmpty()) {
|
||||
contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { it.trim() }
|
||||
contentStr = analyzeRule.getString(replaceRegex, contentStr)
|
||||
contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { " $it" }
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
|
||||
Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}")
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")
|
||||
Debug.log(bookSource.bookSourceUrl, "└\n$contentStr")
|
||||
if (!bookChapter.isVolume && contentStr.isBlank()) {
|
||||
throw ContentEmptyException("内容为空")
|
||||
}
|
||||
if (needSave) {
|
||||
BookHelp.saveContent(bookSource, book, bookChapter, contentStr)
|
||||
}
|
||||
return contentStr
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private suspend fun analyzeContent(
|
||||
book: Book,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
body: String,
|
||||
contentRule: ContentRule,
|
||||
chapter: BookChapter,
|
||||
bookSource: BookSource,
|
||||
nextChapterUrl: String?,
|
||||
getNextPageUrl: Boolean = true,
|
||||
printLog: Boolean = true
|
||||
): Pair<String, List<String>> {
|
||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||
analyzeRule.setContent(body, baseUrl)
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
val rUrl = analyzeRule.setRedirectUrl(redirectUrl)
|
||||
analyzeRule.setNextChapterUrl(nextChapterUrl)
|
||||
val nextUrlList = arrayListOf<String>()
|
||||
analyzeRule.setChapter(chapter)
|
||||
//获取正文
|
||||
var content = analyzeRule.getString(contentRule.content, unescape = false)
|
||||
content = HtmlFormatter.formatKeepImg(content, rUrl)
|
||||
if (content.indexOf('&') > -1) {
|
||||
content = StringEscapeUtils.unescapeHtml4(content)
|
||||
}
|
||||
//获取下一页链接
|
||||
if (getNextPageUrl) {
|
||||
val nextUrlRule = contentRule.nextContentUrl
|
||||
if (!nextUrlRule.isNullOrEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog)
|
||||
analyzeRule.getStringList(nextUrlRule, isUrl = true)?.let {
|
||||
nextUrlList.addAll(it)
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└" + nextUrlList.joinToString(","), printLog)
|
||||
}
|
||||
}
|
||||
return Pair(content, nextUrlList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.book.BookHelp
|
||||
import io.legato.kazusa.help.book.isWebFile
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legato.kazusa.utils.DebugLog
|
||||
import io.legato.kazusa.utils.HtmlFormatter
|
||||
import io.legato.kazusa.utils.NetworkUtils
|
||||
import io.legato.kazusa.utils.StringUtils.wordCountFormat
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import splitties.init.appCtx
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*/
|
||||
object BookInfo {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun analyzeBookInfo(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
body: String?,
|
||||
canReName: Boolean,
|
||||
) {
|
||||
body ?: throw NoStackTraceException(
|
||||
appCtx.getString(R.string.error_get_web_content, baseUrl)
|
||||
)
|
||||
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
|
||||
Debug.log(bookSource.bookSourceUrl, body, state = 20)
|
||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||
analyzeRule.setContent(body).setBaseUrl(baseUrl)
|
||||
analyzeRule.setRedirectUrl(redirectUrl)
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
analyzeBookInfo(book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName)
|
||||
}
|
||||
|
||||
suspend fun analyzeBookInfo(
|
||||
book: Book,
|
||||
body: String,
|
||||
analyzeRule: AnalyzeRule,
|
||||
bookSource: BookSource,
|
||||
baseUrl: String,
|
||||
redirectUrl: String,
|
||||
canReName: Boolean,
|
||||
) {
|
||||
val infoRule = bookSource.getBookInfoRule()
|
||||
infoRule.init?.let {
|
||||
if (it.isNotBlank()) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")
|
||||
analyzeRule.setContent(analyzeRule.getElement(it))
|
||||
}
|
||||
}
|
||||
val mCanReName = canReName && !infoRule.canReName.isNullOrBlank()
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
|
||||
BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let {
|
||||
if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) {
|
||||
book.name = it
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
|
||||
BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let {
|
||||
if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) {
|
||||
book.author = it
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取分类")
|
||||
try {
|
||||
analyzeRule.getStringList(infoRule.kind)
|
||||
?.joinToString(",")
|
||||
?.let {
|
||||
if (it.isNotEmpty()) book.kind = it
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
} ?: Debug.log(bookSource.bookSourceUrl, "└")
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取分类出错", e)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取字数")
|
||||
try {
|
||||
wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let {
|
||||
if (it.isNotEmpty()) book.wordCount = it
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取字数出错", e)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节")
|
||||
try {
|
||||
analyzeRule.getString(infoRule.lastChapter).let {
|
||||
if (it.isNotEmpty()) book.latestChapterTitle = it
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取最新章节出错", e)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
|
||||
try {
|
||||
HtmlFormatter.format(analyzeRule.getString(infoRule.intro)).let {
|
||||
if (it.isNotEmpty()) book.intro = it
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取简介出错", e)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
|
||||
try {
|
||||
analyzeRule.getString(infoRule.coverUrl).let {
|
||||
if (it.isNotEmpty()) {
|
||||
book.coverUrl =
|
||||
NetworkUtils.getAbsoluteURL(redirectUrl, it)
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${it}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取封面出错", e)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
if (!book.isWebFile) {
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||
if (book.tocUrl == baseUrl) {
|
||||
book.tocHtml = body
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
|
||||
} else {
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接")
|
||||
book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true)
|
||||
if (book.downloadUrls.isNullOrEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "└")
|
||||
throw NoStackTraceException("下载链接为空")
|
||||
} else {
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"└" + TextUtils.join(",\n", book.downloadUrls!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.data.entities.SearchBook
|
||||
import io.legato.kazusa.data.entities.rule.BookListRule
|
||||
import io.legato.kazusa.data.entities.rule.ExploreKind
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.book.BookHelp
|
||||
import io.legato.kazusa.help.source.exploreKindsJson
|
||||
import io.legato.kazusa.help.source.getBookType
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setRuleData
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.model.analyzeRule.RuleData
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.GSONStrict
|
||||
import io.legato.kazusa.utils.HtmlFormatter
|
||||
import io.legato.kazusa.utils.NetworkUtils
|
||||
import io.legato.kazusa.utils.StringUtils.wordCountFormat
|
||||
import io.legato.kazusa.utils.fromJsonArray
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import splitties.init.appCtx
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* 获取书籍列表
|
||||
*/
|
||||
object BookList {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun analyzeBookList(
|
||||
bookSource: BookSource,
|
||||
ruleData: RuleData,
|
||||
analyzeUrl: AnalyzeUrl,
|
||||
baseUrl: String,
|
||||
body: String?,
|
||||
isSearch: Boolean = true,
|
||||
isRedirect: Boolean = false,
|
||||
filter: ((name: String, author: String) -> Boolean)? = null,
|
||||
shouldBreak: ((size: Int) -> Boolean)? = null
|
||||
): ArrayList<SearchBook> {
|
||||
body ?: throw NoStackTraceException(
|
||||
appCtx.getString(
|
||||
R.string.error_get_web_content,
|
||||
analyzeUrl.ruleUrl
|
||||
)
|
||||
)
|
||||
val bookList = ArrayList<SearchBook>()
|
||||
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
|
||||
Debug.log(bookSource.bookSourceUrl, body, state = 10)
|
||||
val analyzeRule = AnalyzeRule(ruleData, bookSource)
|
||||
analyzeRule.setContent(body).setBaseUrl(baseUrl)
|
||||
analyzeRule.setRedirectUrl(baseUrl)
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
if (!isSearch) {
|
||||
checkExploreJson(bookSource)
|
||||
}
|
||||
if (isSearch) bookSource.bookUrlPattern?.let {
|
||||
coroutineContext.ensureActive()
|
||||
if (baseUrl.matches(it.toRegex())) {
|
||||
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")
|
||||
getInfoItem(
|
||||
bookSource,
|
||||
analyzeRule,
|
||||
analyzeUrl,
|
||||
body,
|
||||
baseUrl,
|
||||
ruleData.getVariable(),
|
||||
isRedirect,
|
||||
filter
|
||||
)?.let { searchBook ->
|
||||
searchBook.infoHtml = body
|
||||
bookList.add(searchBook)
|
||||
}
|
||||
return bookList
|
||||
}
|
||||
}
|
||||
val collections: List<Any>
|
||||
var reverse = false
|
||||
val bookListRule: BookListRule = when {
|
||||
isSearch -> bookSource.getSearchRule()
|
||||
bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule()
|
||||
else -> bookSource.getExploreRule()
|
||||
}
|
||||
var ruleList: String = bookListRule.bookList ?: ""
|
||||
if (ruleList.startsWith("-")) {
|
||||
reverse = true
|
||||
ruleList = ruleList.substring(1)
|
||||
}
|
||||
if (ruleList.startsWith("+")) {
|
||||
ruleList = ruleList.substring(1)
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表")
|
||||
collections = analyzeRule.getElements(ruleList)
|
||||
coroutineContext.ensureActive()
|
||||
if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析")
|
||||
getInfoItem(
|
||||
bookSource, analyzeRule, analyzeUrl, body, baseUrl, ruleData.getVariable(),
|
||||
isRedirect, filter
|
||||
)?.let { searchBook ->
|
||||
searchBook.infoHtml = body
|
||||
bookList.add(searchBook)
|
||||
}
|
||||
} else {
|
||||
val ruleName = analyzeRule.splitSourceRule(bookListRule.name)
|
||||
val ruleBookUrl = analyzeRule.splitSourceRule(bookListRule.bookUrl)
|
||||
val ruleAuthor = analyzeRule.splitSourceRule(bookListRule.author)
|
||||
val ruleCoverUrl = analyzeRule.splitSourceRule(bookListRule.coverUrl)
|
||||
val ruleIntro = analyzeRule.splitSourceRule(bookListRule.intro)
|
||||
val ruleKind = analyzeRule.splitSourceRule(bookListRule.kind)
|
||||
val ruleLastChapter = analyzeRule.splitSourceRule(bookListRule.lastChapter)
|
||||
val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)
|
||||
Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}")
|
||||
for ((index, item) in collections.withIndex()) {
|
||||
getSearchItem(
|
||||
bookSource, analyzeRule, item, baseUrl, ruleData.getVariable(),
|
||||
index == 0,
|
||||
filter,
|
||||
ruleName = ruleName,
|
||||
ruleBookUrl = ruleBookUrl,
|
||||
ruleAuthor = ruleAuthor,
|
||||
ruleCoverUrl = ruleCoverUrl,
|
||||
ruleIntro = ruleIntro,
|
||||
ruleKind = ruleKind,
|
||||
ruleLastChapter = ruleLastChapter,
|
||||
ruleWordCount = ruleWordCount
|
||||
)?.let { searchBook ->
|
||||
if (baseUrl == searchBook.bookUrl) {
|
||||
searchBook.infoHtml = body
|
||||
}
|
||||
bookList.add(searchBook)
|
||||
}
|
||||
if (shouldBreak?.invoke(bookList.size) == true) {
|
||||
break
|
||||
}
|
||||
}
|
||||
val lh = LinkedHashSet(bookList)
|
||||
bookList.clear()
|
||||
bookList.addAll(lh)
|
||||
if (reverse) {
|
||||
bookList.reverse()
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "◇书籍总数:${bookList.size}")
|
||||
return bookList
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private suspend fun getInfoItem(
|
||||
bookSource: BookSource,
|
||||
analyzeRule: AnalyzeRule,
|
||||
analyzeUrl: AnalyzeUrl,
|
||||
body: String,
|
||||
baseUrl: String,
|
||||
variable: String?,
|
||||
isRedirect: Boolean,
|
||||
filter: ((name: String, author: String) -> Boolean)?
|
||||
): SearchBook? {
|
||||
val book = Book(variable = variable)
|
||||
book.bookUrl = if (isRedirect) {
|
||||
baseUrl
|
||||
} else {
|
||||
NetworkUtils.getAbsoluteURL(analyzeUrl.url, analyzeUrl.ruleUrl)
|
||||
}
|
||||
book.origin = bookSource.bookSourceUrl
|
||||
book.originName = bookSource.bookSourceName
|
||||
book.originOrder = bookSource.customOrder
|
||||
book.type = bookSource.getBookType()
|
||||
analyzeRule.setRuleData(book)
|
||||
BookInfo.analyzeBookInfo(
|
||||
book,
|
||||
body,
|
||||
analyzeRule,
|
||||
bookSource,
|
||||
baseUrl,
|
||||
baseUrl,
|
||||
false
|
||||
)
|
||||
if (filter?.invoke(book.name, book.author) == false) {
|
||||
return null
|
||||
}
|
||||
if (book.name.isNotBlank()) {
|
||||
return book.toSearchBook()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private suspend fun getSearchItem(
|
||||
bookSource: BookSource,
|
||||
analyzeRule: AnalyzeRule,
|
||||
item: Any,
|
||||
baseUrl: String,
|
||||
variable: String?,
|
||||
log: Boolean,
|
||||
filter: ((name: String, author: String) -> Boolean)?,
|
||||
ruleName: List<AnalyzeRule.SourceRule>,
|
||||
ruleBookUrl: List<AnalyzeRule.SourceRule>,
|
||||
ruleAuthor: List<AnalyzeRule.SourceRule>,
|
||||
ruleKind: List<AnalyzeRule.SourceRule>,
|
||||
ruleCoverUrl: List<AnalyzeRule.SourceRule>,
|
||||
ruleWordCount: List<AnalyzeRule.SourceRule>,
|
||||
ruleIntro: List<AnalyzeRule.SourceRule>,
|
||||
ruleLastChapter: List<AnalyzeRule.SourceRule>
|
||||
): SearchBook? {
|
||||
val searchBook = SearchBook(variable = variable)
|
||||
searchBook.type = bookSource.getBookType()
|
||||
searchBook.origin = bookSource.bookSourceUrl
|
||||
searchBook.originName = bookSource.bookSourceName
|
||||
searchBook.originOrder = bookSource.customOrder
|
||||
analyzeRule.setRuleData(searchBook)
|
||||
analyzeRule.setContent(item)
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取书名", log)
|
||||
searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName))
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.name}", log)
|
||||
if (searchBook.name.isNotEmpty()) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取作者", log)
|
||||
searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor))
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}", log)
|
||||
if (filter?.invoke(searchBook.name, searchBook.author) == false) {
|
||||
return null
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取分类", log)
|
||||
try {
|
||||
searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",")
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind ?: ""}", log)
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取字数", log)
|
||||
try {
|
||||
searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount))
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}", log)
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log)
|
||||
try {
|
||||
searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}", log)
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
|
||||
try {
|
||||
searchBook.intro = HtmlFormatter.format(analyzeRule.getString(ruleIntro))
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", log)
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)
|
||||
try {
|
||||
analyzeRule.getString(ruleCoverUrl).let {
|
||||
if (it.isNotEmpty()) {
|
||||
searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it)
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl ?: ""}", log)
|
||||
} catch (e: Exception) {
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log)
|
||||
searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, isUrl = true)
|
||||
if (searchBook.bookUrl.isEmpty()) {
|
||||
searchBook.bookUrl = baseUrl
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${searchBook.bookUrl}", log)
|
||||
return searchBook
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkExploreJson(bookSource: BookSource) {
|
||||
if (Debug.callback == null) {
|
||||
return
|
||||
}
|
||||
val json = bookSource.exploreKindsJson()
|
||||
if (json.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val kinds = GSONStrict.fromJsonArray<ExploreKind>(json).getOrNull()
|
||||
if (kinds != null) {
|
||||
return
|
||||
}
|
||||
GSON.fromJsonArray<ExploreKind>(json).getOrNull()?.let {
|
||||
Debug.log("≡发现地址规则 JSON 格式不规范,请改为规范格式")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.PreferKey
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.BookSourcePart
|
||||
import io.legato.kazusa.data.entities.SearchBook
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.ui.book.search.SearchScope
|
||||
import io.legato.kazusa.utils.getPrefBoolean
|
||||
import io.legato.kazusa.utils.mapParallelSafe
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import splitties.init.appCtx
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.math.min
|
||||
|
||||
class SearchModel(private val scope: CoroutineScope, private val callBack: CallBack) {
|
||||
val threadCount = AppConfig.threadCount
|
||||
private var searchPool: ExecutorCoroutineDispatcher? = null
|
||||
private var mSearchId = 0L
|
||||
private var searchPage = 1
|
||||
private var searchKey: String = ""
|
||||
private var bookSourceParts = emptyList<BookSourcePart>()
|
||||
private var searchBooks = arrayListOf<SearchBook>()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
|
||||
private fun initSearchPool() {
|
||||
searchPool?.close()
|
||||
searchPool = Executors
|
||||
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
fun search(searchId: Long, key: String) {
|
||||
if (searchId != mSearchId) {
|
||||
if (key.isEmpty()) {
|
||||
return
|
||||
}
|
||||
searchKey = key
|
||||
if (mSearchId != 0L) {
|
||||
close()
|
||||
}
|
||||
searchBooks.clear()
|
||||
bookSourceParts = callBack.getSearchScope().getBookSourceParts()
|
||||
if (bookSourceParts.isEmpty()) {
|
||||
callBack.onSearchCancel(NoStackTraceException("启用书源为空"))
|
||||
return
|
||||
}
|
||||
mSearchId = searchId
|
||||
searchPage = 1
|
||||
initSearchPool()
|
||||
} else {
|
||||
searchPage++
|
||||
}
|
||||
startSearch()
|
||||
}
|
||||
|
||||
private fun startSearch() {
|
||||
val precision = appCtx.getPrefBoolean(PreferKey.precisionSearch)
|
||||
var hasMore = false
|
||||
searchJob = scope.launch(searchPool!!) {
|
||||
flow {
|
||||
for (bs in bookSourceParts) {
|
||||
bs.getBookSource()?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
callBack.onSearchStart()
|
||||
}.mapParallelSafe(threadCount) {
|
||||
withTimeout(30000L) {
|
||||
WebBook.searchBookAwait(
|
||||
it, searchKey, searchPage,
|
||||
filter = { name, author ->
|
||||
!precision || name.contains(searchKey) ||
|
||||
author.contains(searchKey)
|
||||
})
|
||||
}
|
||||
}.onEach { items ->
|
||||
for (book in items) {
|
||||
book.releaseHtmlData()
|
||||
}
|
||||
hasMore = hasMore || items.isNotEmpty()
|
||||
appDb.searchBookDao.insert(*items.toTypedArray())
|
||||
mergeItems(items, precision)
|
||||
currentCoroutineContext().ensureActive()
|
||||
callBack.onSearchSuccess(searchBooks)
|
||||
}.onCompletion {
|
||||
if (it == null) callBack.onSearchFinish(searchBooks.isEmpty(), hasMore)
|
||||
}.catch {
|
||||
AppLog.put("书源搜索出错\n${it.localizedMessage}", it)
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mergeItems(newDataS: List<SearchBook>, precision: Boolean) {
|
||||
if (newDataS.isNotEmpty()) {
|
||||
val copyData = ArrayList(searchBooks)
|
||||
val equalData = arrayListOf<SearchBook>()
|
||||
val containsData = arrayListOf<SearchBook>()
|
||||
val otherData = arrayListOf<SearchBook>()
|
||||
copyData.forEach {
|
||||
coroutineContext.ensureActive()
|
||||
if (it.name == searchKey || it.author == searchKey) {
|
||||
equalData.add(it)
|
||||
} else if (it.name.contains(searchKey) || it.author.contains(searchKey)) {
|
||||
containsData.add(it)
|
||||
} else {
|
||||
otherData.add(it)
|
||||
}
|
||||
}
|
||||
newDataS.forEach { nBook ->
|
||||
coroutineContext.ensureActive()
|
||||
if (nBook.name == searchKey || nBook.author == searchKey) {
|
||||
var hasSame = false
|
||||
equalData.forEach { pBook ->
|
||||
coroutineContext.ensureActive()
|
||||
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
||||
pBook.addOrigin(nBook.origin)
|
||||
hasSame = true
|
||||
}
|
||||
}
|
||||
if (!hasSame) {
|
||||
equalData.add(nBook)
|
||||
}
|
||||
} else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) {
|
||||
var hasSame = false
|
||||
containsData.forEach { pBook ->
|
||||
coroutineContext.ensureActive()
|
||||
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
||||
pBook.addOrigin(nBook.origin)
|
||||
hasSame = true
|
||||
}
|
||||
}
|
||||
if (!hasSame) {
|
||||
containsData.add(nBook)
|
||||
}
|
||||
} else if (!precision) {
|
||||
var hasSame = false
|
||||
otherData.forEach { pBook ->
|
||||
coroutineContext.ensureActive()
|
||||
if (pBook.name == nBook.name && pBook.author == nBook.author) {
|
||||
pBook.addOrigin(nBook.origin)
|
||||
hasSame = true
|
||||
}
|
||||
}
|
||||
if (!hasSame) {
|
||||
otherData.add(nBook)
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
equalData.sortByDescending { it.origins.size }
|
||||
equalData.addAll(containsData.sortedByDescending { it.origins.size })
|
||||
if (!precision) {
|
||||
equalData.addAll(otherData)
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
searchBooks = equalData
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSearch() {
|
||||
close()
|
||||
callBack.onSearchCancel()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
searchJob?.cancel()
|
||||
searchPool?.close()
|
||||
searchPool = null
|
||||
mSearchId = 0L
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
fun getSearchScope(): SearchScope
|
||||
fun onSearchStart()
|
||||
fun onSearchSuccess(searchBooks: List<SearchBook>)
|
||||
fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean)
|
||||
fun onSearchCancel(exception: Throwable? = null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
package io.legato.kazusa.model.webBook
|
||||
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookChapter
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.data.entities.BookSourcePart
|
||||
import io.legato.kazusa.data.entities.SearchBook
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.book.addType
|
||||
import io.legato.kazusa.help.book.removeAllBookType
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import io.legato.kazusa.help.http.StrResponse
|
||||
import io.legato.kazusa.help.source.getBookType
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.model.analyzeRule.RuleData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object WebBook {
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
fun searchBook(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
key: String,
|
||||
page: Int? = 1,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
executeContext: CoroutineContext = Dispatchers.Main,
|
||||
): Coroutine<ArrayList<SearchBook>> {
|
||||
return Coroutine.async(scope, context, start = start, executeContext = executeContext) {
|
||||
searchBookAwait(bookSource, key, page)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchBookAwait(
|
||||
bookSource: BookSource,
|
||||
key: String,
|
||||
page: Int? = 1,
|
||||
filter: ((name: String, author: String) -> Boolean)? = null,
|
||||
shouldBreak: ((size: Int) -> Boolean)? = null
|
||||
): ArrayList<SearchBook> {
|
||||
val searchUrl = bookSource.searchUrl
|
||||
if (searchUrl.isNullOrBlank()) {
|
||||
throw NoStackTraceException("搜索url不能为空")
|
||||
}
|
||||
val ruleData = RuleData()
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = searchUrl,
|
||||
key = key,
|
||||
page = page,
|
||||
baseUrl = bookSource.bookSourceUrl,
|
||||
source = bookSource,
|
||||
ruleData = ruleData,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
var res = analyzeUrl.getStrResponseAwait()
|
||||
//检测书源是否已登录
|
||||
bookSource.loginCheckJs?.let { checkJs ->
|
||||
if (checkJs.isNotBlank()) {
|
||||
res = analyzeUrl.evalJS(checkJs, res) as StrResponse
|
||||
}
|
||||
}
|
||||
checkRedirect(bookSource, res)
|
||||
return BookList.analyzeBookList(
|
||||
bookSource = bookSource,
|
||||
ruleData = ruleData,
|
||||
analyzeUrl = analyzeUrl,
|
||||
baseUrl = res.url,
|
||||
body = res.body,
|
||||
isSearch = true,
|
||||
isRedirect = res.raw.priorResponse?.isRedirect == true,
|
||||
filter = filter,
|
||||
shouldBreak = shouldBreak
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发现
|
||||
*/
|
||||
fun exploreBook(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
url: String,
|
||||
page: Int? = 1,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
): Coroutine<List<SearchBook>> {
|
||||
return Coroutine.async(scope, context) {
|
||||
exploreBookAwait(bookSource, url, page)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exploreBookAwait(
|
||||
bookSource: BookSource,
|
||||
url: String,
|
||||
page: Int? = 1,
|
||||
): ArrayList<SearchBook> {
|
||||
val ruleData = RuleData()
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = url,
|
||||
page = page,
|
||||
baseUrl = bookSource.bookSourceUrl,
|
||||
source = bookSource,
|
||||
ruleData = ruleData,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
var res = analyzeUrl.getStrResponseAwait()
|
||||
//检测书源是否已登录
|
||||
bookSource.loginCheckJs?.let { checkJs ->
|
||||
if (checkJs.isNotBlank()) {
|
||||
res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse
|
||||
}
|
||||
}
|
||||
checkRedirect(bookSource, res)
|
||||
return BookList.analyzeBookList(
|
||||
bookSource = bookSource,
|
||||
ruleData = ruleData,
|
||||
analyzeUrl = analyzeUrl,
|
||||
baseUrl = res.url,
|
||||
body = res.body,
|
||||
isSearch = false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 书籍信息
|
||||
*/
|
||||
fun getBookInfo(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
canReName: Boolean = true,
|
||||
): Coroutine<Book> {
|
||||
return Coroutine.async(scope, context) {
|
||||
getBookInfoAwait(bookSource, book, canReName)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBookInfoAwait(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
canReName: Boolean = true,
|
||||
): Book {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
if (!book.infoHtml.isNullOrEmpty()) {
|
||||
BookInfo.analyzeBookInfo(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
baseUrl = book.bookUrl,
|
||||
redirectUrl = book.bookUrl,
|
||||
body = book.infoHtml,
|
||||
canReName = canReName
|
||||
)
|
||||
} else {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = book.bookUrl,
|
||||
baseUrl = bookSource.bookSourceUrl,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
var res = analyzeUrl.getStrResponseAwait()
|
||||
//检测书源是否已登录
|
||||
bookSource.loginCheckJs?.let { checkJs ->
|
||||
if (checkJs.isNotBlank()) {
|
||||
res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse
|
||||
}
|
||||
}
|
||||
checkRedirect(bookSource, res)
|
||||
BookInfo.analyzeBookInfo(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
baseUrl = book.bookUrl,
|
||||
redirectUrl = res.url,
|
||||
body = res.body,
|
||||
canReName = canReName
|
||||
)
|
||||
}
|
||||
return book
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
fun getChapterList(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
runPerJs: Boolean = false,
|
||||
context: CoroutineContext = Dispatchers.IO
|
||||
): Coroutine<List<BookChapter>> {
|
||||
return Coroutine.async(scope, context) {
|
||||
getChapterListAwait(bookSource, book, runPerJs).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runPreUpdateJs(bookSource: BookSource, book: Book): Result<Unit> {
|
||||
return kotlin.runCatching {
|
||||
val preUpdateJs = bookSource.ruleToc?.preUpdateJs
|
||||
if (!preUpdateJs.isNullOrBlank()) {
|
||||
AnalyzeRule(book, bookSource, true)
|
||||
.setCoroutineContext(coroutineContext)
|
||||
.evalJS(preUpdateJs)
|
||||
}
|
||||
}.onFailure {
|
||||
coroutineContext.ensureActive()
|
||||
AppLog.put("执行preUpdateJs规则失败 书源:${bookSource.bookSourceName}", it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChapterListAwait(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
runPerJs: Boolean = false
|
||||
): Result<List<BookChapter>> {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
return kotlin.runCatching {
|
||||
if (runPerJs) {
|
||||
runPreUpdateJs(bookSource, book).getOrThrow()
|
||||
}
|
||||
if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
|
||||
BookChapterList.analyzeChapterList(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
baseUrl = book.tocUrl,
|
||||
redirectUrl = book.tocUrl,
|
||||
body = book.tocHtml
|
||||
)
|
||||
} else {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = book.tocUrl,
|
||||
baseUrl = book.bookUrl,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
var res = analyzeUrl.getStrResponseAwait()
|
||||
//检测书源是否已登录
|
||||
bookSource.loginCheckJs?.let { checkJs ->
|
||||
if (checkJs.isNotBlank()) {
|
||||
res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse
|
||||
}
|
||||
}
|
||||
checkRedirect(bookSource, res)
|
||||
BookChapterList.analyzeChapterList(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
baseUrl = book.tocUrl,
|
||||
redirectUrl = res.url,
|
||||
body = res.body
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 章节内容
|
||||
*/
|
||||
fun getContent(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
nextChapterUrl: String? = null,
|
||||
needSave: Boolean = true,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
executeContext: CoroutineContext = Dispatchers.Main,
|
||||
semaphore: Semaphore? = null,
|
||||
): Coroutine<String> {
|
||||
return Coroutine.async(
|
||||
scope,
|
||||
context,
|
||||
start = start,
|
||||
executeContext = executeContext,
|
||||
semaphore = semaphore
|
||||
) {
|
||||
getContentAwait(bookSource, book, bookChapter, nextChapterUrl, needSave)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContentAwait(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
nextChapterUrl: String? = null,
|
||||
needSave: Boolean = true
|
||||
): String {
|
||||
if (bookSource.getContentRule().content.isNullOrEmpty()) {
|
||||
Debug.log(bookSource.bookSourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
|
||||
return bookChapter.url
|
||||
}
|
||||
if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {
|
||||
Debug.log(bookSource.bookSourceUrl, "⇒一级目录正文不解析规则")
|
||||
return bookChapter.tag ?: ""
|
||||
}
|
||||
return if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
|
||||
BookContent.analyzeContent(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
bookChapter = bookChapter,
|
||||
baseUrl = bookChapter.getAbsoluteURL(),
|
||||
redirectUrl = bookChapter.getAbsoluteURL(),
|
||||
body = book.tocHtml,
|
||||
nextChapterUrl = nextChapterUrl,
|
||||
needSave = needSave
|
||||
)
|
||||
} else {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = bookChapter.getAbsoluteURL(),
|
||||
baseUrl = book.tocUrl,
|
||||
source = bookSource,
|
||||
ruleData = book,
|
||||
chapter = bookChapter,
|
||||
coroutineContext = coroutineContext
|
||||
)
|
||||
var res = analyzeUrl.getStrResponseAwait(
|
||||
jsStr = bookSource.getContentRule().webJs,
|
||||
sourceRegex = bookSource.getContentRule().sourceRegex
|
||||
)
|
||||
//检测书源是否已登录
|
||||
bookSource.loginCheckJs?.let { checkJs ->
|
||||
if (checkJs.isNotBlank()) {
|
||||
res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse
|
||||
}
|
||||
}
|
||||
checkRedirect(bookSource, res)
|
||||
BookContent.analyzeContent(
|
||||
bookSource = bookSource,
|
||||
book = book,
|
||||
bookChapter = bookChapter,
|
||||
baseUrl = bookChapter.getAbsoluteURL(),
|
||||
redirectUrl = res.url,
|
||||
body = res.body,
|
||||
nextChapterUrl = nextChapterUrl,
|
||||
needSave = needSave
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 精准搜索
|
||||
*/
|
||||
fun preciseSearch(
|
||||
scope: CoroutineScope,
|
||||
bookSourceParts: List<BookSourcePart>,
|
||||
name: String,
|
||||
author: String,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
semaphore: Semaphore? = null,
|
||||
): Coroutine<Pair<Book, BookSource>> {
|
||||
return Coroutine.async(scope, context, semaphore = semaphore) {
|
||||
for (s in bookSourceParts) {
|
||||
val source = s.getBookSource() ?: continue
|
||||
val book = preciseSearchAwait(source, name, author).getOrNull()
|
||||
if (book != null) {
|
||||
return@async Pair(book, source)
|
||||
}
|
||||
}
|
||||
throw NoStackTraceException("没有搜索到<$name>$author")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preciseSearchAwait(
|
||||
bookSource: BookSource,
|
||||
name: String,
|
||||
author: String,
|
||||
): Result<Book> {
|
||||
return kotlin.runCatching {
|
||||
coroutineContext.ensureActive()
|
||||
searchBookAwait(
|
||||
bookSource, name,
|
||||
filter = { fName, fAuthor -> fName == name && fAuthor == author },
|
||||
shouldBreak = { it > 0 }
|
||||
).firstOrNull()?.let { searchBook ->
|
||||
coroutineContext.ensureActive()
|
||||
return@runCatching searchBook.toBook()
|
||||
}
|
||||
throw NoStackTraceException("未搜索到 $name($author) 书籍")
|
||||
}.onFailure {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测重定向
|
||||
*/
|
||||
private fun checkRedirect(bookSource: BookSource, response: StrResponse) {
|
||||
response.raw.priorResponse?.let {
|
||||
if (it.isRedirect) {
|
||||
Debug.log(bookSource.bookSourceUrl, "≡检测到重定向(${it.code})")
|
||||
Debug.log(bookSource.bookSourceUrl, "┌重定向后地址")
|
||||
Debug.log(bookSource.bookSourceUrl, "└${response.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user