Improve mime-type handling
This commit is contained in:
@@ -74,6 +74,7 @@ android {
|
||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||
'-opt-in=coil3.annotation.ExperimentalCoilApi',
|
||||
'-opt-in=coil3.annotation.InternalCoilApi',
|
||||
]
|
||||
}
|
||||
lint {
|
||||
|
||||
@@ -4,26 +4,26 @@ import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.aomedia.avif.android.AvifDecoder
|
||||
import org.aomedia.avif.android.AvifDecoder.Info
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
|
||||
object BitmapDecoderCompat {
|
||||
|
||||
private const val FORMAT_AVIF = "avif"
|
||||
|
||||
@Blocking
|
||||
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
|
||||
fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) {
|
||||
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||
@@ -33,7 +33,7 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap {
|
||||
fun decode(stream: InputStream, type: MimeType?, isMutable: Boolean = false): Bitmap {
|
||||
val format = type?.subtype
|
||||
if (format == FORMAT_AVIF) {
|
||||
return decodeAvif(stream.toByteBuffer())
|
||||
@@ -51,12 +51,20 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
|
||||
} else {
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
|
||||
@Blocking
|
||||
fun probeMimeType(file: File): MimeType? {
|
||||
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private fun detectBitmapType(file: File): MimeType? = runCatchingCancellable {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
return options.outMimeType?.toMimeTypeOrNull()
|
||||
}.getOrNull()
|
||||
|
||||
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
|
||||
bitmap ?: throw ImageDecodeException(null, format)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
@@ -12,6 +11,7 @@ import coil3.toAndroidUri
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Path.Companion.toPath
|
||||
import okio.openZip
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import coil3.Uri as CoilUri
|
||||
|
||||
@@ -25,7 +25,7 @@ class CbzFetcher(
|
||||
val entryName = requireNotNull(uri.fragment)
|
||||
SourceFetchResult(
|
||||
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||
@@ -78,13 +79,14 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
|
||||
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
|
||||
return response.map { body ->
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType()?.toMimeType(), isMutable = true)
|
||||
.use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import coil3.util.MimeTypeMap as CoilMimeTypeMap
|
||||
|
||||
object MimeTypes {
|
||||
|
||||
fun getMimeTypeFromExtension(fileName: String): MimeType? {
|
||||
return CoilMimeTypeMap.getMimeTypeFromExtension(getNormalizedExtension(fileName) ?: return null)
|
||||
?.toMimeTypeOrNull()
|
||||
}
|
||||
|
||||
fun getMimeTypeFromUrl(url: String): MimeType? {
|
||||
return CoilMimeTypeMap.getMimeTypeFromUrl(url)?.toMimeTypeOrNull()
|
||||
}
|
||||
|
||||
fun getExtension(mimeType: MimeType?): String? {
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType?.toString() ?: return null)?.nullIfEmpty()
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun probeMimeType(file: File): MimeType? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
runCatchingCancellable {
|
||||
Files.probeContentType(file.toPath())?.toMimeTypeOrNull()
|
||||
}.getOrNull()?.let { return it }
|
||||
}
|
||||
return getMimeTypeFromExtension(file.name)
|
||||
}
|
||||
|
||||
fun getNormalizedExtension(name: String): String? = name
|
||||
.lowercase()
|
||||
.removeSuffix('~')
|
||||
.removeSuffix(".tmp")
|
||||
.substringAfterLast('.', "")
|
||||
.takeIf { it.length in 2..5 }
|
||||
}
|
||||
@@ -7,15 +7,13 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.database.getStringOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
@@ -41,12 +39,6 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output ->
|
||||
output.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
|
||||
val ZipEntry.mimeType: MediaType?
|
||||
get() {
|
||||
val ext = name.substringAfterLast('.')
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
fun File.getStorageName(context: Context): String = runCatching {
|
||||
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -115,3 +107,6 @@ fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VER
|
||||
val walk = walk()
|
||||
if (includeDirectories) walk else walk.filter { it.isFile }
|
||||
}
|
||||
|
||||
val File.normalizedExtension: String?
|
||||
get() = MimeTypes.getNormalizedExtension(name)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import okhttp3.MediaType
|
||||
|
||||
private const val TYPE_IMAGE = "image"
|
||||
private val REGEX_MIME = Regex("^\\w+/([-+.\\w]+|\\*)$", RegexOption.IGNORE_CASE)
|
||||
|
||||
@JvmInline
|
||||
value class MimeType(private val value: String) {
|
||||
|
||||
val type: String?
|
||||
get() = value.substringBefore('/', "").takeIfSpecified()
|
||||
|
||||
val subtype: String?
|
||||
get() = value.substringAfterLast('/', "").takeIfSpecified()
|
||||
|
||||
private fun String.takeIfSpecified(): String? = takeUnless {
|
||||
it.isEmpty() || it == "*"
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
fun MediaType.toMimeType(): MimeType = MimeType("$type/$subtype")
|
||||
|
||||
fun String.toMimeTypeOrNull(): MimeType? = if (REGEX_MIME.matches(this)) {
|
||||
MimeType(lowercase())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val MimeType.isImage: Boolean
|
||||
get() = type == TYPE_IMAGE
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toUri
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
@@ -21,8 +20,10 @@ import okio.Path.Companion.toOkioPath
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
@@ -47,7 +48,7 @@ class MangaPageFetcher(
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), options.fileSystem),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
@@ -67,13 +68,13 @@ class MangaPageFetcher(
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response.toNetworkResponse())
|
||||
}
|
||||
val mimeType = response.mimeType
|
||||
val mimeType = response.mimeType?.toMimeTypeOrNull()
|
||||
val file = response.requireBody().use {
|
||||
pagesCache.put(pageUrl, it.source(), mimeType)
|
||||
}
|
||||
SourceFetchResult(
|
||||
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||
mimeType = mimeType,
|
||||
mimeType = mimeType?.toString(),
|
||||
dataSource = DataSource.NETWORK,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.BackoffPolicy
|
||||
@@ -25,6 +24,7 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -42,6 +43,7 @@ import okio.buffer
|
||||
import okio.sink
|
||||
import okio.use
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
|
||||
import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
@@ -49,7 +51,9 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.Throttler
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag
|
||||
@@ -62,6 +66,7 @@ import org.koitharu.kotatsu.core.util.ext.getWorkInputData
|
||||
import org.koitharu.kotatsu.core.util.ext.getWorkSpec
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.withTicker
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator
|
||||
@@ -132,7 +137,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
downloadMangaImpl(manga, task, downloadedIds)
|
||||
}
|
||||
Result.success(currentState.toWorkData())
|
||||
} catch (e: CancellationException) {
|
||||
} catch (_: CancellationException) {
|
||||
withContext(NonCancellable) {
|
||||
val notification = notificationFactory.create(currentState.copy(isStopped = true))
|
||||
notificationManager.notify(id.hashCode(), notification)
|
||||
@@ -201,7 +206,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
|
||||
if (coverUrl.isNotEmpty()) {
|
||||
downloadFile(coverUrl, destination, repo.source).let { file ->
|
||||
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
||||
output.addCover(file, getMediaType(coverUrl, file))
|
||||
file.deleteAwait()
|
||||
}
|
||||
}
|
||||
@@ -230,7 +235,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
chapter = chapter,
|
||||
file = file,
|
||||
pageNumber = pageIndex,
|
||||
ext = MimeTypeMap.getFileExtensionFromUrl(url),
|
||||
type = getMediaType(url, file),
|
||||
)
|
||||
if (file.extension == "tmp") {
|
||||
file.deleteAwait()
|
||||
@@ -354,6 +359,13 @@ class DownloadWorker @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getMediaType(url: String, file: File): MimeType? = runInterruptible(Dispatchers.IO) {
|
||||
BitmapDecoderCompat.probeMimeType(file)?.let {
|
||||
return@runInterruptible it
|
||||
}
|
||||
MimeTypes.getMimeTypeFromUrl(url)
|
||||
}
|
||||
|
||||
private suspend fun downloadFile(
|
||||
url: String,
|
||||
destination: File,
|
||||
@@ -364,18 +376,29 @@ class DownloadWorker @AssistedInject constructor(
|
||||
return imageProxyInterceptor.interceptPageRequest(request, okHttp)
|
||||
.ensureSuccess()
|
||||
.use { response ->
|
||||
val file = File(destination, UUID.randomUUID().toString() + ".tmp")
|
||||
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.sink(append = false).buffer().use {
|
||||
it.writeAllCancellable(body.source())
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
file.delete()
|
||||
file?.delete()
|
||||
throw e
|
||||
}
|
||||
file
|
||||
checkNotNull(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import okio.sink
|
||||
import okio.use
|
||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
@@ -59,7 +61,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(url: String, source: Source, mimeType: String?): File = withContext(Dispatchers.IO) {
|
||||
suspend fun put(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
|
||||
val file = createBufferFile(url, mimeType)
|
||||
try {
|
||||
val bytes = file.sink(append = false).buffer().use {
|
||||
@@ -78,7 +80,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
|
||||
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||
val file = createBufferFile(url, "image/png")
|
||||
val file = createBufferFile(url, MimeType("image/png"))
|
||||
try {
|
||||
bitmap.compressToPNG(file)
|
||||
val cache = lruCache.get()
|
||||
@@ -107,9 +109,8 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(SIZE_DEFAULT)
|
||||
|
||||
private suspend fun createBufferFile(url: String, mimeType: String?): File {
|
||||
val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
|
||||
?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
|
||||
private suspend fun createBufferFile(url: String, mimeType: MimeType?): File {
|
||||
val ext = MimeTypes.getExtension(mimeType) ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
|
||||
val cacheDir = cacheDir.get()
|
||||
val rootDir = checkNotNull(cacheDir.parentFile) { "Cannot get parent for ${cacheDir.absolutePath}" }
|
||||
val name = UUID.randomUUID().toString() + "." + ext
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.local.data.input
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,8 +17,10 @@ import okio.openZip
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||
import org.koitharu.kotatsu.core.util.ext.isFileUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isImage
|
||||
import org.koitharu.kotatsu.core.util.ext.isRegularFile
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||
@@ -87,7 +88,6 @@ class LocalMangaParser(private val uri: Uri) {
|
||||
} else {
|
||||
val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase()
|
||||
val coverEntry = fileSystem.findFirstImage(rootPath)
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
Manga(
|
||||
id = rootFile.absolutePath.longHashCode(),
|
||||
title = title,
|
||||
@@ -103,7 +103,7 @@ class LocalMangaParser(private val uri: Uri) {
|
||||
when {
|
||||
path == coverEntry -> null
|
||||
!fileSystem.isRegularFile(path) -> null
|
||||
mimeTypeMap.isImage(path) -> path.parent
|
||||
path.isImage() -> path.parent
|
||||
hasZipExtension(path.name) -> path
|
||||
else -> null
|
||||
}
|
||||
@@ -157,10 +157,7 @@ class LocalMangaParser(private val uri: Uri) {
|
||||
val pattern = index.getChapterNamesPattern(chapter)
|
||||
entries.filter { x -> x.name.substringBefore('.').matches(pattern) }
|
||||
} else {
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
entries.filter { x ->
|
||||
mimeTypeMap.isImage(x) && x.parent == rootPath
|
||||
}
|
||||
entries.filter { x -> x.isImage() && x.parent == rootPath }
|
||||
}.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() })
|
||||
.map { x ->
|
||||
val entryUri = chapterUri.child(x, resolve = true).toString()
|
||||
@@ -218,21 +215,18 @@ class LocalMangaParser(private val uri: Uri) {
|
||||
rootPath: Path,
|
||||
recursive: Boolean
|
||||
): Path? = runCatchingCancellable {
|
||||
val mimeTypeMap = MimeTypeMap.getSingleton()
|
||||
if (recursive) {
|
||||
listRecursively(rootPath)
|
||||
} else {
|
||||
list(rootPath).asSequence()
|
||||
}.filter { isRegularFile(it) && mimeTypeMap.isImage(it) }
|
||||
}.filter { isRegularFile(it) && it.isImage() }
|
||||
.toListSorted(compareBy(AlphanumComparator()) { x -> x.toString() })
|
||||
.firstOrNull()
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
|
||||
private fun MimeTypeMap.isImage(path: Path): Boolean =
|
||||
getMimeTypeFromExtension(path.name.substringAfterLast('.'))
|
||||
?.startsWith("image/") == true
|
||||
private fun Path.isImage(): Boolean = MimeTypes.getMimeTypeFromExtension(name)?.isImage == true
|
||||
|
||||
private fun Uri.resolve(): Uri = if (isFileUri()) {
|
||||
val file = toFile()
|
||||
|
||||
@@ -8,6 +8,8 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.core.zip.ZipOutput
|
||||
@@ -35,10 +37,10 @@ class LocalMangaDirOutput(
|
||||
|
||||
override suspend fun mergeWithExisting() = Unit
|
||||
|
||||
override suspend fun addCover(file: File, ext: String) = mutex.withLock {
|
||||
override suspend fun addCover(file: File, type: MimeType?) = mutex.withLock {
|
||||
val name = buildString {
|
||||
append("cover")
|
||||
if (ext.isNotEmpty() && ext.length <= 4) {
|
||||
MimeTypes.getExtension(type)?.let { ext ->
|
||||
append('.')
|
||||
append(ext)
|
||||
}
|
||||
@@ -50,14 +52,14 @@ class LocalMangaDirOutput(
|
||||
flushIndex()
|
||||
}
|
||||
|
||||
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) =
|
||||
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?) =
|
||||
mutex.withLock {
|
||||
val output = chaptersOutput.getOrPut(chapter.value) {
|
||||
ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP))
|
||||
}
|
||||
val name = buildString {
|
||||
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
|
||||
if (ext.isNotEmpty() && ext.length <= 4) {
|
||||
MimeTypes.getExtension(type)?.let { ext ->
|
||||
append('.')
|
||||
append(ext)
|
||||
}
|
||||
@@ -96,7 +98,9 @@ class LocalMangaDirOutput(
|
||||
}
|
||||
|
||||
suspend fun deleteChapters(ids: Set<Long>) = mutex.withLock {
|
||||
val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters) {
|
||||
val chapters = checkNotNull(
|
||||
(index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters,
|
||||
) {
|
||||
"No chapters found"
|
||||
}.withIndex()
|
||||
val victimsIds = ids.toMutableSet()
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.Closeable
|
||||
import org.koitharu.kotatsu.core.prefs.DownloadFormat
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -20,9 +21,9 @@ sealed class LocalMangaOutput(
|
||||
|
||||
abstract suspend fun mergeWithExisting()
|
||||
|
||||
abstract suspend fun addCover(file: File, ext: String)
|
||||
abstract suspend fun addCover(file: File, type: MimeType?)
|
||||
|
||||
abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String)
|
||||
abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?)
|
||||
|
||||
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.core.util.ext.readText
|
||||
import org.koitharu.kotatsu.core.zip.ZipOutput
|
||||
@@ -39,10 +41,10 @@ class LocalMangaZipOutput(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addCover(file: File, ext: String) = mutex.withLock {
|
||||
override suspend fun addCover(file: File, type: MimeType?) = mutex.withLock {
|
||||
val name = buildString {
|
||||
append(FILENAME_PATTERN.format(0, 0, 0))
|
||||
if (ext.isNotEmpty() && ext.length <= 4) {
|
||||
MimeTypes.getExtension(type)?.let { ext ->
|
||||
append('.')
|
||||
append(ext)
|
||||
}
|
||||
@@ -53,11 +55,11 @@ class LocalMangaZipOutput(
|
||||
index.setCoverEntry(name)
|
||||
}
|
||||
|
||||
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) =
|
||||
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, type: MimeType?) =
|
||||
mutex.withLock {
|
||||
val name = buildString {
|
||||
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
|
||||
if (ext.isNotEmpty() && ext.length <= 4) {
|
||||
MimeTypes.getExtension(type)?.let { ext ->
|
||||
append('.')
|
||||
append(ext)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
|
||||
@@ -47,9 +48,9 @@ import org.koitharu.kotatsu.core.util.ext.isFileUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import org.koitharu.kotatsu.core.util.ext.mimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||
@@ -57,7 +58,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
@@ -66,7 +66,6 @@ import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@@ -146,7 +145,7 @@ class PageLoader @Inject constructor(
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
context.ensureRamAtLeast(entry.size * 2)
|
||||
zip.getInputStream(entry).use {
|
||||
BitmapDecoderCompat.decode(it, entry.mimeType)
|
||||
BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +249,7 @@ class PageLoader @Inject constructor(
|
||||
val request = createPageRequest(pageUrl, page.source)
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||
response.requireBody().withProgress(progress).use {
|
||||
cache.put(pageUrl, it.source(), response.mimeType)
|
||||
cache.put(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||
}
|
||||
}.toUri()
|
||||
}
|
||||
@@ -264,7 +263,7 @@ class PageLoader @Inject constructor(
|
||||
private fun Deferred<Uri>.isValid(): Boolean {
|
||||
return getCompletionResultOrNull()?.map { uri ->
|
||||
uri.exists() && uri.isTargetNotEmpty()
|
||||
}?.getOrDefault(false) ?: true
|
||||
}?.getOrDefault(false) != false
|
||||
}
|
||||
|
||||
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import java.io.File
|
||||
|
||||
@@ -15,8 +15,7 @@ class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
|
||||
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input.substringAfterLast(File.separatorChar))
|
||||
intent.type = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
|
||||
intent.type = MimeTypes.getMimeTypeFromExtension(input)?.toString() ?: "image/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val defaultUri = input.toUriOrNull()?.run {
|
||||
path?.let { p ->
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
@@ -28,7 +26,9 @@ import okio.buffer
|
||||
import okio.openZip
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.isFileUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
@@ -99,10 +99,10 @@ class PageSaveHelper @AssistedInject constructor(
|
||||
val pageUri = pageLoader.loadPage(task.page, force = false)
|
||||
val proposedName = task.getFileBaseName()
|
||||
val ext = getPageExtension(pageUrl, pageUri)
|
||||
val mime = requireNotNull(MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)) {
|
||||
val mime = requireNotNull(MimeTypes.getMimeTypeFromExtension("_.$ext")) {
|
||||
"Unknown type of $proposedName"
|
||||
}
|
||||
val destination = destinationDir.createFile(mime, proposedName.substringBeforeLast('.'))
|
||||
val destination = destinationDir.createFile(mime.toString(), proposedName)
|
||||
copyImpl(pageUri, destination?.uri ?: throw IOException("Cannot create destination file"))
|
||||
result.add(destination.uri)
|
||||
}
|
||||
@@ -119,12 +119,7 @@ class PageSaveHelper @AssistedInject constructor(
|
||||
) { "Invalid page url: $url" }
|
||||
var extension = name.substringAfterLast('.', "")
|
||||
if (extension.length !in 2..4) {
|
||||
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) }
|
||||
extension = if (mimeType != null) {
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||
} else {
|
||||
EXTENSION_FALLBACK
|
||||
}
|
||||
extension = fileUri.toFileOrNull()?.let { file -> getImageExtension(file) } ?: EXTENSION_FALLBACK
|
||||
}
|
||||
return extension
|
||||
}
|
||||
@@ -155,8 +150,7 @@ class PageSaveHelper @AssistedInject constructor(
|
||||
if (proposedName == null) {
|
||||
return dir
|
||||
} else {
|
||||
val ext = proposedName.substringAfterLast('.', "")
|
||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
|
||||
val mime = MimeTypes.getMimeTypeFromExtension(proposedName)?.toString() ?: return null
|
||||
return dir.createFile(mime, proposedName.substringBeforeLast('.'))
|
||||
}
|
||||
}
|
||||
@@ -179,12 +173,8 @@ class PageSaveHelper @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
options.outMimeType
|
||||
private suspend fun getImageExtension(file: File): String? = runInterruptible(Dispatchers.IO) {
|
||||
MimeTypes.getExtension(BitmapDecoderCompat.probeMimeType(file))
|
||||
}
|
||||
|
||||
data class Task(
|
||||
|
||||
Reference in New Issue
Block a user