Skip to content

Commit

Permalink
Load local manga pages directly #552
Browse files Browse the repository at this point in the history
  • Loading branch information
Koitharu committed Nov 24, 2023
1 parent 0c839ce commit 880dd6d
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 78 deletions.
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 598
versionName = '6.3.1'
versionCode = 599
versionName = '6.3.2'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
Expand Down Expand Up @@ -134,7 +134,7 @@ dependencies {

implementation 'io.coil-kt:coil-base:2.5.0'
implementation 'io.coil-kt:coil-svg:2.5.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0fef1a47c9'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:826d7b4512'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import java.nio.file.attribute.BasicFileAttributes
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.walk
import kotlin.io.path.readAttributes
import kotlin.io.path.walk

fun File.subdir(name: String) = File(this, name).also {
if (!it.exists()) it.mkdirs()
Expand Down Expand Up @@ -50,7 +50,7 @@ fun File.getStorageName(context: Context): String = runCatching {
}
}.getOrNull() ?: context.getString(R.string.other_storage)

fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null
fun Uri.toFileOrNull() = if (scheme == URI_SCHEME_FILE) path?.let(::File) else null

suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
delete() || deleteRecursively()
Expand Down
50 changes: 50 additions & 0 deletions app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Uri.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.core.util.ext

import android.net.Uri
import androidx.core.net.toFile
import okio.Source
import okio.source
import okio.use
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import java.io.File
import java.util.zip.ZipFile

const val URI_SCHEME_FILE = "file"
const val URI_SCHEME_ZIP = "file+zip"

fun Uri.exists(): Boolean = when (scheme) {
URI_SCHEME_FILE -> toFile().exists()
URI_SCHEME_ZIP -> {
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { it.getEntry(fragment) != null }
}

else -> unsupportedUri(this)
}

fun Uri.isTargetNotEmpty(): Boolean = when (scheme) {
URI_SCHEME_FILE -> toFile().isNotEmpty()
URI_SCHEME_ZIP -> {
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { (it.getEntry(fragment)?.size ?: 0L) != 0L }
}

else -> unsupportedUri(this)
}

fun Uri.source(): Source = when (scheme) {
URI_SCHEME_FILE -> toFile().source()
URI_SCHEME_ZIP -> {
val zip = ZipFile(schemeSpecificPart)
val entry = zip.getEntry(fragment)
zip.getInputStream(entry).source().withExtraCloseable(zip)
}

else -> unsupportedUri(this)
}

fun File.toZipUri(entryName: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryName")

private fun unsupportedUri(uri: Uri): Nothing {
throw IllegalArgumentException("Bad uri $uri: only schemes $URI_SCHEME_FILE and $URI_SCHEME_ZIP are supported")
}
32 changes: 0 additions & 32 deletions app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipPool.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ fun hasCbzExtension(string: String): Boolean {
return isCbzExtension(ext)
}

fun hasCbzExtension(file: File) = isCbzExtension(file.name)
fun hasCbzExtension(file: File) = isCbzExtension(file.extension)

fun isCbzUri(uri: Uri) = isCbzExtension(uri.scheme)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_FILE
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.resolveFile
Expand Down Expand Up @@ -84,7 +85,7 @@ class LocalStorageManager @Inject constructor(
}

suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
if (uri.scheme == "file") {
if (uri.scheme == URI_SCHEME_FILE) {
uri.toFile()
} else {
uri.resolveFile(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.net.Uri
import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.core.net.toUri
import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.lifecycle.RetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -33,15 +34,16 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.exists
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.isTargetNotEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.core.zip.ZipPool
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isCbzUri
import org.koitharu.kotatsu.parsers.model.MangaPage
Expand Down Expand Up @@ -71,21 +73,19 @@ class PageLoader @Inject constructor(

val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default

private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
private val semaphore = Semaphore(3)
private val convertLock = Mutex()
private val prefetchLock = Mutex()
private var repository: MangaRepository? = null
private val prefetchQueue = LinkedList<MangaPage>()
private val zipPool = ZipPool(2)
private val counter = AtomicInteger(0)
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive

override fun onCleared() {
synchronized(tasks) {
tasks.clear()
}
zipPool.evictAll()
}

fun isPrefetchApplicable(): Boolean {
Expand Down Expand Up @@ -113,7 +113,7 @@ class PageLoader @Inject constructor(
}
}

fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<Uri, Float> {
var task = tasks[page.id]?.takeIf { it.isValid() }
if (force) {
task?.cancel()
Expand All @@ -127,7 +127,7 @@ class PageLoader @Inject constructor(
return task
}

suspend fun loadPage(page: MangaPage, force: Boolean): File {
suspend fun loadPage(page: MangaPage, force: Boolean): Uri {
return loadPageAsync(page, force).await()
}

Expand Down Expand Up @@ -167,11 +167,11 @@ class PageLoader @Inject constructor(
}
}

private fun loadPageAsyncImpl(page: MangaPage, skipCache: Boolean): ProgressDeferred<File, Float> {
private fun loadPageAsyncImpl(page: MangaPage, skipCache: Boolean): ProgressDeferred<Uri, Float> {
val progress = MutableStateFlow(PROGRESS_UNDEFINED)
val deferred = loaderScope.async {
if (!skipCache) {
cache.get(page.url)?.let { return@async it }
cache.get(page.url)?.let { return@async it.toUri() }
}
counter.incrementAndGet()
try {
Expand All @@ -195,36 +195,30 @@ class PageLoader @Inject constructor(
}
}

private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File = semaphore.withPermit {
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): Uri = semaphore.withPermit {
val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
val uri = Uri.parse(pageUrl)
return if (isCbzUri(uri)) {
runInterruptible(Dispatchers.IO) {
zipPool[uri]
}.use {
cache.put(pageUrl, it)
}
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
} else {
val request = createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) {
"Null response"
}
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}
}.toUri()
}
}

private fun isLowRam(): Boolean {
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
}

private fun Deferred<File>.isValid(): Boolean {
return getCompletionResultOrNull()?.map { file ->
file.exists() && file.isNotEmpty()
private fun Deferred<Uri>.isValid(): Boolean {
return getCompletionResultOrNull()?.map { uri ->
uri.exists() && uri.isTargetNotEmpty()
}?.getOrDefault(false) ?: true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException
import okio.buffer
import okio.sink
import okio.source
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
Expand All @@ -41,8 +42,8 @@ class PageSaveHelper @Inject constructor(
saveLauncher: ActivityResultLauncher<String>,
): Uri {
val pageUrl = pageLoader.getPageUrl(page)
val pageFile = pageLoader.loadPage(page, force = false)
val proposedName = getProposedFileName(pageUrl, pageFile)
val pageUri = pageLoader.loadPage(page, force = false)
val proposedName = getProposedFileName(pageUrl, pageUri)
val destination = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { cont ->
continuation = cont
Expand All @@ -54,7 +55,7 @@ class PageSaveHelper @Inject constructor(
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.sink()?.buffer()
}?.use { output ->
pageFile.source().use { input ->
pageUri.source().use { input ->
output.writeAllCancellable(input)
}
} ?: throw IOException("Output stream is null")
Expand All @@ -65,7 +66,7 @@ class PageSaveHelper @Inject constructor(
resume(uri)
} != null

private suspend fun getProposedFileName(url: String, file: File): String {
private suspend fun getProposedFileName(url: String, fileUri: Uri): String {
var name = if (url.startsWith("cbz://")) {
requireNotNull(url.toUri().fragment)
} else {
Expand All @@ -74,7 +75,7 @@ class PageSaveHelper @Inject constructor(
var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.')
if (extension.length !in 2..4) {
val mimeType = getImageMimeType(file)
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) }
extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else {
Expand Down
Loading

0 comments on commit 880dd6d

Please sign in to comment.