下载优化

This commit is contained in:
HapeLee
2026-05-15 03:04:07 +08:00
parent 22cb773ada
commit cfd21da161
5 changed files with 65 additions and 33 deletions
@@ -17,6 +17,7 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.LogUtils
import io.legado.app.utils.onEachParallel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
@@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
@@ -180,16 +182,14 @@ object CacheBook {
_downloadSummaryFlow.value = buildSummary(stats)
}
@Synchronized
fun getOrCreate(bookUrl: String): CacheBookModel? {
val book = appDb.bookDao.getBook(bookUrl) ?: return null
val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return null
return getOrCreate(source, book)
suspend fun getOrCreate(bookUrl: String): CacheBookModel? = withContext(Dispatchers.IO) {
val book = appDb.bookDao.getBook(bookUrl) ?: return@withContext null
val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return@withContext null
getOrCreate(source, book)
}
@Synchronized
fun getOrCreate(bookSource: BookSource, book: Book): CacheBookModel {
updateBookSource(bookSource)
cacheBookMap[book.bookUrl]?.let { model ->
model.bookSource = bookSource
model.book = book
@@ -202,14 +202,16 @@ object CacheBook {
}
private fun updateBookSource(newBookSource: BookSource) {
cacheBookMap.forEach { (_, model) ->
if (model.bookSource.bookSourceUrl == newBookSource.bookSourceUrl) {
// 只有在必要时才更新,且避免在 getOrCreate 中高频调用
val sourceUrl = newBookSource.bookSourceUrl
cacheBookMap.values.forEach { model ->
if (model.bookSource.bookSourceUrl == sourceUrl && model.bookSource != newBookSource) {
model.bookSource = newBookSource
}
}
}
fun start(context: Context, book: Book, selectedIndices: List<Int>) {
suspend fun start(context: Context, book: Book, selectedIndices: List<Int>) {
start(
context = context,
request = CacheDownloadRequest(
@@ -220,7 +222,7 @@ object CacheBook {
)
}
fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
suspend fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
start(
context = context,
request = CacheDownloadRequest(
@@ -241,19 +243,28 @@ object CacheBook {
}
}
fun start(context: Context, requests: List<CacheDownloadRequest>) {
requests.asSequence()
.filter { it.hasValidSelection() }
.filter { request ->
appDb.bookDao.getBook(request.bookUrl)?.isLocal != true
}
.forEach { request ->
isPaused = false
startCacheBookService(context) {
action = IntentAction.start
putRequestExtras(request)
}
suspend fun start(context: Context, requests: List<CacheDownloadRequest>) = withContext(Dispatchers.IO) {
val validRequests = requests.filter { it.hasValidSelection() }
if (validRequests.isEmpty()) return@withContext
val urls = validRequests.map { it.bookUrl }.toSet()
val localBookUrls = appDb.bookDao.getCacheableBooks(urls)
.filter { it.isLocal }
.map { it.bookUrl }
.toSet()
val finalRequests = validRequests.filterNot { it.bookUrl in localBookUrls }
if (finalRequests.isEmpty()) return@withContext
isPaused = false
// 如果请求较多,可以通过 Intent 传递一个特殊的标志让 Service 自己去检查队列,
// 或者分批发送。这里我们先简单处理,但确保不在主线程做数据库查询。
finalRequests.forEach { request ->
startCacheBookService(context) {
action = IntentAction.start
putRequestExtras(request)
}
}
}
private fun android.content.Intent.putRequestExtras(request: CacheDownloadRequest) {
@@ -116,12 +116,21 @@ class CacheDownloadStateStore {
}
private fun CacheDownloadState.recalculate(): CacheDownloadState {
val totalWaiting = books.values.sumOf { it.waitingCount }
val totalRunning = books.values.sumOf { it.runningIndices.size }
val totalPaused = books.values.sumOf { it.pausedIndices.size }
val totalFailure = books.values.sumOf { it.failedIndices.size } +
books.values.count { it.failureMessage != null }
val totalSuccess = books.values.sumOf { it.successCount }
var totalWaiting = 0
var totalRunning = 0
var totalPaused = 0
var totalFailure = 0
var totalSuccess = 0
books.values.forEach { bookState ->
totalWaiting += bookState.waitingCount
totalRunning += bookState.runningIndices.size
totalPaused += bookState.pausedIndices.size
totalFailure += bookState.failedIndices.size
if (bookState.failureMessage != null) totalFailure++
totalSuccess += bookState.successCount
}
return copy(
isRunning = totalWaiting > totalPaused || totalRunning > 0,
totalWaiting = totalWaiting,
@@ -150,7 +150,7 @@ class CacheBookService : BaseService() {
serviceCommandMutex.withLock {
CacheBook.pauseAllFromService()
notificationContent = CacheBook.downloadSummary
upCacheBookNotification()
upCacheBookNotification(force = true)
}
}
}
@@ -160,7 +160,7 @@ class CacheBookService : BaseService() {
CacheBook.resumeFromService()
ensureDownloadJob()
notificationContent = CacheBook.downloadSummary
upCacheBookNotification()
upCacheBookNotification(force = true)
}
}
}
@@ -364,7 +364,8 @@ class CacheBookService : BaseService() {
}
notificationContent = CacheBook.downloadSummary
upCacheBookNotification()
// 移除这里的直接调用,依靠 onCreate 的循环更新
// upCacheBookNotification()
}
private fun AdmissionRequest.isCurrent(): Boolean {
@@ -521,7 +522,13 @@ class CacheBookService : BaseService() {
}
}
private fun upCacheBookNotification() {
private var lastNotificationTime = 0L
private fun upCacheBookNotification(force: Boolean = false) {
val now = System.currentTimeMillis()
if (!force && now - lastNotificationTime < 500L) return
lastNotificationTime = now
val total = CacheBook.totalCount
val progress = CacheBook.completedCount
val pendingBookCount = synchronized(admissionQueue) { admissionQueue.size }
@@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnAttach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.google.android.material.datepicker.MaterialDatePicker
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
@@ -48,6 +49,7 @@ import io.legado.app.utils.isTv
import io.legado.app.utils.setLightStatusBar
import io.legado.app.utils.setNavigationBarColorAuto
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
import kotlinx.coroutines.launch
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.themeColor
import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -304,7 +306,9 @@ abstract class BaseReadBookActivity :
val end = editEnd.text!!.toString().let {
if (it.isEmpty()) book.totalChapterNum else it.toInt()
}
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
lifecycleScope.launch {
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
}
}
}
cancelButton()
@@ -38,6 +38,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet