Merge branch 'master' into master

pull/1296/head
kunfei 4 years ago committed by GitHub
commit 08a6f08559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/src/main/assets/updateLog.md
  2. 18
      app/src/main/java/io/legado/app/help/BookSourceAnalyzer.kt
  3. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  4. 108
      app/src/main/java/io/legado/app/model/CacheBook.kt
  5. 2
      app/src/main/java/io/legado/app/model/ReadBook.kt
  6. 12
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  7. 45
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  8. 3
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  9. 6
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt
  10. 5
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt
  11. 5
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  12. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  13. 1
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  14. 1
      app/src/main/res/layout/activity_source_debug.xml

@ -15,12 +15,13 @@
**2021/09/05** **2021/09/05**
1. 采用exoPlayer播放音频,支持更多格式 1. 采用exoPlayer播放音频,支持更多格式
2. js规则内的@get,@put,{{}},不再在拆分时替换,都有对应的java函数,可以留着解析url时实时运算 2. 替换不再阻塞
3. 替换不再阻塞 3. 修复详情页初始化:规则bug
4. 修复详情页初始化:规则bug 4. 书源内的并发率生效,两种格式
5. 书源内的并发率生效,两种格式
* 时间 格式: 如 500, 访问间隔500毫秒 * 时间 格式: 如 500, 访问间隔500毫秒
* 次数/时间 格式: 如 5/60000, 每分钟最多访问5次 * 次数/时间 格式: 如 5/60000, 每分钟最多访问5次
5. 其它bug修复
**2021/09/01** **2021/09/01**

@ -99,6 +99,7 @@ object BookSourceAnalyzer {
source.customOrder = sourceAny.customOrder source.customOrder = sourceAny.customOrder
source.enabled = sourceAny.enabled source.enabled = sourceAny.enabled
source.enabledExplore = sourceAny.enabledExplore source.enabledExplore = sourceAny.enabledExplore
source.concurrentRate = sourceAny.concurrentRate
source.header = sourceAny.header source.header = sourceAny.header
source.loginUrl = when (sourceAny.loginUrl) { source.loginUrl = when (sourceAny.loginUrl) {
null -> null null -> null
@ -155,21 +156,22 @@ object BookSourceAnalyzer {
var customOrder: Int = 0, // 手动排序编号 var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用 var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现 var enabledExplore: Boolean = true, // 启用发现
var concurrentRate: String? = null, // 并发率
var header: String? = null, // 请求头 var header: String? = null, // 请求头
var loginUrl: Any? = null, // 登录规则 var loginUrl: Any? = null, // 登录规则
var loginUi: List<RowUi>? = null, //登录UI var loginUi: List<RowUi>? = null, // 登录UI
var loginCheckJs: String? = null, //登录检测js var loginCheckJs: String? = null, //登录检测js
var bookSourceComment: String? = "", //书源注释 var bookSourceComment: String? = "", //书源注释
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var respondTime: Long = 180000L, // 响应时间,用于排序 var respondTime: Long = 180000L, // 响应时间,用于排序
var weight: Int = 0, // 智能排序的权重 var weight: Int = 0, // 智能排序的权重
var exploreUrl: String? = null, // 发现url var exploreUrl: String? = null, // 发现url
var ruleExplore: Any? = null, // 发现规则 var ruleExplore: Any? = null, // 发现规则
var searchUrl: String? = null, // 搜索url var searchUrl: String? = null, // 搜索url
var ruleSearch: Any? = null, // 搜索规则 var ruleSearch: Any? = null, // 搜索规则
var ruleBookInfo: Any? = null, // 书籍信息页规则 var ruleBookInfo: Any? = null, // 书籍信息页规则
var ruleToc: Any? = null, // 目录页规则 var ruleToc: Any? = null, // 目录页规则
var ruleContent: Any? = null // 正文页规则 var ruleContent: Any? = null // 正文页规则
) )
// default规则适配 // default规则适配

@ -82,7 +82,7 @@ interface JsExtensions {
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
}.getOrElse { }.getOrElse {
StrResponse(analyzeUrl.url, it.localizedMessage) StrResponse(analyzeUrl.getDirectUrl(), it.localizedMessage)
} }
} }
} }
@ -96,7 +96,7 @@ interface JsExtensions {
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
}.getOrElse { }.getOrElse {
StrResponse(analyzeUrl.url, it.localizedMessage) StrResponse(analyzeUrl.getDirectUrl(), it.localizedMessage)
} }
} }
} }

