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,21 @@
package io.legato.kazusa.help.source
import io.legato.kazusa.constant.SourceType
import io.legato.kazusa.data.entities.BaseSource
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.model.SharedJsScope
import org.mozilla.javascript.Scriptable
import kotlin.coroutines.CoroutineContext
fun BaseSource.getShareScope(coroutineContext: CoroutineContext? = null): Scriptable? {
return SharedJsScope.getScope(jsLib, coroutineContext)
}
fun BaseSource.getSourceType(): Int {
return when (this) {
is BookSource -> SourceType.book
is RssSource -> SourceType.rss
else -> error("unknown source type: ${this::class.simpleName}.")
}
}
@@ -0,0 +1,121 @@
package io.legato.kazusa.help.source
import com.script.rhino.runScriptWithContext
import io.legato.kazusa.constant.BookSourceType
import io.legato.kazusa.constant.BookType
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.BookSourcePart
import io.legato.kazusa.data.entities.rule.ExploreKind
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.MD5Utils
import io.legato.kazusa.utils.fromJsonArray
import io.legato.kazusa.utils.isJsonArray
import io.legato.kazusa.utils.printOnDebug
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
/**
* 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新
*/
private val mutexMap by lazy { hashMapOf<String, Mutex>() }
private val exploreKindsMap by lazy { ConcurrentHashMap<String, List<ExploreKind>>() }
private val aCache by lazy { ACache.get("explore") }
private fun BookSource.getExploreKindsKey(): String {
return MD5Utils.md5Encode(bookSourceUrl + exploreUrl)
}
private fun BookSourcePart.getExploreKindsKey(): String {
return getBookSource()!!.getExploreKindsKey()
}
suspend fun BookSourcePart.exploreKinds(): List<ExploreKind> {
return getBookSource()!!.exploreKinds()
}
suspend fun BookSource.exploreKinds(): List<ExploreKind> {
val exploreKindsKey = getExploreKindsKey()
exploreKindsMap[exploreKindsKey]?.let { return it }
val exploreUrl = exploreUrl
if (exploreUrl.isNullOrBlank()) {
return emptyList()
}
val mutex = mutexMap[bookSourceUrl] ?: Mutex().apply { mutexMap[bookSourceUrl] = this }
mutex.withLock {
exploreKindsMap[exploreKindsKey]?.let { return it }
val kinds = arrayListOf<ExploreKind>()
withContext(Dispatchers.IO) {
kotlin.runCatching {
var ruleStr = exploreUrl
if (exploreUrl.startsWith("<js>", true)
|| exploreUrl.startsWith("@js:", true)
) {
ruleStr = aCache.getAsString(exploreKindsKey)
if (ruleStr.isNullOrBlank()) {
val jsStr = if (exploreUrl.startsWith("@")) {
exploreUrl.substring(4)
} else {
exploreUrl.substring(4, exploreUrl.lastIndexOf("<"))
}
ruleStr = runScriptWithContext {
evalJS(jsStr).toString().trim()
}
aCache.put(exploreKindsKey, ruleStr)
}
}
if (ruleStr.isJsonArray()) {
GSON.fromJsonArray<ExploreKind>(ruleStr).getOrThrow().let {
kinds.addAll(it)
}
} else {
ruleStr.split("(&&|\n)+".toRegex()).forEach { kindStr ->
val kindCfg = kindStr.split("::")
kinds.add(ExploreKind(kindCfg.first(), kindCfg.getOrNull(1)))
}
}
}.onFailure {
kinds.add(ExploreKind("ERROR:${it.localizedMessage}", it.stackTraceToString()))
it.printOnDebug()
}
}
exploreKindsMap[exploreKindsKey] = kinds
return kinds
}
}
suspend fun BookSourcePart.clearExploreKindsCache() {
withContext(Dispatchers.IO) {
val exploreKindsKey = getExploreKindsKey()
aCache.remove(exploreKindsKey)
exploreKindsMap.remove(exploreKindsKey)
}
}
suspend fun BookSource.clearExploreKindsCache() {
withContext(Dispatchers.IO) {
val exploreKindsKey = getExploreKindsKey()
aCache.remove(exploreKindsKey)
exploreKindsMap.remove(exploreKindsKey)
}
}
fun BookSource.exploreKindsJson(): String {
val exploreKindsKey = getExploreKindsKey()
return aCache.getAsString(exploreKindsKey)?.takeIf { it.isJsonArray() }
?: exploreUrl.takeIf { it.isJsonArray() }
?: ""
}
fun BookSource.getBookType(): Int {
return when (bookSourceType) {
BookSourceType.file -> BookType.text or BookType.webFile
BookSourceType.image -> BookType.image
BookSourceType.audio -> BookType.audio
else -> BookType.text
}
}
@@ -0,0 +1,62 @@
package io.legato.kazusa.help.source
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.MD5Utils
import io.legato.kazusa.utils.NetworkUtils
import com.script.rhino.runScriptWithContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private val aCache by lazy { ACache.get("rssSortUrl") }
private fun RssSource.getSortUrlsKey(): String {
return MD5Utils.md5Encode(sourceUrl + sortUrl)
}
suspend fun RssSource.sortUrls(): List<Pair<String, String>> {
return arrayListOf<Pair<String, String>>().apply {
val sortUrlsKey = getSortUrlsKey()
withContext(Dispatchers.IO) {
kotlin.runCatching {
var str = sortUrl
if (sortUrl?.startsWith("<js>", false) == true
|| sortUrl?.startsWith("@js:", false) == true
) {
str = aCache.getAsString(sortUrlsKey)
if (str.isNullOrBlank()) {
val jsStr = if (sortUrl!!.startsWith("@")) {
sortUrl!!.substring(4)
} else {
sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<"))
}
str = runScriptWithContext {
evalJS(jsStr).toString()
}
aCache.put(sortUrlsKey, str)
}
}
str?.split("(&&|\n)+".toRegex())?.forEach { sort ->
val name = sort.substringBefore("::")
val url = sort.substringAfter("::", "")
if (url.isNotEmpty()) {
if (url.startsWith("{{")) {
add(Pair(name, url))
} else {
add(Pair(name, NetworkUtils.getAbsoluteURL(sourceUrl, url)))
}
}
}
if (isEmpty()) {
add(Pair("", sourceUrl))
}
}
}
}
}
suspend fun RssSource.removeSortCache() {
withContext(Dispatchers.IO) {
aCache.remove(getSortUrlsKey())
}
}
@@ -0,0 +1,178 @@
package io.legato.kazusa.help.source
import io.legato.kazusa.constant.SourceType
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.BaseSource
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.BookSourcePart
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.help.AppCacheManager
import io.legato.kazusa.help.config.SourceConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.model.AudioPlay
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.model.ReadManga
import io.legato.kazusa.utils.EncoderUtils
import io.legato.kazusa.utils.NetworkUtils
import io.legato.kazusa.utils.splitNotBlank
import io.legato.kazusa.utils.toastOnUi
import splitties.init.appCtx
object SourceHelp {
private val list18Plus by lazy {
try {
return@lazy String(appCtx.assets.open("18PlusList.txt").readBytes())
.splitNotBlank("\n").map {
EncoderUtils.base64Decode(it)
}.toHashSet()
} catch (_: Exception) {
return@lazy emptySet()
}
}
fun getSource(key: String?): BaseSource? {
key ?: return null
if (ReadBook.bookSource?.bookSourceUrl == key) {
return ReadBook.bookSource
} else if (AudioPlay.bookSource?.bookSourceUrl == key) {
return AudioPlay.bookSource
} else if (ReadManga.bookSource?.bookSourceUrl == key) {
return ReadManga.bookSource
}
return appDb.bookSourceDao.getBookSource(key)
?: appDb.rssSourceDao.getByKey(key)
}
fun getSource(key: String?, @SourceType.Type type: Int): BaseSource? {
key ?: return null
return when (type) {
SourceType.book -> appDb.bookSourceDao.getBookSource(key)
SourceType.rss -> appDb.rssSourceDao.getByKey(key)
else -> null
}
}
fun deleteSource(key: String, @SourceType.Type type: Int) {
when (type) {
SourceType.book -> deleteBookSource(key)
SourceType.rss -> deleteRssSource(key)
}
}
fun deleteBookSourceParts(sources: List<BookSourcePart>) {
appDb.runInTransaction {
sources.forEach {
deleteBookSourceInternal(it.bookSourceUrl)
}
}
AppCacheManager.clearSourceVariables()
}
fun deleteBookSources(sources: List<BookSource>) {
appDb.runInTransaction {
sources.forEach {
deleteBookSourceInternal(it.bookSourceUrl)
}
}
AppCacheManager.clearSourceVariables()
}
private fun deleteBookSourceInternal(key: String) {
appDb.bookSourceDao.delete(key)
appDb.cacheDao.deleteSourceVariables(key)
SourceConfig.removeSource(key)
}
fun deleteBookSource(key: String) {
deleteBookSourceInternal(key)
AppCacheManager.clearSourceVariables()
}
fun deleteRssSources(sources: List<RssSource>) {
appDb.runInTransaction {
sources.forEach {
deleteRssSourceInternal(it.sourceUrl)
}
}
AppCacheManager.clearSourceVariables()
}
private fun deleteRssSourceInternal(key: String) {
appDb.rssSourceDao.delete(key)
appDb.rssArticleDao.delete(key)
appDb.cacheDao.deleteSourceVariables(key)
}
fun deleteRssSource(key: String) {
deleteRssSourceInternal(key)
AppCacheManager.clearSourceVariables()
}
fun enableSource(key: String, @SourceType.Type type: Int, enable: Boolean) {
when (type) {
SourceType.book -> appDb.bookSourceDao.enable(key, enable)
SourceType.rss -> appDb.rssSourceDao.enable(key, enable)
}
}
fun insertRssSource(vararg rssSources: RssSource) {
val rssSourcesGroup = rssSources.groupBy {
is18Plus(it.sourceUrl)
}
rssSourcesGroup[true]?.forEach {
appCtx.toastOnUi("${it.sourceName}是18+网址,禁止导入.")
}
rssSourcesGroup[false]?.let {
appDb.rssSourceDao.insert(*it.toTypedArray())
}
}
fun insertBookSource(vararg bookSources: BookSource) {
val bookSourcesGroup = bookSources.groupBy {
is18Plus(it.bookSourceUrl)
}
bookSourcesGroup[true]?.forEach {
appCtx.toastOnUi("${it.bookSourceName}是18+网址,禁止导入.")
}
bookSourcesGroup[false]?.let {
appDb.bookSourceDao.insert(*it.toTypedArray())
}
Coroutine.async {
adjustSortNumber()
}
}
private fun is18Plus(url: String?): Boolean {
if (list18Plus.isEmpty()) {
return false
}
url ?: return false
val baseUrl = NetworkUtils.getBaseUrl(url) ?: return false
kotlin.runCatching {
val host = baseUrl.split("//", ".").let {
if (it.size > 2) "${it[it.lastIndex - 1]}.${it.last()}" else return false
}
return list18Plus.contains(host)
}
return false
}
/**
* 调整排序序号
*/
fun adjustSortNumber() {
if (
appDb.bookSourceDao.maxOrder > 99999
|| appDb.bookSourceDao.minOrder < -99999
|| appDb.bookSourceDao.hasDuplicateOrder
) {
val sources = appDb.bookSourceDao.allPart
sources.forEachIndexed { index, bookSource ->
bookSource.customOrder = index
}
appDb.bookSourceDao.upOrder(sources)
}
}
}
@@ -0,0 +1,117 @@
package io.legato.kazusa.help.source
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.entities.BaseSource
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.help.CacheManager
import io.legato.kazusa.help.IntentData
import io.legato.kazusa.ui.association.VerificationCodeActivity
import io.legato.kazusa.ui.browser.WebViewActivity
import io.legato.kazusa.utils.isMainThread
import io.legato.kazusa.utils.startActivity
import splitties.init.appCtx
import java.util.concurrent.locks.LockSupport
import kotlin.time.Duration.Companion.minutes
/**
* 源验证
*/
object SourceVerificationHelp {
private val waitTime = 1.minutes.inWholeNanoseconds
private fun getVerificationResultKey(source: BaseSource) =
getVerificationResultKey(source.getKey())
private fun getVerificationResultKey(sourceKey: String) = "${sourceKey}_verificationResult"
/**
* 获取书源验证结果
* 图片验证码 防爬 滑动验证码 点击字符 等等
*/
fun getVerificationResult(
source: BaseSource?,
url: String,
title: String,
useBrowser: Boolean,
refetchAfterSuccess: Boolean = true
): String {
source
?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
if (isMainThread) {
error("getVerificationResult must be called on a background thread")
}
clearResult(source.getKey())
if (!useBrowser) {
appCtx.startActivity<VerificationCodeActivity> {
putExtra("imageUrl", url)
putExtra("sourceOrigin", source.getKey())
putExtra("sourceName", source.getTag())
putExtra("sourceType", source.getSourceType())
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
}
} else {
startBrowser(source, url, title, true, refetchAfterSuccess)
}
var waitUserInput = false
while (getResult(source.getKey()) == null) {
if (!waitUserInput) {
AppLog.putDebug("等待返回验证结果...")
waitUserInput = true
}
LockSupport.parkNanos(this, waitTime)
}
return getResult(source.getKey())!!.let {
it.ifBlank {
throw NoStackTraceException("验证结果为空")
}
}
}
/**
* 启动内置浏览器
* @param saveResult 保存网页源代码到数据库
*/
fun startBrowser(
source: BaseSource?,
url: String,
title: String,
saveResult: Boolean? = false,
refetchAfterSuccess: Boolean? = true
) {
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
appCtx.startActivity<WebViewActivity> {
putExtra("title", title)
putExtra("url", url)
putExtra("sourceOrigin", source.getKey())
putExtra("sourceName", source.getTag())
putExtra("sourceType", source.getSourceType())
putExtra("sourceVerificationEnable", saveResult)
putExtra("refetchAfterSuccess", refetchAfterSuccess)
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
}
}
fun checkResult(sourceKey: String) {
getResult(sourceKey) ?: setResult(sourceKey, "")
val thread = IntentData.get<Thread>(getVerificationResultKey(sourceKey))
LockSupport.unpark(thread)
}
fun setResult(sourceKey: String, result: String?) {
CacheManager.putMemory(getVerificationResultKey(sourceKey), result ?: "")
}
fun getResult(sourceKey: String): String? {
return CacheManager.get(getVerificationResultKey(sourceKey))
}
fun clearResult(sourceKey: String) {
CacheManager.delete(getVerificationResultKey(sourceKey))
}
}