Fix concurrent manga downloading #154

This commit is contained in:
Koitharu
2022-04-29 10:07:04 +03:00
parent 714b708fa9
commit 684b494edb
4 changed files with 107 additions and 18 deletions

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.download.domain
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.webkit.MimeTypeMap
import coil.ImageLoader
@@ -75,10 +74,12 @@ class DownloadManager(
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
@Suppress("NAME_SHADOWING") var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover)
localMangaRepository.lockManga(manga.id)
semaphore.acquire()
coroutineContext[WakeLockNode]?.acquire()
outState.value = DownloadState.Preparing(startId, manga, null)
var cover: Drawable? = null
val destination = localMangaRepository.getOutputDir()
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
val tempFileName = "${manga.id}_$startId.tmp"
@@ -88,16 +89,6 @@ class DownloadManager(
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
}
val repo = MangaRepository(manga.source)
cover = runCatching {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build()
).drawable
}.getOrNull()
outState.value = DownloadState.Preparing(startId, manga, cover)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data)
@@ -176,6 +167,7 @@ class DownloadManager(
}
coroutineContext[WakeLockNode]?.release()
semaphore.release()
localMangaRepository.unlockManga(manga.id)
}
}
@@ -208,6 +200,17 @@ class DownloadManager(
)
}
private suspend fun loadCover(manga: Manga) = runCatching {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build()
).drawable
}.getOrNull()
class Factory(
private val context: Context,
private val imageLoader: ImageLoader,

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.toCamelCase
import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.CompositeMutex
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.resolveName
@@ -34,6 +35,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter()
private val locks = CompositeMutex<Long>()
override suspend fun getList(
offset: Int,
@@ -112,11 +114,18 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
return file.deleteAwait()
}
suspend fun deleteChapters(manga: Manga, ids: Set<Long>) = runInterruptible(Dispatchers.IO) {
val uri = Uri.parse(manga.url)
val file = uri.toFile()
val cbz = CbzMangaOutput(file, manga)
CbzMangaOutput.filterChapters(cbz, ids)
suspend fun deleteChapters(manga: Manga, ids: Set<Long>) {
lockManga(manga.id)
try {
runInterruptible(Dispatchers.IO) {
val uri = Uri.parse(manga.url)
val file = uri.toFile()
val cbz = CbzMangaOutput(file, manga)
CbzMangaOutput.filterChapters(cbz, ids)
}
} finally {
unlockManga(manga.id)
}
}
@WorkerThread
@@ -278,6 +287,14 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
}
}
suspend fun lockManga(id: Long) {
locks.lock(id)
}
suspend fun unlockManga(id: Long) {
locks.unlock(id)
}
private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir ->
dir.listFiles(filenameFilter)?.toList().orEmpty()
}

View File

@@ -0,0 +1,66 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
import kotlin.coroutines.resume
class CompositeMutex<T : Any> : Set<T> {
private val data = HashMap<T, MutableList<CancellableContinuation<Unit>>>()
private val mutex = Mutex()
override val size: Int
get() = data.size
override fun contains(element: T): Boolean {
return data.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> data.containsKey(x) }
}
override fun isEmpty(): Boolean {
return data.isEmpty()
}
override fun iterator(): Iterator<T> {
return data.keys.iterator()
}
suspend fun lock(element: T) {
waitForRemoval(element)
mutex.withLock {
val lastValue = data.put(element, LinkedList<CancellableContinuation<Unit>>())
check(lastValue == null) {
"CompositeMutex is double-locked for $element"
}
}
}
suspend fun unlock(element: T) {
val continuations = mutex.withLock {
checkNotNull(data.remove(element)) {
"CompositeMutex is not locked for $element"
}
}
continuations.forEach { c ->
if (c.isActive) {
c.resume(Unit)
}
}
}
private suspend fun waitForRemoval(element: T) {
val list = data[element] ?: return
suspendCancellableCoroutine<Unit> { continuation ->
list.add(continuation)
continuation.invokeOnCancellation {
list.remove(continuation)
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.utils.progress
import android.os.SystemClock
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@@ -11,6 +12,7 @@ class TimeLeftEstimator {
private var times = ArrayList<Int>()
private var lastTick: Tick? = null
private val tooLargeTime = TimeUnit.DAYS.toMillis(1)
fun tick(value: Int, total: Int) {
if (total < 0) {
@@ -36,7 +38,8 @@ class TimeLeftEstimator {
}
val timePerTick = times.average()
val ticksLeft = progress.total - progress.value
return (ticksLeft * timePerTick).roundToLong()
val eta = (ticksLeft * timePerTick).roundToLong()
return if (eta < tooLargeTime) eta else NO_TIME
}
private class Tick(