This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
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}")
}
}
}
}