Disk cache for favicons
This commit is contained in:
@@ -42,18 +42,21 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
|||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||||
|
import org.koitharu.kotatsu.core.util.FileSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
|
||||||
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
|
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
|
import org.koitharu.kotatsu.local.data.FaviconCache
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
|
import org.koitharu.kotatsu.local.data.PageCache
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
@@ -101,7 +104,7 @@ interface AppModule {
|
|||||||
fun provideCoil(
|
fun provideCoil(
|
||||||
@LocalizedAppContext context: Context,
|
@LocalizedAppContext context: Context,
|
||||||
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
faviconFetcherFactory: FaviconFetcher.Factory,
|
||||||
imageProxyInterceptor: ImageProxyInterceptor,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
coverRestoreInterceptor: CoverRestoreInterceptor,
|
coverRestoreInterceptor: CoverRestoreInterceptor,
|
||||||
@@ -138,7 +141,7 @@ interface AppModule {
|
|||||||
add(SvgDecoder.Factory())
|
add(SvgDecoder.Factory())
|
||||||
add(CbzFetcher.Factory())
|
add(CbzFetcher.Factory())
|
||||||
add(AvifImageDecoder.Factory())
|
add(AvifImageDecoder.Factory())
|
||||||
add(FaviconFetcher.Factory(mangaRepositoryFactory))
|
add(faviconFetcherFactory)
|
||||||
add(MangaPageKeyer())
|
add(MangaPageKeyer())
|
||||||
add(pageFetcherFactory)
|
add(pageFetcherFactory)
|
||||||
add(imageProxyInterceptor)
|
add(imageProxyInterceptor)
|
||||||
@@ -195,5 +198,29 @@ interface AppModule {
|
|||||||
fun provideWorkManager(
|
fun provideWorkManager(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
): WorkManager = WorkManager.getInstance(context)
|
): WorkManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@PageCache
|
||||||
|
fun providePageCache(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
) = LocalStorageCache(
|
||||||
|
context = context,
|
||||||
|
dir = CacheDir.PAGES,
|
||||||
|
defaultSize = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||||
|
minSize = FileSize.MEGABYTES.convert(20, FileSize.BYTES),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@FaviconCache
|
||||||
|
fun provideFaviconCache(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
) = LocalStorageCache(
|
||||||
|
context = context,
|
||||||
|
dir = CacheDir.FAVICONS,
|
||||||
|
defaultSize = FileSize.MEGABYTES.convert(8, FileSize.BYTES),
|
||||||
|
minSize = FileSize.MEGABYTES.convert(2, FileSize.BYTES),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ import coil3.ColorImage
|
|||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.asImage
|
import coil3.asImage
|
||||||
import coil3.decode.DataSource
|
import coil3.decode.DataSource
|
||||||
|
import coil3.decode.ImageSource
|
||||||
import coil3.fetch.FetchResult
|
import coil3.fetch.FetchResult
|
||||||
import coil3.fetch.Fetcher
|
import coil3.fetch.Fetcher
|
||||||
import coil3.fetch.ImageFetchResult
|
import coil3.fetch.ImageFetchResult
|
||||||
|
import coil3.fetch.SourceFetchResult
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import coil3.size.pxOrElse
|
import coil3.size.pxOrElse
|
||||||
import coil3.toAndroidUri
|
import coil3.toAndroidUri
|
||||||
|
import coil3.toBitmap
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import okio.FileSystem
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
@@ -26,8 +31,16 @@ import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||||
|
import org.koitharu.kotatsu.local.data.FaviconCache
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
import coil3.Uri as CoilUri
|
import coil3.Uri as CoilUri
|
||||||
|
|
||||||
@@ -36,6 +49,7 @@ class FaviconFetcher(
|
|||||||
private val options: Options,
|
private val options: Options,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val localStorageCache: LocalStorageCache,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
@@ -61,6 +75,16 @@ class FaviconFetcher(
|
|||||||
options.size.width.pxOrElse { FALLBACK_SIZE },
|
options.size.width.pxOrElse { FALLBACK_SIZE },
|
||||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||||
)
|
)
|
||||||
|
val cacheKey = options.diskCacheKey ?: "${repository.source.name}_$sizePx"
|
||||||
|
if (options.diskCachePolicy.readEnabled) {
|
||||||
|
localStorageCache[cacheKey]?.let { file ->
|
||||||
|
return SourceFetchResult(
|
||||||
|
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||||
|
mimeType = MimeTypes.probeMimeType(file)?.toString(),
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
var favicons = repository.getFavicons()
|
var favicons = repository.getFavicons()
|
||||||
var lastError: Exception? = null
|
var lastError: Exception? = null
|
||||||
while (favicons.isNotEmpty()) {
|
while (favicons.isNotEmpty()) {
|
||||||
@@ -69,7 +93,11 @@ class FaviconFetcher(
|
|||||||
try {
|
try {
|
||||||
val result = imageLoader.fetch(icon.url, options)
|
val result = imageLoader.fetch(icon.url, options)
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result
|
return if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
writeToCache(cacheKey, result)
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
favicons -= icon
|
favicons -= icon
|
||||||
}
|
}
|
||||||
@@ -97,8 +125,39 @@ class FaviconFetcher(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(
|
private suspend fun writeToCache(key: String, result: FetchResult): FetchResult = runCatchingCancellable {
|
||||||
|
when (result) {
|
||||||
|
is ImageFetchResult -> {
|
||||||
|
if (result.dataSource == DataSource.NETWORK) {
|
||||||
|
localStorageCache.set(key, result.image.toBitmap()).asFetchResult()
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is SourceFetchResult -> {
|
||||||
|
if (result.dataSource == DataSource.NETWORK) {
|
||||||
|
result.source.source().use {
|
||||||
|
localStorageCache.set(key, it, result.mimeType?.toMimeTypeOrNull()).asFetchResult()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}.getOrDefault(result)
|
||||||
|
|
||||||
|
private fun File.asFetchResult() = SourceFetchResult(
|
||||||
|
source = ImageSource(toOkioPath(), FileSystem.SYSTEM),
|
||||||
|
mimeType = MimeTypes.probeMimeType(this)?.toString(),
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Factory @Inject constructor(
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
@FaviconCache private val faviconCache: LocalStorageCache,
|
||||||
) : Fetcher.Factory<CoilUri> {
|
) : Fetcher.Factory<CoilUri> {
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
@@ -106,7 +165,7 @@ class FaviconFetcher(
|
|||||||
options: Options,
|
options: Options,
|
||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader
|
||||||
): Fetcher? = if (data.scheme == URI_SCHEME_FAVICON) {
|
): Fetcher? = if (data.scheme == URI_SCHEME_FAVICON) {
|
||||||
FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory)
|
FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory, faviconCache)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import org.koitharu.kotatsu.core.util.MimeTypes
|
|||||||
import org.koitharu.kotatsu.core.util.ext.fetch
|
import org.koitharu.kotatsu.core.util.ext.fetch
|
||||||
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
|
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
|
||||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||||
|
import org.koitharu.kotatsu.local.data.PageCache
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||||
@@ -34,7 +35,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class MangaPageFetcher(
|
class MangaPageFetcher(
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val pagesCache: PagesCache,
|
private val pagesCache: LocalStorageCache,
|
||||||
private val options: Options,
|
private val options: Options,
|
||||||
private val page: MangaPage,
|
private val page: MangaPage,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
@@ -53,7 +54,7 @@ class MangaPageFetcher(
|
|||||||
val repo = mangaRepositoryFactory.create(page.source)
|
val repo = mangaRepositoryFactory.create(page.source)
|
||||||
val pageUrl = repo.getPageUrl(page)
|
val pageUrl = repo.getPageUrl(page)
|
||||||
if (options.diskCachePolicy.readEnabled) {
|
if (options.diskCachePolicy.readEnabled) {
|
||||||
pagesCache.get(pageUrl)?.let { file ->
|
pagesCache[pageUrl]?.let { file ->
|
||||||
return SourceFetchResult(
|
return SourceFetchResult(
|
||||||
source = ImageSource(file.toOkioPath(), options.fileSystem),
|
source = ImageSource(file.toOkioPath(), options.fileSystem),
|
||||||
mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
|
mimeType = MimeTypes.getMimeTypeFromExtension(file.name)?.toString(),
|
||||||
@@ -78,7 +79,7 @@ class MangaPageFetcher(
|
|||||||
}
|
}
|
||||||
val mimeType = response.mimeType?.toMimeTypeOrNull()
|
val mimeType = response.mimeType?.toMimeTypeOrNull()
|
||||||
val file = response.requireBody().use {
|
val file = response.requireBody().use {
|
||||||
pagesCache.put(pageUrl, it.source(), mimeType)
|
pagesCache.set(pageUrl, it.source(), mimeType)
|
||||||
}
|
}
|
||||||
SourceFetchResult(
|
SourceFetchResult(
|
||||||
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
|
||||||
@@ -107,7 +108,7 @@ class MangaPageFetcher(
|
|||||||
|
|
||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||||
private val pagesCache: PagesCache,
|
@PageCache private val pagesCache: LocalStorageCache,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
) : Fetcher.Factory<MangaPage> {
|
) : Fetcher.Factory<MangaPage> {
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ import org.koitharu.kotatsu.core.util.progress.RealtimeEtaEstimator
|
|||||||
import org.koitharu.kotatsu.download.domain.DownloadProgress
|
import org.koitharu.kotatsu.download.domain.DownloadProgress
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PageCache
|
||||||
import org.koitharu.kotatsu.local.data.TempFileFilter
|
import org.koitharu.kotatsu.local.data.TempFileFilter
|
||||||
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
|
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
|
||||||
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
|
||||||
@@ -103,7 +104,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
@Assisted appContext: Context,
|
@Assisted appContext: Context,
|
||||||
@Assisted params: WorkerParameters,
|
@Assisted params: WorkerParameters,
|
||||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||||
private val cache: PagesCache,
|
@PageCache private val cache: LocalStorageCache,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val mangaLock: MangaLock,
|
private val mangaLock: MangaLock,
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
@@ -233,7 +234,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
runFailsafe {
|
runFailsafe {
|
||||||
val url = repo.getPageUrl(page)
|
val url = repo.getPageUrl(page)
|
||||||
val file = cache.get(url)
|
val file = cache[url]
|
||||||
?: downloadFile(url, destination, repo.source)
|
?: downloadFile(url, destination, repo.source)
|
||||||
output.addPage(
|
output.addPage(
|
||||||
chapter = chapter,
|
chapter = chapter,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.local.data
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class PageCache
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class FaviconCache
|
||||||
@@ -5,7 +5,6 @@ import android.graphics.Bitmap
|
|||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.tomclaw.cache.DiskLruCache
|
import com.tomclaw.cache.DiskLruCache
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -14,7 +13,6 @@ import okio.buffer
|
|||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.use
|
import okio.use
|
||||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
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.MimeTypes
|
||||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||||
@@ -28,22 +26,24 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|||||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
class LocalStorageCache(
|
||||||
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
context: Context,
|
||||||
|
private val dir: CacheDir,
|
||||||
|
private val defaultSize: Long,
|
||||||
|
private val minSize: Long,
|
||||||
|
) {
|
||||||
|
|
||||||
private val cacheDir = suspendLazy {
|
private val cacheDir = suspendLazy {
|
||||||
val dirs = context.externalCacheDirs + context.cacheDir
|
val dirs = context.externalCacheDirs + context.cacheDir
|
||||||
dirs.firstNotNullOf {
|
dirs.firstNotNullOf {
|
||||||
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
|
it?.subdir(dir.dir)?.takeIfWriteable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val lruCache = suspendLazy {
|
private val lruCache = suspendLazy {
|
||||||
val dir = cacheDir.get()
|
val dir = cacheDir.get()
|
||||||
val availableSize = (getAvailableSize() * 0.8).toLong()
|
val availableSize = (getAvailableSize() * 0.8).toLong()
|
||||||
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
|
val size = defaultSize.coerceAtMost(availableSize).coerceAtLeast(minSize)
|
||||||
runCatchingCancellable {
|
runCatchingCancellable {
|
||||||
DiskLruCache.create(dir, size)
|
DiskLruCache.create(dir, size)
|
||||||
}.recoverCatching { error ->
|
}.recoverCatching { error ->
|
||||||
@@ -54,14 +54,14 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun get(url: String): File? = withContext(Dispatchers.IO) {
|
suspend operator fun get(url: String): File? = withContext(Dispatchers.IO) {
|
||||||
val cache = lruCache.get()
|
val cache = lruCache.get()
|
||||||
runInterruptible {
|
runInterruptible {
|
||||||
cache.get(url)?.takeIfReadable()
|
cache.get(url)?.takeIfReadable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun put(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
|
suspend operator fun set(url: String, source: Source, mimeType: MimeType?): File = withContext(Dispatchers.IO) {
|
||||||
val file = createBufferFile(url, mimeType)
|
val file = createBufferFile(url, mimeType)
|
||||||
try {
|
try {
|
||||||
val bytes = file.sink(append = false).buffer().use {
|
val bytes = file.sink(append = false).buffer().use {
|
||||||
@@ -79,7 +79,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
suspend operator fun set(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||||
val file = createBufferFile(url, MimeType("image/png"))
|
val file = createBufferFile(url, MimeType("image/png"))
|
||||||
try {
|
try {
|
||||||
bitmap.compressToPNG(file)
|
bitmap.compressToPNG(file)
|
||||||
@@ -107,7 +107,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
}.getOrDefault(SIZE_DEFAULT)
|
}.getOrDefault(defaultSize)
|
||||||
|
|
||||||
private suspend fun createBufferFile(url: String, mimeType: MimeType?): File {
|
private suspend fun createBufferFile(url: String, mimeType: MimeType?): File {
|
||||||
val ext = MimeTypes.getExtension(mimeType) ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
|
val ext = MimeTypes.getExtension(mimeType) ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" }
|
||||||
@@ -116,13 +116,4 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val name = UUID.randomUUID().toString() + "." + ext
|
val name = UUID.randomUUID().toString() + "." + ext
|
||||||
return File(rootDir, name)
|
return File(rootDir, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
val SIZE_MIN
|
|
||||||
get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES)
|
|
||||||
|
|
||||||
val SIZE_DEFAULT
|
|
||||||
get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,8 @@ import org.koitharu.kotatsu.core.util.ext.use
|
|||||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
|
import org.koitharu.kotatsu.download.ui.worker.DownloadSlowdownDispatcher
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.LocalStorageCache
|
||||||
|
import org.koitharu.kotatsu.local.data.PageCache
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||||
@@ -84,7 +85,7 @@ class PageLoader @Inject constructor(
|
|||||||
@LocalizedAppContext private val context: Context,
|
@LocalizedAppContext private val context: Context,
|
||||||
lifecycle: ActivityRetainedLifecycle,
|
lifecycle: ActivityRetainedLifecycle,
|
||||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||||
private val cache: PagesCache,
|
@PageCache private val cache: LocalStorageCache,
|
||||||
private val coil: ImageLoader,
|
private val coil: ImageLoader,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
@@ -196,7 +197,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.use { image ->
|
}.use { image ->
|
||||||
cache.put(uri.toString(), image).toUri()
|
cache.set(uri.toString(), image).toUri()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val file = uri.toFile()
|
val file = uri.toFile()
|
||||||
@@ -300,7 +301,7 @@ class PageLoader @Inject constructor(
|
|||||||
val request = createPageRequest(pageUrl, page.source)
|
val request = createPageRequest(pageUrl, page.source)
|
||||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||||
response.requireBody().withProgress(progress).use {
|
response.requireBody().withProgress(progress).use {
|
||||||
cache.put(pageUrl, it.source(), it.contentType()?.toMimeType())
|
cache.set(pageUrl, it.source(), it.contentType()?.toMimeType())
|
||||||
}
|
}
|
||||||
}.toUri()
|
}.toUri()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class StorageManageSettingsFragment : BasePreferenceFragment(R.string.storage_us
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||||
viewModel.clearCache(preference.key, CacheDir.THUMBS)
|
viewModel.clearCache(preference.key, CacheDir.THUMBS, CacheDir.FAVICONS)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,16 +82,18 @@ class StorageManageSettingsViewModel @Inject constructor(
|
|||||||
loadStorageUsage()
|
loadStorageUsage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache(key: String, cache: CacheDir) {
|
fun clearCache(key: String, vararg caches: CacheDir) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
loadingKeys.update { it + key }
|
loadingKeys.update { it + key }
|
||||||
storageManager.clearCache(cache)
|
for (cache in caches) {
|
||||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
storageManager.clearCache(cache)
|
||||||
loadStorageUsage()
|
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||||
if (cache == CacheDir.THUMBS || cache == CacheDir.FAVICONS) {
|
if (cache == CacheDir.THUMBS) {
|
||||||
coil.memoryCache?.clear()
|
coil.memoryCache?.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
loadStorageUsage()
|
||||||
} finally {
|
} finally {
|
||||||
loadingKeys.update { it - key }
|
loadingKeys.update { it - key }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user