From de1a297338528f8e9f96effa3e53e3b3662a0e8b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Jul 2025 09:38:35 +0300 Subject: [PATCH] Fix downloading edited manga (close #1493) --- .../org/koitharu/kotatsu/core/util/ext/IO.kt | 9 ++++ .../download/ui/worker/DownloadWorker.kt | 49 ++++++++++++++----- .../data/importer/SingleMangaImporter.kt | 16 +++--- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt index 3882949e8..42ee4b89f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -1,5 +1,8 @@ package org.koitharu.kotatsu.core.util.ext +import android.content.ContentResolver +import android.net.Uri +import androidx.annotation.CheckResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.currentCoroutineContext @@ -12,6 +15,7 @@ import okio.FileSystem import okio.IOException import okio.Path import okio.Source +import okio.source import org.koitharu.kotatsu.core.util.CancellableSource import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody import java.io.ByteArrayOutputStream @@ -57,3 +61,8 @@ fun FileSystem.isRegularFile(path: Path) = try { } catch (_: IOException) { false } + +@CheckResult +fun ContentResolver.openSource(uri: Uri): Source = checkNotNull(openInputStream(uri)) { + "Cannot open input stream from $uri" +}.source() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index aa4a86e8e..5506d028e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.pm.ServiceInfo import android.os.Build import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.hilt.work.HiltWorker import androidx.work.BackoffPolicy import androidx.work.Constraints @@ -64,8 +65,11 @@ import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getWorkInputData import org.koitharu.kotatsu.core.util.ext.getWorkSpec +import org.koitharu.kotatsu.core.util.ext.openSource import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toMimeType +import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull import org.koitharu.kotatsu.core.util.ext.withTicker import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator @@ -371,6 +375,25 @@ class DownloadWorker @AssistedInject constructor( destination: File, source: MangaSource, ): File { + if (url.startsWith("content:", ignoreCase = true) || url.startsWith("file:", ignoreCase = true)) { + val uri = url.toUri() + val cr = applicationContext.contentResolver + val ext = uri.toFileOrNull()?.let { + MimeTypes.getNormalizedExtension(it.name) + } ?: cr.getType(uri)?.toMimeTypeOrNull()?.let { MimeTypes.getExtension(it) } + val file = destination.createTempFile(ext) + try { + cr.openSource(uri).use { input -> + file.sink(append = false).buffer().use { + it.writeAllCancellable(input) + } + } + } catch (e: Exception) { + file.delete() + throw e + } + return file + } val request = PageLoader.createPageRequest(url, source) slowdownDispatcher.delay(source) return imageProxyInterceptor.interceptPageRequest(request, okHttp) @@ -379,22 +402,14 @@ class DownloadWorker @AssistedInject constructor( var file: File? = null try { response.requireBody().use { body -> - file = File( - destination, - buildString { - append(UUID.randomUUID().toString()) - MimeTypes.getExtension(body.contentType()?.toMimeType())?.let { ext -> - append('.') - append(ext) - } - append(".tmp") - }, + file = destination.createTempFile( + ext = MimeTypes.getExtension(body.contentType()?.toMimeType()) ) file.sink(append = false).buffer().use { it.writeAllCancellable(body.source()) } } - } catch (e: CancellationException) { + } catch (e: Exception) { file?.delete() throw e } @@ -402,6 +417,18 @@ class DownloadWorker @AssistedInject constructor( } } + private fun File.createTempFile(ext: String?) = File( + this, + buildString { + append(UUID.randomUUID().toString()) + if (!ext.isNullOrEmpty()) { + append('.') + append(ext) + } + append(".tmp") + }, + ) + private suspend fun publishState(state: DownloadState) { val previousState = currentState lastPublishedState = state diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt index 91eb31b7b..e3b805c3b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt @@ -11,8 +11,8 @@ import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import okio.buffer import okio.sink -import okio.source import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException +import org.koitharu.kotatsu.core.util.ext.openSource import org.koitharu.kotatsu.core.util.ext.resolveName import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.local.data.LocalStorageChanges @@ -51,12 +51,12 @@ class SingleMangaImporter @Inject constructor( } val dest = File(getOutputDir(), name) runInterruptible { - contentResolver.openInputStream(uri) - }?.use { source -> + contentResolver.openSource(uri) + }.use { source -> dest.sink().buffer().use { output -> - output.writeAllCancellable(source.source()) + output.writeAllCancellable(source) } - } ?: throw IOException("Cannot open input stream: $uri") + } LocalMangaParser(dest).getManga(withDetails = false) } @@ -80,7 +80,7 @@ class SingleMangaImporter @Inject constructor( docFile.copyTo(subDir) } } else { - inputStream().source().use { input -> + source().use { input -> File(destDir, requireName()).sink().buffer().use { output -> output.writeAllCancellable(input) } @@ -92,8 +92,8 @@ class SingleMangaImporter @Inject constructor( return storageManager.getDefaultWriteableDir() ?: throw IOException("External files dir unavailable") } - private suspend fun DocumentFile.inputStream() = runInterruptible(Dispatchers.IO) { - contentResolver.openInputStream(uri) ?: throw IOException("Cannot open input stream: $uri") + private suspend fun DocumentFile.source() = runInterruptible(Dispatchers.IO) { + contentResolver.openSource(uri) } private fun DocumentFile.requireName(): String {