@ -3,6 +3,7 @@ package io.legado.app.model
import android.content.Context import android.content.Context
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -12,67 +13,91 @@ import io.legado.app.utils.msg
import io.legado.app.utils.startService import io.legado.app.utils.startService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import splitties.init.appCtx import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
object CacheBook { class CacheBook(val bookSource: BookSource, val book: Book) {
val logs = arrayListOf<String>()
private val downloadMap = ConcurrentHashMap<String, CopyOnWriteArraySet<Int>>()
fun addLog(log: String?) { companion object {
log ?: return
synchronized(this) { val logs = arrayListOf<String>()
if (logs.size > 1000) { private val cacheBookMap = hashMapOf<String, CacheBook>()
logs.removeAt(0)
fun get(bookUrl: String): CacheBook? {
var cacheBook = cacheBookMap[bookUrl]
if (cacheBook != null) {
return cacheBook
} }
logs.add(log) val book = appDb.bookDao.getBook(bookUrl) ?: return null
val bookSource = appDb.bookSourceDao.getBookSource(book.origin) ?: return null
cacheBook = CacheBook(bookSource, book)
cacheBookMap[bookUrl] = cacheBook
return cacheBook
} }
}
fun start(context: Context, bookUrl: String, start: Int, end: Int) { fun get(bookSource: BookSource, book: Book): CacheBook {
context.startService<CacheBookService> { var cacheBook = cacheBookMap[book.bookUrl]
action = IntentAction.start if (cacheBook != null) {
putExtra("bookUrl", bookUrl) return cacheBook
putExtra("start", start) }
putExtra("end", end) cacheBook = CacheBook(bookSource, book)
cacheBookMap[book.bookUrl] = cacheBook
return cacheBook
} }
}
fun remove(context: Context, bookUrl: String) { fun addLog(log: String?) {
context.startService<CacheBookService> { log ?: return
action = IntentAction.remove synchronized(this) {
putExtra("bookUrl", bookUrl) if (logs.size > 1000) {
logs.removeAt(0)
}
logs.add(log)
}
} }
}
fun stop(context: Context) { fun start(context: Context, bookUrl: String, start: Int, end: Int) {
context.startService<CacheBookService> { context.startService<CacheBookService> {
action = IntentAction.stop action = IntentAction.start
putExtra("bookUrl", bookUrl)
putExtra("start", start)
putExtra("end", end)
}
} }
}
fun downloadCount(): Int { fun remove(context: Context, bookUrl: String) {
var count = 0 context.startService<CacheBookService> {
downloadMap.forEach { action = IntentAction.remove
count += it.value.size putExtra("bookUrl", bookUrl)
}
} }
return count
fun stop(context: Context) {
context.startService<CacheBookService> {
action = IntentAction.stop
}
}
val downloadCount: Int
get() {
var count = 0
cacheBookMap.forEach {
count += it.value.downloadSet.size
}
return count
}
} }
val downloadSet = CopyOnWriteArraySet<Int>()
fun download( fun download(
scope: CoroutineScope, scope: CoroutineScope,
bookSource: BookSource,
book: Book,
chapter: BookChapter, chapter: BookChapter,
resetPageOffset: Boolean = false resetPageOffset: Boolean = false
) { ) {
if (downloadMap[book.bookUrl]?.contains(chapter.index) == true) { if (downloadSet.contains(chapter.index)) {
return return
} }
if (downloadMap[book.bookUrl] == null) { downloadSet.add(chapter.index)
downloadMap[book.bookUrl] = CopyOnWriteArraySet()
}
downloadMap[book.bookUrl]?.add(chapter.index)
WebBook.getContent(scope, bookSource, book, chapter) WebBook.getContent(scope, bookSource, book, chapter)
.onSuccess { content -> .onSuccess { content ->
if (ReadBook.book?.bookUrl == book.bookUrl) { if (ReadBook.book?.bookUrl == book.bookUrl) {
@ -93,10 +118,7 @@ object CacheBook {
) )
} }
}.onFinally { }.onFinally {
downloadMap[book.bookUrl]?.remove(chapter.index) downloadSet.remove(chapter.index)
if (downloadMap[book.bookUrl].isNullOrEmpty()) {
downloadMap.remove(book.bookUrl)
}
ReadBook.removeLoading(chapter.index) ReadBook.removeLoading(chapter.index)
} }
} }

@ -295,7 +295,7 @@ object ReadBook : CoroutineScope by MainScope() {
val book = book val book = book
val bookSource = bookSource val bookSource = bookSource
if (book != null && bookSource != null) { if (book != null && bookSource != null) {
CacheBook.download(scope, bookSource, book, chapter) CacheBook.get(bookSource, book).download(scope, chapter)
} else if (book != null) { } else if (book != null) {
contentLoadFinish( contentLoadFinish(
book, chapter, "没有书源", resetPageOffset = resetPageOffset book, chapter, "没有书源", resetPageOffset = resetPageOffset

@ -453,18 +453,6 @@ class AnalyzeRule(
} }
else -> ruleStr else -> ruleStr
} }
if (mode != Mode.Js) {
//非js规则才需要继续处理
initNonJsRule()
}
}
/**
* 处理非js规则,js不做处理
* js里面的{{}}规则可以留到下次处理
* 一些url需要实时更新参数可以再<js></js>内嵌套{{}},{{}}会访问时才处理
*/
private fun initNonJsRule() {
//分离put //分离put
rule = splitPutRule(rule, putMap) rule = splitPutRule(rule, putMap)
//@get,{{ }}, 拆分 //@get,{{ }}, 拆分

@ -25,6 +25,7 @@ import kotlin.collections.HashMap
* Created by GKF on 2018/1/24. * Created by GKF on 2018/1/24.
* 搜索URL规则解析 * 搜索URL规则解析
*/ */
@Suppress("unused")
@Keep @Keep
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
class AnalyzeUrl( class AnalyzeUrl(
@ -34,7 +35,6 @@ class AnalyzeUrl(
val speakText: String? = null, val speakText: String? = null,
val speakSpeed: Int? = null, val speakSpeed: Int? = null,
var baseUrl: String = "", var baseUrl: String = "",
var useWebView: Boolean = false,
val book: BaseBook? = null, val book: BaseBook? = null,
val chapter: BookChapter? = null, val chapter: BookChapter? = null,
private val ruleData: RuleDataInterface? = null, private val ruleData: RuleDataInterface? = null,
@ -47,17 +47,18 @@ class AnalyzeUrl(
private val accessTime = hashMapOf<String, FetchRecord>() private val accessTime = hashMapOf<String, FetchRecord>()
} }
var url: String = ""
val headerMap = HashMap<String, String>() val headerMap = HashMap<String, String>()
var body: String? = null var body: String? = null
var type: String? = null var type: String? = null
private lateinit var urlHasQuery: String private var url: String = ""
private var queryStr: String? = null private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>() private val fieldMap = LinkedHashMap<String, String>()
private var charset: String? = null private var charset: String? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
private var proxy: String? = null private var proxy: String? = null
private var retry: Int = 0 private var retry: Int = 0
private var useWebView: Boolean = false
private var webJs: String? = null
init { init {
val urlMatcher = paramPattern.matcher(baseUrl) val urlMatcher = paramPattern.matcher(baseUrl)
@ -140,12 +141,13 @@ class AnalyzeUrl(
*/ */
private fun initUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。 private fun initUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。
val urlMatcher = paramPattern.matcher(ruleUrl) val urlMatcher = paramPattern.matcher(ruleUrl)
urlHasQuery = if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl val urlNoOption =
url = NetworkUtils.getAbsoluteURL(baseUrl, urlHasQuery) if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl
url = NetworkUtils.getAbsoluteURL(baseUrl, urlNoOption)
NetworkUtils.getBaseUrl(url)?.let { NetworkUtils.getBaseUrl(url)?.let {
baseUrl = it baseUrl = it
} }
if (urlHasQuery.length != ruleUrl.length) { if (urlNoOption.length != ruleUrl.length) {
GSON.fromJsonObject<UrlOption>(ruleUrl.substring(urlMatcher.end()))?.let { option -> GSON.fromJsonObject<UrlOption>(ruleUrl.substring(urlMatcher.end()))?.let { option ->
option.method?.let { option.method?.let {
if (it.equals("POST", true)) method = RequestMethod.POST if (it.equals("POST", true)) method = RequestMethod.POST
@ -170,6 +172,7 @@ class AnalyzeUrl(
useWebView = true useWebView = true
} }
} }
webJs = option.webJs
option.js?.let { option.js?.let {
evalJS(it) evalJS(it)
} }
@ -182,21 +185,21 @@ class AnalyzeUrl(
} }
when (method) { when (method) {
RequestMethod.GET -> { RequestMethod.GET -> {
if (!useWebView) { val pos = url.indexOf('?')
val pos = url.indexOf('?') if (pos != -1) {
if (pos != -1) { analyzeFields(url.substring(pos + 1))
analyzeFields(url.substring(pos + 1)) url = url.substring(0, pos)
url = url.substring(0, pos) } else body?.let {
}
}
}
RequestMethod.POST -> {
body?.let {
if (!it.isJson()) { if (!it.isJson()) {
analyzeFields(it) analyzeFields(it)
} }
} }
} }
RequestMethod.POST -> body?.let {
if (!it.isJson()) {
analyzeFields(it)
}
}
} }
} }
@ -415,7 +418,14 @@ class AnalyzeUrl(
headerMap.forEach { (key, value) -> headerMap.forEach { (key, value) ->
headers.addHeader(key, value) headers.addHeader(key, value)
} }
return GlideUrl(urlHasQuery, headers.build()) return GlideUrl(getDirectUrl(), headers.build())
}
fun getDirectUrl(): String {
val qs = fieldMap.map {
"${it.key}=${it.value}"
}
return "$url?${qs.joinToString("&")}"
} }
fun getUserAgent(): String { fun getUserAgent(): String {
@ -430,6 +440,7 @@ class AnalyzeUrl(
val method: String?, val method: String?,
val charset: String?, val charset: String?,
val webView: Any?, val webView: Any?,
val webJs: String?,
val headers: Any?, val headers: Any?,
val body: Any?, val body: Any?,
val type: String?, val type: String?,

@ -129,8 +129,7 @@ class AudioPlayService : BaseService(),
AnalyzeUrl( AnalyzeUrl(
url, url,
headerMapF = AudioPlay.headers(), headerMapF = AudioPlay.headers(),
source = AudioPlay.bookSource, source = AudioPlay.bookSource
useWebView = true
) )
val uri = Uri.parse(analyzeUrl.url) val uri = Uri.parse(analyzeUrl.url)
val mediaSource = ExoPlayerHelper val mediaSource = ExoPlayerHelper

@ -25,7 +25,9 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
override val viewModel by viewModels<BookSourceDebugModel>() override val viewModel by viewModels<BookSourceDebugModel>()
private lateinit var adapter: BookSourceDebugAdapter private lateinit var adapter: BookSourceDebugAdapter
private lateinit var searchView: SearchView private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view)
}
private val qrCodeResult = registerForActivityResult(QrCodeResult()) { private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it?.let { it?.let {
startSearch(it) startSearch(it)
@ -33,7 +35,6 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
searchView = binding.titleBar.findViewById(R.id.search_view)
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
viewModel.init(intent.getStringExtra("key")) { viewModel.init(intent.getStringExtra("key")) {
@ -75,6 +76,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
openOrCloseHelp(hasFocus) openOrCloseHelp(hasFocus)
} }
openOrCloseHelp(true)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

@ -54,7 +54,10 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
fun pasteSource(onSuccess: (source: BookSource) -> Unit) { fun pasteSource(onSuccess: (source: BookSource) -> Unit) {
execute(context = Dispatchers.Main) { execute(context = Dispatchers.Main) {
context.getClipText()?.let { text -> val text = context.getClipText()
if (text.isNullOrBlank()) {
error("剪贴板为空")
} else {
importSource(text, onSuccess) importSource(text, onSuccess)
} }
}.onError { }.onError {

@ -118,8 +118,9 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
if (!BookHelp.hasContent(book, chapter)) { if (!BookHelp.hasContent(book, chapter)) {
var addToCache = false var addToCache = false
while (!addToCache) { while (!addToCache) {
if (CacheBook.downloadCount() < 10) { val cacheBook = CacheBook.get(bookSource, book)
CacheBook.download(this, bookSource, book, chapter) if (CacheBook.downloadCount < 10) {
cacheBook.download(this, chapter)
addToCache = true addToCache = true
} else { } else {
delay(100) delay(100)

@ -193,7 +193,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
} }
viewModel.urlLiveData.observe(this) { viewModel.urlLiveData.observe(this) {
upJavaScriptEnable() upJavaScriptEnable()
binding.webView.loadUrl(it.url, it.headerMap) binding.webView.loadUrl(it.getDirectUrl(), it.headerMap)
} }
} }

@ -84,7 +84,6 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
ruleUrl = url, ruleUrl = url,
baseUrl = baseUrl, baseUrl = baseUrl,
useWebView = true,
headerMapF = rssSource?.getHeaderMap() headerMapF = rssSource?.getHeaderMap()
) )
urlLiveData.postValue(analyzeUrl) urlLiveData.postValue(analyzeUrl)

@ -39,6 +39,7 @@
android:id="@+id/help" android:id="@+id/help"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/title_bar"> app:layout_constraintTop_toBottomOf="@+id/title_bar">
<LinearLayout <LinearLayout

Loading…
Cancel
Save