feat: decode decrypted cover
This commit is contained in:
@@ -155,5 +155,6 @@ 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为待解密图片的`inputStream`,src为图片链接
|
||||
|
||||
|
||||
@@ -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 {
|
||||
if (it.isBlank()) {
|
||||
return@let
|
||||
}
|
||||
kotlin.runCatching {
|
||||
bookSource.evalJS(it) {
|
||||
put("book", book)
|
||||
put("result", bytes)
|
||||
put("src", src)
|
||||
} as ByteArray
|
||||
}.onSuccess {
|
||||
bytes = it
|
||||
}.onFailure {
|
||||
AppLog.putDebug("${src}解密bytes错误", it)
|
||||
}
|
||||
}
|
||||
FileUtils.createFileIfNotExist(
|
||||
ImageUtils.decode(
|
||||
src, bytes, isCover = false, bookSource, book
|
||||
)?.let {
|
||||
FileUtils.createFileIfNotExist(
|
||||
downloadDir,
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
cacheImageFolderName,
|
||||
"${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}"
|
||||
).writeBytes(bytes)
|
||||
).writeBytes(it)
|
||||
}
|
||||
} 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>
|
||||
|
||||
Reference in New Issue
Block a user