Merge pull request #2314 from Xwite/master

feat: decode decrypted cover
pull/2319/head
kunfei 2 years ago committed by GitHub
commit 2edeb25731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/src/main/assets/help/ruleHelp.md
  2. 5
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  3. 3
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  4. 10
      app/src/main/java/io/legado/app/data/entities/RssSource.kt
  5. 34
      app/src/main/java/io/legado/app/help/BookHelp.kt
  6. 20
      app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt
  7. 3
      app/src/main/java/io/legado/app/help/source/SourceAnalyzer.kt
  8. 2
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  9. 2
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  10. 69
      app/src/main/java/io/legado/app/utils/ImageUtils.kt
  11. 3
      app/src/main/res/values-es-rES/strings.xml
  12. 3
      app/src/main/res/values-ja-rJP/strings.xml
  13. 3
      app/src/main/res/values-pt-rBR/strings.xml
  14. 3
      app/src/main/res/values-zh-rHK/strings.xml
  15. 3
      app/src/main/res/values-zh-rTW/strings.xml
  16. 3
      app/src/main/res/values-zh/strings.xml
  17. 3
      app/src/main/res/values/strings.xml

@ -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`

@ -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() {

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

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

@ -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 {

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

@ -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, // 最后更新时间,用于排序

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save