kunfei 3 лет назад
Родитель
Сommit
1990ff8dc8

+ 39 - 6
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -3,15 +3,19 @@ package io.legado.app.data.entities
 import android.os.Parcelable
 import android.text.TextUtils
 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.BookSourceType
 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.fromJsonArray
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.splitNotBlank
 import kotlinx.parcelize.Parcelize
 import java.io.InputStream
+import java.lang.reflect.Type
 
 @Suppress("unused")
 @Parcelize
@@ -222,17 +226,46 @@ data class BookSource(
 
     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> {
-            return SourceAnalyzer.jsonToBookSource(json)
+            return gson.fromJsonObject(json)
         }
 
-        fun fromJsonArray(json: String): Result<MutableList<BookSource>> {
-            return SourceAnalyzer.jsonToBookSources(json)
+        fun fromJsonArray(json: String): Result<List<BookSource>> {
+            return gson.fromJsonArray(json)
         }
 
-        fun fromJsonArray(inputStream: InputStream): Result<MutableList<BookSource>> {
-            return SourceAnalyzer.jsonToBookSources(inputStream)
+        fun fromJsonArray(inputStream: InputStream): Result<List<BookSource>> {
+            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 {

+ 14 - 13
app/src/main/java/io/legado/app/data/entities/RssSource.kt

@@ -176,7 +176,7 @@ data class RssSource(
     companion object {
         private val gson by lazy {
             GSON.newBuilder()
-                .registerTypeAdapter(String::class.java, RssJsonDeserializer())
+                .registerTypeAdapter(String::class.java, StringJsonDeserializer())
                 .create()
         }
 
@@ -187,20 +187,21 @@ data class RssSource(
         fun fromJsonArray(jsonArray: String): Result<List<RssSource>> {
             return gson.fromJsonArray(jsonArray)
         }
-    }
 
-    class RssJsonDeserializer : 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 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()
+                }
             }
+
         }
 
     }

+ 17 - 1
app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 /**
@@ -20,4 +22,18 @@ data class BookInfoRule(
     var wordCount: String? = null,
     var canReName: 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
+            }
+        }
+
+    }
+
+}

+ 19 - 1
app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 /**
@@ -16,4 +18,20 @@ data class ContentRule(
     var imageStyle: String? = null,   //默认大小居中,FULL最大宽度
     var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
     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
+            }
+        }
+
+    }
+
+
+}

+ 17 - 1
app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 /**
@@ -18,4 +20,18 @@ data class ExploreRule(
     override var bookUrl: String? = null,
     override var coverUrl: 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
+            }
+        }
+
+    }
+
+}

+ 17 - 1
app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 @Parcelize
@@ -17,4 +19,18 @@ data class ReviewRule(
     var postReviewUrl: String? = null,      // 发送回复URL
     var postQuoteUrl: 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
+            }
+        }
+
+    }
+
+}

+ 17 - 1
app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 /**
@@ -20,4 +22,18 @@ data class SearchRule(
     override var bookUrl: String? = null,
     override var coverUrl: 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
+            }
+        }
+
+    }
+
+}

+ 17 - 1
app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt

@@ -1,6 +1,8 @@
 package io.legado.app.data.entities.rule
 
 import android.os.Parcelable
+import com.google.gson.JsonDeserializer
+import io.legado.app.utils.GSON
 import kotlinx.parcelize.Parcelize
 
 @Parcelize
@@ -14,4 +16,18 @@ data class TocRule(
     var isPay: String? = null,
     var updateTime: 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
+            }
+        }
+
+    }
+
+}

+ 0 - 389
app/src/main/java/io/legado/app/help/source/SourceAnalyzer.kt

@@ -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)
-    }
-
-}

+ 220 - 4
app/src/main/java/io/legado/app/help/storage/ImportOldData.kt

@@ -3,16 +3,26 @@ package io.legado.app.help.storage
 import android.content.Context
 import android.net.Uri
 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.data.appDb
 import io.legado.app.data.entities.Book
 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.utils.*
+import splitties.init.appCtx
 import java.io.File
+import java.util.regex.Pattern
 
 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) {
         if (uri.isContentScheme()) {
             DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
@@ -92,10 +102,9 @@ object ImportOldData {
     }
 
     fun importOldSource(json: String): Int {
-        val count = BookSource.fromJsonArray(json).onSuccess {
-            appDb.bookSourceDao.insert(*it.toTypedArray())
-        }.getOrNull()?.size
-        return count ?: 0
+        val sources = fromOldBookSources(json)
+        appDb.bookSourceDao.insert(*sources.toTypedArray())
+        return sources.size
     }
 
     private fun importOldReplaceRule(json: String): Int {
@@ -147,4 +156,211 @@ object ImportOldData {
         }
         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)
+    }
+
 }