This commit is contained in:
kunfei
2023-03-21 21:23:15 +08:00
parent aa76fb9c6f
commit 1990ff8dc8
10 changed files with 376 additions and 417 deletions
@@ -3,15 +3,19 @@ package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import android.text.TextUtils import android.text.TextUtils
import androidx.room.* import androidx.room.*
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookSourceType import io.legado.app.constant.BookSourceType
import io.legado.app.data.entities.rule.* import io.legado.app.data.entities.rule.*
import io.legado.app.help.source.SourceAnalyzer
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Type
@Suppress("unused") @Suppress("unused")
@Parcelize @Parcelize
@@ -222,17 +226,46 @@ data class BookSource(
companion object { companion object {
private val gson by lazy {
GSON.newBuilder()
.registerTypeAdapter(String::class.java, StringJsonDeserializer())
.registerTypeAdapter(ExploreRule::class.java, ExploreRule.jsonDeserializer)
.registerTypeAdapter(SearchRule::class.java, SearchRule.jsonDeserializer)
.registerTypeAdapter(BookInfoRule::class.java, BookInfoRule.jsonDeserializer)
.registerTypeAdapter(TocRule::class.java, TocRule.jsonDeserializer)
.registerTypeAdapter(ContentRule::class.java, ContentRule.jsonDeserializer)
.registerTypeAdapter(ReviewRule::class.java, ReviewRule.jsonDeserializer)
.create()
}
fun fromJson(json: String): Result<BookSource> { fun fromJson(json: String): Result<BookSource> {
return SourceAnalyzer.jsonToBookSource(json) return gson.fromJsonObject(json)
} }
fun fromJsonArray(json: String): Result<MutableList<BookSource>> { fun fromJsonArray(json: String): Result<List<BookSource>> {
return SourceAnalyzer.jsonToBookSources(json) return gson.fromJsonArray(json)
} }
fun fromJsonArray(inputStream: InputStream): Result<MutableList<BookSource>> { fun fromJsonArray(inputStream: InputStream): Result<List<BookSource>> {
return SourceAnalyzer.jsonToBookSources(inputStream) return gson.fromJsonArray(inputStream)
} }
class StringJsonDeserializer : JsonDeserializer<String?> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext?
): String? {
return when {
json.isJsonPrimitive -> json.asString
json.isJsonNull -> null
else -> json.toString()
}
}
}
} }
class Converters { class Converters {
@@ -176,7 +176,7 @@ data class RssSource(
companion object { companion object {
private val gson by lazy { private val gson by lazy {
GSON.newBuilder() GSON.newBuilder()
.registerTypeAdapter(String::class.java, RssJsonDeserializer()) .registerTypeAdapter(String::class.java, StringJsonDeserializer())
.create() .create()
} }
@@ -187,9 +187,8 @@ data class RssSource(
fun fromJsonArray(jsonArray: String): Result<List<RssSource>> { fun fromJsonArray(jsonArray: String): Result<List<RssSource>> {
return gson.fromJsonArray(jsonArray) return gson.fromJsonArray(jsonArray)
} }
}
class RssJsonDeserializer : JsonDeserializer<String?> { class StringJsonDeserializer : JsonDeserializer<String?> {
override fun deserialize( override fun deserialize(
json: JsonElement, json: JsonElement,
@@ -205,4 +204,6 @@ data class RssSource(
} }
}
} }
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -20,4 +22,18 @@ data class BookInfoRule(
var wordCount: String? = null, var wordCount: String? = null,
var canReName: String? = null, var canReName: String? = null,
var downloadUrls: String? = null var downloadUrls: String? = null
) : Parcelable ) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<BookInfoRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, BookInfoRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, BookInfoRule::class.java)
else -> null
}
}
}
}
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -16,4 +18,20 @@ data class ContentRule(
var imageStyle: String? = null, //默认大小居中,FULL最大宽度 var imageStyle: String? = null, //默认大小居中,FULL最大宽度
var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
var payAction: String? = null, //购买操作,js或者包含{{js}}的url var payAction: String? = null, //购买操作,js或者包含{{js}}的url
) : Parcelable ) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<ContentRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, ContentRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, ContentRule::class.java)
else -> null
}
}
}
}
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -18,4 +20,18 @@ data class ExploreRule(
override var bookUrl: String? = null, override var bookUrl: String? = null,
override var coverUrl: String? = null, override var coverUrl: String? = null,
override var wordCount: String? = null override var wordCount: String? = null
) : BookListRule, Parcelable ) : BookListRule, Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<ExploreRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, ExploreRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, ExploreRule::class.java)
else -> null
}
}
}
}
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -17,4 +19,18 @@ data class ReviewRule(
var postReviewUrl: String? = null, // 发送回复URL var postReviewUrl: String? = null, // 发送回复URL
var postQuoteUrl: String? = null, // 发送回复段评URL var postQuoteUrl: String? = null, // 发送回复段评URL
var deleteUrl: String? = null, // 删除段评URL var deleteUrl: String? = null, // 删除段评URL
): Parcelable ) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<ReviewRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, ReviewRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, ReviewRule::class.java)
else -> null
}
}
}
}
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -20,4 +22,18 @@ data class SearchRule(
override var bookUrl: String? = null, override var bookUrl: String? = null,
override var coverUrl: String? = null, override var coverUrl: String? = null,
override var wordCount: String? = null override var wordCount: String? = null
) : BookListRule, Parcelable ) : BookListRule, Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<SearchRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, SearchRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, SearchRule::class.java)
else -> null
}
}
}
}
@@ -1,6 +1,8 @@
package io.legado.app.data.entities.rule package io.legado.app.data.entities.rule
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legado.app.utils.GSON
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -14,4 +16,18 @@ data class TocRule(
var isPay: String? = null, var isPay: String? = null,
var updateTime: String? = null, var updateTime: String? = null,
var nextTocUrl: String? = null var nextTocUrl: String? = null
) : Parcelable ) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<TocRule?> { json, _, _ ->
when {
json.isJsonObject -> GSON.fromJson(json, TocRule::class.java)
json.isJsonPrimitive -> GSON.fromJson(json.asString, TocRule::class.java)
else -> null
}
}
}
}
@@ -1,389 +0,0 @@
package io.legado.app.help.source
import androidx.annotation.Keep
import com.jayway.jsonpath.JsonPath
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.constant.BookSourceType
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.*
import io.legado.app.exception.NoStackTraceException
import io.legado.app.utils.*
import java.io.InputStream
import java.util.regex.Pattern
import splitties.init.appCtx
@Suppress("RegExpRedundantEscape")
object SourceAnalyzer {
private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE)
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
fun jsonToBookSources(json: String): Result<MutableList<BookSource>> {
return kotlin.runCatching {
val bookSources = mutableListOf<BookSource>()
when {
json.isJsonArray() -> {
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = jsonPath.parse(item)
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
bookSources.add(it)
}
}
}
json.isJsonObject() -> {
jsonToBookSource(json).getOrThrow().let {
bookSources.add(it)
}
}
else -> {
throw NoStackTraceException(appCtx.getString(R.string.wrong_format))
}
}
bookSources
}
}
fun jsonToBookSources(inputStream: InputStream): Result<MutableList<BookSource>> {
return kotlin.runCatching {
val bookSources = mutableListOf<BookSource>()
val documentContext = jsonPath.parse(inputStream)
try {
val items: List<Map<String, Any>> = documentContext.read("$")
items.forEach {
val jsonItem = jsonPath.parse(it)
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
bookSources.add(it)
}
}
} catch (_: Exception) {
val item: Map<String, Any> = documentContext.read("$")
val jsonItem = jsonPath.parse(item)
jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {
bookSources.add(it)
}
}
bookSources
}.onFailure {
throw NoStackTraceException(appCtx.getString(R.string.wrong_format))
}
}
fun jsonToBookSource(json: String): Result<BookSource> {
val source = BookSource()
val sourceAny = GSON.fromJsonObject<BookSourceAny>(json.trim())
.onFailure {
AppLog.put(appCtx.getString(R.string.wrong_format), it)
}.getOrNull()
return kotlin.runCatching {
if (sourceAny?.ruleToc == null) {
source.apply {
val jsonItem = jsonPath.parse(json.trim())
bookSourceUrl = jsonItem.readString("bookSourceUrl")
?: throw NoStackTraceException(appCtx.getString(R.string.wrong_format))
bookSourceName = jsonItem.readString("bookSourceName") ?: ""
bookSourceGroup = jsonItem.readString("bookSourceGroup")
loginUrl = jsonItem.readString("loginUrl")
loginUi = jsonItem.readString("loginUi")
loginCheckJs = jsonItem.readString("loginCheckJs")
coverDecodeJs = jsonItem.readString("coverDecodeJs")
bookSourceComment = jsonItem.readString("bookSourceComment") ?: ""
bookUrlPattern = jsonItem.readString("ruleBookUrlPattern")
customOrder = jsonItem.readInt("serialNumber") ?: 0
header = uaToHeader(jsonItem.readString("httpUserAgent"))
searchUrl = toNewUrl(jsonItem.readString("ruleSearchUrl"))
exploreUrl = toNewUrls(jsonItem.readString("ruleFindUrl"))
bookSourceType =
if (jsonItem.readString("bookSourceType") == "AUDIO") BookSourceType.audio else BookSourceType.default
enabled = jsonItem.readBool("enable") ?: true
if (exploreUrl.isNullOrBlank()) {
enabledExplore = false
}
ruleSearch = SearchRule(
bookList = toNewRule(jsonItem.readString("ruleSearchList")),
name = toNewRule(jsonItem.readString("ruleSearchName")),
author = toNewRule(jsonItem.readString("ruleSearchAuthor")),
intro = toNewRule(jsonItem.readString("ruleSearchIntroduce")),
kind = toNewRule(jsonItem.readString("ruleSearchKind")),
bookUrl = toNewRule(jsonItem.readString("ruleSearchNoteUrl")),
coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter"))
)
ruleExplore = ExploreRule(
bookList = toNewRule(jsonItem.readString("ruleFindList")),
name = toNewRule(jsonItem.readString("ruleFindName")),
author = toNewRule(jsonItem.readString("ruleFindAuthor")),
intro = toNewRule(jsonItem.readString("ruleFindIntroduce")),
kind = toNewRule(jsonItem.readString("ruleFindKind")),
bookUrl = toNewRule(jsonItem.readString("ruleFindNoteUrl")),
coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter"))
)
ruleBookInfo = BookInfoRule(
init = toNewRule(jsonItem.readString("ruleBookInfoInit")),
name = toNewRule(jsonItem.readString("ruleBookName")),
author = toNewRule(jsonItem.readString("ruleBookAuthor")),
intro = toNewRule(jsonItem.readString("ruleIntroduce")),
kind = toNewRule(jsonItem.readString("ruleBookKind")),
coverUrl = toNewRule(jsonItem.readString("ruleCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")),
tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl"))
)
ruleToc = TocRule(
chapterList = toNewRule(jsonItem.readString("ruleChapterList")),
chapterName = toNewRule(jsonItem.readString("ruleChapterName")),
chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")),
nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext"))
)
var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: ""
if (content.startsWith("$") && !content.startsWith("$.")) {
content = content.substring(1)
}
ruleContent = ContentRule(
content = content,
replaceRegex = toNewRule(jsonItem.readString("ruleBookContentReplace")),
nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext"))
)
}
} else {
source.bookSourceUrl = sourceAny.bookSourceUrl
source.bookSourceName = sourceAny.bookSourceName
source.bookSourceGroup = sourceAny.bookSourceGroup
source.bookSourceType = sourceAny.bookSourceType
source.bookUrlPattern = sourceAny.bookUrlPattern
source.customOrder = sourceAny.customOrder
source.enabled = sourceAny.enabled
source.enabledExplore = sourceAny.enabledExplore
source.enabledCookieJar = sourceAny.enabledCookieJar
source.enabledReview = sourceAny.enabledReview
source.concurrentRate = sourceAny.concurrentRate
source.header = sourceAny.header
source.loginUrl = when (sourceAny.loginUrl) {
null -> null
is String -> sourceAny.loginUrl.toString()
else -> JsonPath.parse(sourceAny.loginUrl).readString("url")
}
source.loginUi = if (sourceAny.loginUi is List<*>) {
GSON.toJson(sourceAny.loginUi)
} else {
sourceAny.loginUi?.toString()
}
source.loginCheckJs = sourceAny.loginCheckJs
source.coverDecodeJs = sourceAny.coverDecodeJs
source.bookSourceComment = sourceAny.bookSourceComment
source.variableComment = sourceAny.variableComment
source.lastUpdateTime = sourceAny.lastUpdateTime
source.respondTime = sourceAny.respondTime
source.weight = sourceAny.weight
source.exploreUrl = sourceAny.exploreUrl
source.ruleExplore = if (sourceAny.ruleExplore is String) {
GSON.fromJsonObject<ExploreRule>(sourceAny.ruleExplore.toString())
.getOrNull()
} else {
GSON.fromJsonObject<ExploreRule>(GSON.toJson(sourceAny.ruleExplore))
.getOrNull()
}
source.searchUrl = sourceAny.searchUrl
source.ruleSearch = if (sourceAny.ruleSearch is String) {
GSON.fromJsonObject<SearchRule>(sourceAny.ruleSearch.toString())
.getOrNull()
} else {
GSON.fromJsonObject<SearchRule>(GSON.toJson(sourceAny.ruleSearch))
.getOrNull()
}
source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) {
GSON.fromJsonObject<BookInfoRule>(sourceAny.ruleBookInfo.toString())
.getOrNull()
} else {
GSON.fromJsonObject<BookInfoRule>(GSON.toJson(sourceAny.ruleBookInfo))
.getOrNull()
}
source.ruleToc = if (sourceAny.ruleToc is String) {
GSON.fromJsonObject<TocRule>(sourceAny.ruleToc.toString())
.getOrNull()
} else {
GSON.fromJsonObject<TocRule>(GSON.toJson(sourceAny.ruleToc))
.getOrNull()
}
source.ruleContent = if (sourceAny.ruleContent is String) {
GSON.fromJsonObject<ContentRule>(sourceAny.ruleContent.toString())
.getOrNull()
} else {
GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent))
.getOrNull()
}
source.ruleReview = if (sourceAny.ruleReview is String) {
GSON.fromJsonObject<ReviewRule>(sourceAny.ruleReview.toString())
.getOrNull()
} else {
GSON.fromJsonObject<ReviewRule>(GSON.toJson(sourceAny.ruleReview))
.getOrNull()
}
}
source
}
}
@Keep
data class BookSourceAny(
var bookSourceName: String = "", // 名称
var bookSourceGroup: String? = null, // 分组
var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = BookSourceType.default, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null, // 详情页url正则
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现
var enabledReview: Boolean = false, // 启用段评
var enabledCookieJar: Boolean = false, // 启用CookieJar
var concurrentRate: String? = null, // 并发率
var header: String? = null, // 请求头
var loginUrl: Any? = null, // 登录规则
var loginUi: Any? = null, // 登录UI
var loginCheckJs: String? = null, // 登录检测js
var coverDecodeJs: String? = null, // 封面解密js
var bookSourceComment: String? = "", // 书源注释
var variableComment: String? = null, // 变量说明
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var respondTime: Long = 180000L, // 响应时间,用于排序
var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, // 发现url
var ruleExplore: Any? = null, // 发现规则
var searchUrl: String? = null, // 搜索url
var ruleSearch: Any? = null, // 搜索规则
var ruleBookInfo: Any? = null, // 书籍信息页规则
var ruleToc: Any? = null, // 目录页规则
var ruleContent: Any? = null, // 正文页规则
var ruleReview: Any? = null // 段评规则
)
// default规则适配
// #正则#替换内容 替换成 ##正则##替换内容
// | 替换成 ||
// & 替换成 &&
private fun toNewRule(oldRule: String?): String? {
if (oldRule.isNullOrBlank()) return null
var newRule = oldRule
var reverse = false
var allinone = false
if (oldRule.startsWith("-")) {
reverse = true
newRule = oldRule.substring(1)
}
if (newRule.startsWith("+")) {
allinone = true
newRule = newRule.substring(1)
}
if (!newRule.startsWith("@CSS:", true) &&
!newRule.startsWith("@XPath:", true) &&
!newRule.startsWith("//") &&
!newRule.startsWith("##") &&
!newRule.startsWith(":") &&
!newRule.contains("@js:", true) &&
!newRule.contains("<js>", true)
) {
if (newRule.contains("#") && !newRule.contains("##")) {
newRule = oldRule.replace("#", "##")
}
if (newRule.contains("|") && !newRule.contains("||")) {
if (newRule.contains("##")) {
val list = newRule.split("##")
if (list[0].contains("|")) {
newRule = list[0].replace("|", "||")
for (i in 1 until list.size) {
newRule += "##" + list[i]
}
}
} else {
newRule = newRule.replace("|", "||")
}
}
if (newRule.contains("&")
&& !newRule.contains("&&")
&& !newRule.contains("http")
&& !newRule.startsWith("/")
) {
newRule = newRule.replace("&", "&&")
}
}
if (allinone) {
newRule = "+$newRule"
}
if (reverse) {
newRule = "-$newRule"
}
return newRule
}
private fun toNewUrls(oldUrls: String?): String? {
if (oldUrls.isNullOrBlank()) return null
if (oldUrls.startsWith("@js:") || oldUrls.startsWith("<js>")) {
return oldUrls
}
if (!oldUrls.contains("\n") && !oldUrls.contains("&&")) {
return toNewUrl(oldUrls)
}
val urls = oldUrls.split("(&&|\r?\n)+".toRegex())
return urls.map {
toNewUrl(it)?.replace("\n\\s*".toRegex(), "")
}.joinToString("\n")
}
private fun toNewUrl(oldUrl: String?): String? {
if (oldUrl.isNullOrBlank()) return null
var url: String = oldUrl
if (oldUrl.startsWith("<js>", true)) {
url = url.replace("=searchKey", "={{key}}")
.replace("=searchPage", "={{page}}")
return url
}
val map = HashMap<String, String>()
var mather = headerPattern.matcher(url)
if (mather.find()) {
val header = mather.group()
url = url.replace(header, "")
map["headers"] = header.substring(8)
}
var urlList = url.split("|")
url = urlList[0]
if (urlList.size > 1) {
map["charset"] = urlList[1].split("=")[1]
}
mather = jsPattern.matcher(url)
val jsList = arrayListOf<String>()
while (mather.find()) {
jsList.add(mather.group())
url = url.replace(jsList.last(), "$${jsList.size - 1}")
}
url = url.replace("{", "<").replace("}", ">")
url = url.replace("searchKey", "{{key}}")
url = url.replace("<searchPage([-+]1)>".toRegex(), "{{page$1}}")
.replace("searchPage([-+]1)".toRegex(), "{{page$1}}")
.replace("searchPage", "{{page}}")
for ((index, item) in jsList.withIndex()) {
url = url.replace(
"$$index",
item.replace("searchKey", "key").replace("searchPage", "page")
)
}
urlList = url.split("@")
url = urlList[0]
if (urlList.size > 1) {
map["method"] = "POST"
map["body"] = urlList[1]
}
if (map.size > 0) {
url += "," + GSON.toJson(map)
}
return url
}
private fun uaToHeader(ua: String?): String? {
if (ua.isNullOrEmpty()) return null
val map = mapOf(Pair(AppConst.UA_NAME, ua))
return GSON.toJson(map)
}
}
@@ -3,16 +3,26 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookSourceType
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.*
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.ReplaceAnalyzer import io.legado.app.help.ReplaceAnalyzer
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File import java.io.File
import java.util.regex.Pattern
object ImportOldData { object ImportOldData {
private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE)
private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE)
fun importUri(context: Context, uri: Uri) { fun importUri(context: Context, uri: Uri) {
if (uri.isContentScheme()) { if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
@@ -92,10 +102,9 @@ object ImportOldData {
} }
fun importOldSource(json: String): Int { fun importOldSource(json: String): Int {
val count = BookSource.fromJsonArray(json).onSuccess { val sources = fromOldBookSources(json)
appDb.bookSourceDao.insert(*it.toTypedArray()) appDb.bookSourceDao.insert(*sources.toTypedArray())
}.getOrNull()?.size return sources.size
return count ?: 0
} }
private fun importOldReplaceRule(json: String): Int { private fun importOldReplaceRule(json: String): Int {
@@ -147,4 +156,211 @@ object ImportOldData {
} }
return books return books
} }
private fun fromOldBookSources(json: String): MutableList<BookSource> {
val sources = mutableListOf<BookSource>()
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
for (item in items) {
val jsonItem = jsonPath.parse(item)
val source = BookSource()
source.apply {
bookSourceUrl = jsonItem.readString("bookSourceUrl")
?: throw NoStackTraceException(appCtx.getString(R.string.wrong_format))
bookSourceName = jsonItem.readString("bookSourceName") ?: ""
bookSourceGroup = jsonItem.readString("bookSourceGroup")
loginUrl = jsonItem.readString("loginUrl")
loginUi = jsonItem.readString("loginUi")
loginCheckJs = jsonItem.readString("loginCheckJs")
coverDecodeJs = jsonItem.readString("coverDecodeJs")
bookSourceComment = jsonItem.readString("bookSourceComment") ?: ""
bookUrlPattern = jsonItem.readString("ruleBookUrlPattern")
customOrder = jsonItem.readInt("serialNumber") ?: 0
header = uaToHeader(jsonItem.readString("httpUserAgent"))
searchUrl = toNewUrl(jsonItem.readString("ruleSearchUrl"))
exploreUrl = toNewUrls(jsonItem.readString("ruleFindUrl"))
bookSourceType =
if (jsonItem.readString("bookSourceType") == "AUDIO") BookSourceType.audio else BookSourceType.default
enabled = jsonItem.readBool("enable") ?: true
if (exploreUrl.isNullOrBlank()) {
enabledExplore = false
}
ruleSearch = SearchRule(
bookList = toNewRule(jsonItem.readString("ruleSearchList")),
name = toNewRule(jsonItem.readString("ruleSearchName")),
author = toNewRule(jsonItem.readString("ruleSearchAuthor")),
intro = toNewRule(jsonItem.readString("ruleSearchIntroduce")),
kind = toNewRule(jsonItem.readString("ruleSearchKind")),
bookUrl = toNewRule(jsonItem.readString("ruleSearchNoteUrl")),
coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter"))
)
ruleExplore = ExploreRule(
bookList = toNewRule(jsonItem.readString("ruleFindList")),
name = toNewRule(jsonItem.readString("ruleFindName")),
author = toNewRule(jsonItem.readString("ruleFindAuthor")),
intro = toNewRule(jsonItem.readString("ruleFindIntroduce")),
kind = toNewRule(jsonItem.readString("ruleFindKind")),
bookUrl = toNewRule(jsonItem.readString("ruleFindNoteUrl")),
coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter"))
)
ruleBookInfo = BookInfoRule(
init = toNewRule(jsonItem.readString("ruleBookInfoInit")),
name = toNewRule(jsonItem.readString("ruleBookName")),
author = toNewRule(jsonItem.readString("ruleBookAuthor")),
intro = toNewRule(jsonItem.readString("ruleIntroduce")),
kind = toNewRule(jsonItem.readString("ruleBookKind")),
coverUrl = toNewRule(jsonItem.readString("ruleCoverUrl")),
lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")),
tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl"))
)
ruleToc = TocRule(
chapterList = toNewRule(jsonItem.readString("ruleChapterList")),
chapterName = toNewRule(jsonItem.readString("ruleChapterName")),
chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")),
nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext"))
)
var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: ""
if (content.startsWith("$") && !content.startsWith("$.")) {
content = content.substring(1)
}
ruleContent = ContentRule(
content = content,
replaceRegex = toNewRule(jsonItem.readString("ruleBookContentReplace")),
nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext"))
)
}
sources.add(source)
}
return sources
}
// default规则适配
// #正则#替换内容 替换成 ##正则##替换内容
// | 替换成 ||
// & 替换成 &&
private fun toNewRule(oldRule: String?): String? {
if (oldRule.isNullOrBlank()) return null
var newRule = oldRule
var reverse = false
var allinone = false
if (oldRule.startsWith("-")) {
reverse = true
newRule = oldRule.substring(1)
}
if (newRule.startsWith("+")) {
allinone = true
newRule = newRule.substring(1)
}
if (!newRule.startsWith("@CSS:", true) &&
!newRule.startsWith("@XPath:", true) &&
!newRule.startsWith("//") &&
!newRule.startsWith("##") &&
!newRule.startsWith(":") &&
!newRule.contains("@js:", true) &&
!newRule.contains("<js>", true)
) {
if (newRule.contains("#") && !newRule.contains("##")) {
newRule = oldRule.replace("#", "##")
}
if (newRule.contains("|") && !newRule.contains("||")) {
if (newRule.contains("##")) {
val list = newRule.split("##")
if (list[0].contains("|")) {
newRule = list[0].replace("|", "||")
for (i in 1 until list.size) {
newRule += "##" + list[i]
}
}
} else {
newRule = newRule.replace("|", "||")
}
}
if (newRule.contains("&")
&& !newRule.contains("&&")
&& !newRule.contains("http")
&& !newRule.startsWith("/")
) {
newRule = newRule.replace("&", "&&")
}
}
if (allinone) {
newRule = "+$newRule"
}
if (reverse) {
newRule = "-$newRule"
}
return newRule
}
private fun toNewUrls(oldUrls: String?): String? {
if (oldUrls.isNullOrBlank()) return null
if (oldUrls.startsWith("@js:") || oldUrls.startsWith("<js>")) {
return oldUrls
}
if (!oldUrls.contains("\n") && !oldUrls.contains("&&")) {
return toNewUrl(oldUrls)
}
val urls = oldUrls.split("(&&|\r?\n)+".toRegex())
return urls.map {
toNewUrl(it)?.replace("\n\\s*".toRegex(), "")
}.joinToString("\n")
}
private fun toNewUrl(oldUrl: String?): String? {
if (oldUrl.isNullOrBlank()) return null
var url: String = oldUrl
if (oldUrl.startsWith("<js>", true)) {
url = url.replace("=searchKey", "={{key}}")
.replace("=searchPage", "={{page}}")
return url
}
val map = HashMap<String, String>()
var mather = headerPattern.matcher(url)
if (mather.find()) {
val header = mather.group()
url = url.replace(header, "")
map["headers"] = header.substring(8)
}
var urlList = url.split("|")
url = urlList[0]
if (urlList.size > 1) {
map["charset"] = urlList[1].split("=")[1]
}
mather = jsPattern.matcher(url)
val jsList = arrayListOf<String>()
while (mather.find()) {
jsList.add(mather.group())
url = url.replace(jsList.last(), "$${jsList.size - 1}")
}
url = url.replace("{", "<").replace("}", ">")
url = url.replace("searchKey", "{{key}}")
url = url.replace("<searchPage([-+]1)>".toRegex(), "{{page$1}}")
.replace("searchPage([-+]1)".toRegex(), "{{page$1}}")
.replace("searchPage", "{{page}}")
for ((index, item) in jsList.withIndex()) {
url = url.replace(
"$$index",
item.replace("searchKey", "key").replace("searchPage", "page")
)
}
urlList = url.split("@")
url = urlList[0]
if (urlList.size > 1) {
map["method"] = "POST"
map["body"] = urlList[1]
}
if (map.size > 0) {
url += "," + GSON.toJson(map)
}
return url
}
private fun uaToHeader(ua: String?): String? {
if (ua.isNullOrEmpty()) return null
val map = mapOf(Pair(AppConst.UA_NAME, ua))
return GSON.toJson(map)
}
} }