Просмотр исходного кода

Merge pull request #2314 from Xwite/master

feat: decode decrypted cover
kunfei 4 лет назад
Родитель
Сommit
a155b333ce

+ 5 - 2
app/src/main/assets/help/ruleHelp.md

@@ -155,5 +155,8 @@ let options = {
 > 可直接填写链接或者JavaScript,如果执行结果是字符串链接将会自动打开浏览器
 
 * 图片解密
-> 适用于图片需要二次解密的情况,直接填写JavaScript,返回解密后的bytes
-> 部分变量说明:java(仅支持[js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)),result为待解密图片的bytes,src为图片链接
+> 适用于图片需要二次解密的情况,直接填写JavaScript,返回解密后的`ByteArray`  
+> 部分变量说明:java(仅支持[js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)),result为待解密图片的`ByteArray`,src为图片链接
+
+* 封面解密
+> 同图片解密 其中result为待解密封面的`inputStream`

+ 3 - 2
app/src/main/java/io/legado/app/data/AppDatabase.kt

@@ -20,7 +20,7 @@ val appDb by lazy {
 }
 
 @Database(
-    version = 53,
+    version = 54,
     exportSchema = true,
     entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
         ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -37,7 +37,8 @@ val appDb by lazy {
         AutoMigration(from = 49, to = 50),
         AutoMigration(from = 50, to = 51),
         AutoMigration(from = 51, to = 52),
-        AutoMigration(from = 52, to = 53)
+        AutoMigration(from = 52, to = 53),
+        AutoMigration(from = 53, to = 54),
     ]
 )
 abstract class AppDatabase : RoomDatabase() {

+ 3 - 0
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -54,6 +54,8 @@ data class BookSource(
     override var loginUi: String? = null,
     // 登录检测js
     var loginCheckJs: String? = null,
+    // 封面解密js
+    var coverDecodeJs: String? = null,
     // 注释
     var bookSourceComment: String? = null,
     // 自定义变量说明
@@ -206,6 +208,7 @@ data class BookSource(
                 && equal(loginUrl, source.loginUrl)
                 && equal(loginUi, source.loginUi)
                 && equal(loginCheckJs, source.loginCheckJs)
+                && equal(coverDecodeJs, source.coverDecodeJs)
                 && equal(exploreUrl, source.exploreUrl)
                 && equal(searchUrl, source.searchUrl)
                 && getSearchRule() == source.getSearchRule()

+ 9 - 1
app/src/main/java/io/legado/app/data/entities/RssSource.kt

@@ -40,6 +40,8 @@ data class RssSource(
     override var loginUi: String? = null,
     //登录检测js
     var loginCheckJs: String? = null,
+    //封面解密js
+    var coverDecodeJs: String? = null,
     var sortUrl: String? = null,
     var singleUrl: Boolean = false,
     /*列表规则*/
@@ -58,6 +60,9 @@ data class RssSource(
     var enableJs: Boolean = true,
     var loadWithBaseUrl: Boolean = true,
     /*其它规则*/
+    // 最后更新时间,用于排序
+    @ColumnInfo(defaultValue = "0")
+    var lastUpdateTime: Long = 0,
     var customOrder: Int = 0
 ) : Parcelable, BaseSource {
 
@@ -90,6 +95,7 @@ data class RssSource(
                 && equal(loginUrl, source.loginUrl)
                 && equal(loginUi, source.loginUi)
                 && equal(loginCheckJs, source.loginCheckJs)
+                && equal(coverDecodeJs, source.coverDecodeJs)
                 && equal(sortUrl, source.sortUrl)
                 && singleUrl == source.singleUrl
                 && articleStyle == source.articleStyle
@@ -174,7 +180,9 @@ data class RssSource(
                     enableJs = doc.readBool("$.enableJs") ?: true,
                     loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
                     enabledCookieJar = doc.readBool("$.enabledCookieJar") ?: false,
-                    customOrder = doc.readInt("$.customOrder") ?: 0
+                    customOrder = doc.readInt("$.customOrder") ?: 0,
+                    lastUpdateTime = doc.readLong("$.lastUpdateTime") ?: 0L,
+                    coverDecodeJs = doc.readString("$.coverDecodeJs")
                 )
             }
         }

+ 11 - 23
app/src/main/java/io/legado/app/help/BookHelp.kt

@@ -136,31 +136,19 @@ object BookHelp {
         downloadImages.add(src)
         val analyzeUrl = AnalyzeUrl(src, source = bookSource)
         try {
-            var bytes = analyzeUrl.getByteArrayAwait()
+            val bytes = analyzeUrl.getByteArrayAwait()
             //某些图片被加密,需要进一步解密
-            bookSource?.getContentRule()?.imageDecode?.let { imageDecode ->
-                if (imageDecode.isBlank()) {
-                    return@let
-                }
-                kotlin.runCatching {
-                    bookSource.evalJS(imageDecode) {
-                        put("book", book)
-                        put("result", bytes)
-                        put("src", src)
-                    } as ByteArray
-                }.onSuccess {
-                    bytes = it
-                }.onFailure {
-                    AppLog.putDebug("${src}解密bytes错误", it)
-                }
+            ImageUtils.decode(
+              src, bytes, isCover = false, bookSource, book
+            )?.let {
+                FileUtils.createFileIfNotExist(
+                    downloadDir,
+                    cacheFolderName,
+                    book.getFolderName(),
+                    cacheImageFolderName,
+                    "${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}"
+                ).writeBytes(it)
             }
-            FileUtils.createFileIfNotExist(
-                downloadDir,
-                cacheFolderName,
-                book.getFolderName(),
-                cacheImageFolderName,
-                "${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}"
-            ).writeBytes(bytes)
         } catch (e: Exception) {
             AppLog.putDebug("${src}下载错误", e)
         } finally {

+ 16 - 4
app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt

@@ -9,10 +9,12 @@ import com.bumptech.glide.load.model.GlideUrl
 import com.bumptech.glide.util.ContentLengthInputStream
 import com.bumptech.glide.util.Preconditions
 import io.legado.app.data.appDb
+import io.legado.app.data.entities.BaseSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.http.addHeaders
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.utils.isWifiConnect
+import io.legado.app.utils.ImageUtils
 import okhttp3.Call
 import okhttp3.Request
 import okhttp3.Response
@@ -20,6 +22,7 @@ import okhttp3.ResponseBody
 import splitties.init.appCtx
 import java.io.IOException
 import java.io.InputStream
+import java.io.ByteArrayInputStream
 
 
 class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Options) :
@@ -27,6 +30,7 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
     private var stream: InputStream? = null
     private var responseBody: ResponseBody? = null
     private var callback: DataFetcher.DataCallback<in InputStream>? = null
+    private var source: BaseSource? = null
 
     @Volatile
     private var call: Call? = null
@@ -40,7 +44,7 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
         val requestBuilder: Request.Builder = Request.Builder().url(url.toStringUrl())
         val headerMap = HashMap<String, String>()
         options.get(OkHttpModelLoader.sourceOriginOption)?.let { sourceUrl ->
-            val source = appDb.bookSourceDao.getBookSource(sourceUrl)
+            source = appDb.bookSourceDao.getBookSource(sourceUrl)
                 ?: appDb.rssSourceDao.getByKey(sourceUrl)
             source?.getHeaderMap(true)?.let {
                 headerMap.putAll(it)
@@ -81,9 +85,17 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
     override fun onResponse(call: Call, response: Response) {
         responseBody = response.body
         if (response.isSuccessful) {
-            val contentLength: Long = Preconditions.checkNotNull(responseBody).contentLength()
-            stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength)
-            callback?.onDataReady(stream)
+            val decodeResult = ImageUtils.decode(
+                url.toStringUrl(), responseBody!!.byteStream(),
+                isCover = true, source
+            )
+            if (decodeResult == null) {
+                callback?.onLoadFailed(NoStackTraceException("封面二次解密失败"))
+            } else {
+                val contentLength: Long = if (decodeResult is ByteArrayInputStream) decodeResult.available().toLong() else Preconditions.checkNotNull(responseBody).contentLength()
+                stream = ContentLengthInputStream.obtain(decodeResult, contentLength)
+                callback?.onDataReady(stream)
+            }
         } else {
             callback?.onLoadFailed(HttpException(response.message, response.code))
         }

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

@@ -83,6 +83,7 @@ object SourceAnalyzer {
                     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
@@ -165,6 +166,7 @@ object SourceAnalyzer {
                     sourceAny.loginUi?.toString()
                 }
                 source.loginCheckJs = sourceAny.loginCheckJs
+                source.coverDecodeJs = sourceAny.coverDecodeJs
                 source.bookSourceComment = sourceAny.bookSourceComment
                 source.variableComment = sourceAny.variableComment
                 source.lastUpdateTime = sourceAny.lastUpdateTime
@@ -236,6 +238,7 @@ object SourceAnalyzer {
         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,                   // 最后更新时间,用于排序

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -221,6 +221,7 @@ class BookSourceEditActivity :
             add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url))
             add(EditEntity("loginUi", source?.loginUi, R.string.login_ui))
             add(EditEntity("loginCheckJs", source?.loginCheckJs, R.string.login_check_js))
+            add(EditEntity("coverDecodeJs", source?.coverDecodeJs, R.string.cover_decode_js))
             add(EditEntity("bookUrlPattern", source?.bookUrlPattern, R.string.book_url_pattern))
             add(EditEntity("header", source?.header, R.string.source_http_header))
             add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment))
@@ -345,6 +346,7 @@ class BookSourceEditActivity :
                 "loginUrl" -> source.loginUrl = it.value
                 "loginUi" -> source.loginUi = it.value
                 "loginCheckJs" -> source.loginCheckJs = it.value
+                "coverDecodeJs" -> source.coverDecodeJs = it.value
                 "bookUrlPattern" -> source.bookUrlPattern = it.value
                 "header" -> source.header = it.value
                 "bookSourceComment" -> source.bookSourceComment = it.value

+ 2 - 0
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt

@@ -169,6 +169,7 @@ class RssSourceEditActivity :
             add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url))
             add(EditEntity("loginUi", source?.loginUi, R.string.login_ui))
             add(EditEntity("loginCheckJs", source?.loginCheckJs, R.string.login_check_js))
+            add(EditEntity("coverDecodeJs", source?.coverDecodeJs, R.string.cover_decode_js))
             add(EditEntity("header", source?.header, R.string.source_http_header))
             add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment))
             add(EditEntity("concurrentRate", source?.concurrentRate, R.string.concurrent_rate))
@@ -203,6 +204,7 @@ class RssSourceEditActivity :
                 "loginUrl" -> source.loginUrl = it.value
                 "loginUi" -> source.loginUi = it.value
                 "loginCheckJs" -> source.loginCheckJs = it.value
+                "coverDecodeJs" -> source.coverDecodeJs = it.value
                 "header" -> source.header = it.value
                 "variableComment" -> source.variableComment = it.value
                 "concurrentRate" -> source.concurrentRate = it.value

+ 69 - 0
app/src/main/java/io/legado/app/utils/ImageUtils.kt

@@ -0,0 +1,69 @@
+package io.legado.app.utils
+
+import io.legado.app.constant.AppLog
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BaseSource
+import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.RssSource
+import java.io.InputStream
+import java.io.ByteArrayInputStream
+
+/**
+ * 加密图片解密工具
+ */
+object ImageUtils {
+
+    /**
+     * @param isCover 根据这个执行书源中不同的解密规则
+     * @return 解密失败返回Null 解密规则为空不处理
+     */
+    fun decode(
+        src: String, bytes: ByteArray, isCover: Boolean,
+        source: BaseSource?, book: Book? = null
+    ): ByteArray? {
+        val ruleJs = getRuleJs(source, isCover)
+        if (ruleJs.isNullOrBlank()) return bytes
+        //解密库hutool.crypto ByteArray|InputStream -> ByteArray
+        return kotlin.runCatching {
+            source?.evalJS(ruleJs) {
+                put("book", book)
+                put("result", bytes)
+                put("src", src)
+            } as ByteArray
+        }.onFailure {
+            AppLog.putDebug("${src}解密错误", it)
+        }.getOrNull()
+    }
+
+    fun decode(
+        src: String, inputStream: InputStream, isCover: Boolean,
+        source: BaseSource?, book: Book? = null
+    ): InputStream? {
+        val ruleJs = getRuleJs(source, isCover)
+        if (ruleJs.isNullOrBlank()) return inputStream
+        //解密库hutool.crypto ByteArray|InputStream -> ByteArray
+        return kotlin.runCatching {
+            val bytes = source?.evalJS(ruleJs) {
+                put("book", book)
+                put("result", inputStream)
+                put("src", src)
+            } as ByteArray
+            ByteArrayInputStream(bytes)
+        }.onFailure {
+            AppLog.putDebug("${src}解密错误", it)
+        }.getOrNull()
+    }
+
+
+   private fun getRuleJs(
+       source: BaseSource?, isCover: Boolean
+   ): String? {
+      return when (source) {
+          is BookSource ->
+             if (isCover) source?.coverDecodeJs else source?.getContentRule()?.imageDecode
+          is RssSource -> source?.coverDecodeJs
+          else -> null
+      }
+   }
+
+}

+ 2 - 1
app/src/main/res/values-es-rES/strings.xml

@@ -872,7 +872,7 @@
     <string name="anti_alias">Anti-Aliasing</string>
     <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
     <string name="upload_url">上传URL</string>
-    <string name="download_url_rule">下载URL规则</string>
+    <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
     <string name="sort_by_respondTime">Ordenar por tiempo de respuesta</string>
     <string name="export_success">导出成功</string>
     <string name="path">路径</string>
@@ -1032,4 +1032,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values-ja-rJP/strings.xml

@@ -875,7 +875,7 @@
     <string name="anti_alias">Anti-Aliasing</string>
     <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
     <string name="upload_url">上传URL</string>
-    <string name="download_url_rule">下载URL规则</string>
+    <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
     <string name="sort_by_respondTime">Sort by respond time</string>
     <string name="export_success">导出成功</string>
     <string name="path">路径</string>
@@ -1035,4 +1035,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values-pt-rBR/strings.xml

@@ -873,7 +873,7 @@
     <string name="anti_alias">Anti-Aliasing</string>
     <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
     <string name="upload_url">上传URL</string>
-    <string name="download_url_rule">下载URL规则</string>
+    <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
     <string name="sort_by_respondTime">Classificar por tempo de resposta</string>
     <string name="export_success">导出成功</string>
     <string name="path">路径</string>
@@ -1035,4 +1035,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values-zh-rHK/strings.xml

@@ -872,7 +872,7 @@
     <string name="anti_alias">抗鋸齒</string>
     <string name="pref_anti_alias_summary">繪製圖片時抗鋸齒</string>
     <string name="upload_url">上傳URL</string>
-    <string name="download_url_rule">下載URL規則</string>
+    <string name="download_url_rule">下载URL规则(downloadUrls)</string>
     <string name="sort_by_respondTime">響應時間排序</string>
     <string name="export_success">導出成功</string>
     <string name="path">路徑</string>
@@ -1032,4 +1032,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values-zh-rTW/strings.xml

@@ -874,7 +874,7 @@
     <string name="anti_alias">抗鋸齒</string>
     <string name="pref_anti_alias_summary">繪製圖片時抗鋸齒</string>
     <string name="upload_url">上傳URL</string>
-    <string name="download_url_rule">下載URL規則</string>
+    <string name="download_url_rule">下载URL规则(downloadUrls)</string>
     <string name="sort_by_respondTime">反應時間排序</string>
     <string name="export_success">匯出成功</string>
     <string name="path">路徑</string>
@@ -1034,4 +1034,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values-zh/strings.xml

@@ -876,7 +876,7 @@
     <string name="anti_alias">抗锯齿</string>
     <string name="pref_anti_alias_summary">绘制图片时抗锯齿</string>
     <string name="upload_url">上传 URL</string>
-    <string name="download_url_rule">下载 URL 规则</string>
+    <string name="download_url_rule">下载URL规则(downloadUrls)</string>
     <string name="sort_by_respondTime">响应时间排序</string>
     <string name="export_success">导出成功</string>
     <string name="path">路径</string>
@@ -1034,4 +1034,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
 </resources>

+ 2 - 1
app/src/main/res/values/strings.xml

@@ -877,7 +877,7 @@
     <string name="anti_alias">Anti-Aliasing</string>
     <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
     <string name="upload_url">upload url</string>
-    <string name="download_url_rule">下载URL规则</string>
+    <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
     <string name="export_success">export success</string>
     <string name="path">path</string>
     <string name="direct_link_upload_rule">直链上传规则</string>
@@ -1035,4 +1035,5 @@
     <string name="ignore_audio_focus_title">忽略音频焦点</string>
     <string name="ignore_audio_focus_summary">允许与其他应用同时播放音频</string>
     <string name="refresh_sort">刷新分类</string>
+    <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
 </resources>