refactor(coil): Simplify cover fetcher code

Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
Ahmad Ansori Palembani 2024-08-18 11:30:59 +07:00
parent 7fc73ddcdc
commit 103fae06d3
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
4 changed files with 31 additions and 80 deletions

View file

@ -22,3 +22,4 @@
- More StorIO to SQLDelight migration effort - More StorIO to SQLDelight migration effort
- Target Android 15 - Target Android 15
- Adjust manga cover cache key - Adjust manga cover cache key
- Refactor manga cover fetcher (@ivaniskandar, @AntsyLich, @null2264)

View file

@ -37,7 +37,6 @@ import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.CoilDiskCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
@ -237,7 +236,6 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
override fun newImageLoader(context: PlatformContext): ImageLoader { override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(this@App).apply { return ImageLoader.Builder(this@App).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client } val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
val diskCacheLazy = lazy { CoilDiskCache.get(this@App) }
components { components {
// NetworkFetcher.Factory // NetworkFetcher.Factory
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
@ -245,13 +243,10 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
add(TachiyomiImageDecoder.Factory()) add(TachiyomiImageDecoder.Factory())
// Fetcher.Factory // Fetcher.Factory
add(BufferedSourceFetcher.Factory()) add(BufferedSourceFetcher.Factory())
add(MangaCoverFetcher.Factory(callFactoryLazy, diskCacheLazy)) add(MangaCoverFetcher.Factory(callFactoryLazy))
// Keyer // Keyer
add(MangaCoverKeyer()) add(MangaCoverKeyer())
} }
diskCache(diskCacheLazy::value)
//memoryCache { MemoryCache.Builder().maxSizePercent(this@App, 0.40).build() }
crossfade(true) crossfade(true)
allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice) allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true) allowHardware(true)

View file

@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import android.content.Context
import coil3.disk.DiskCache
import coil3.disk.directory
/**
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null
@Synchronized
fun get(context: Context): DiskCache {
return instance ?: run {
val safeCacheDir = context.cacheDir.apply { mkdirs() }
// Create the singleton disk cache instance.
DiskCache.Builder()
.directory(safeCacheDir.resolve(FOLDER_NAME))
.build()
.also { instance = it }
}
}
}

View file

@ -32,6 +32,7 @@ import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okio.FileSystem import okio.FileSystem
import okio.IOException
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Source import okio.Source
import okio.buffer import okio.buffer
@ -44,33 +45,19 @@ class MangaCoverFetcher(
private val sourceLazy: Lazy<HttpSource?>, private val sourceLazy: Lazy<HttpSource?>,
private val options: Options, private val options: Options,
private val callFactoryLazy: Lazy<Call.Factory>, private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
private val diskCacheKeyLazy: Lazy<String>, private val diskCacheKeyLazy: Lazy<String>,
private val coverFileLazy: Lazy<File?>, private val coverFileLazy: Lazy<File?>,
private val customCoverFileLazy: Lazy<File>, private val customCoverFileLazy: Lazy<File>,
private val imageLoader: ImageLoader,
) : Fetcher { ) : Fetcher {
private val diskCacheKey: String private val diskCacheKey: String
get() = diskCacheKeyLazy.value get() = diskCacheKeyLazy.value
private lateinit var url: String private lateinit var url: String
val fileScope = CoroutineScope(Job() + Dispatchers.IO) private val fileScope = CoroutineScope(Job() + Dispatchers.IO)
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
// diskCacheKey is thumbnail_url
url = manga.thumbnail_url.orEmpty()
return when (getResourceType(url)) {
Type.URL -> httpLoader()
Type.File -> {
setRatioAndColorsInScope(manga, File(url.substringAfter("file://")))
fileLoader(File(url.substringAfter("file://")))
}
Type.URI -> fileUriLoader(url)
null -> tryCustomCover() ?: error("No cover specified")
}
}
private suspend fun tryCustomCover(): FetchResult? {
if (options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)) { if (options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)) {
val customCoverFile = customCoverFileLazy.value val customCoverFile = customCoverFileLazy.value
if (customCoverFile.exists()) { if (customCoverFile.exists()) {
@ -78,26 +65,29 @@ class MangaCoverFetcher(
return fileLoader(customCoverFile) return fileLoader(customCoverFile)
} }
} }
return null
url = manga.thumbnail_url ?: error("No cover specified")
return when (getResourceType(url)) {
Type.URL -> httpLoader()
Type.File -> {
setRatioAndColorsInScope(manga, File(url.substringAfter("file://")))
fileLoader(File(url.substringAfter("file://")))
}
Type.URI -> fileUriLoader(url)
null -> error("Invalid image")
}
} }
private suspend fun httpLoader(): FetchResult { private suspend fun httpLoader(): FetchResult {
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
val onlyCache = !networkRead && diskRead
val shouldFetchRemotely = networkRead && !diskRead && !onlyCache
if (!shouldFetchRemotely) {
val customCoverLoader = tryCustomCover()
if (customCoverLoader != null) return customCoverLoader
}
val coverFile = coverFileLazy.value val coverFile = coverFileLazy.value
if (!shouldFetchRemotely && coverFile != null && coverFile.exists() && options.diskCachePolicy.readEnabled) { if (coverFile?.exists() == true && options.diskCachePolicy.readEnabled) {
if (!manga.favorite) { if (!manga.favorite) {
coverFile.setLastModified(Date().time) coverFile.setLastModified(Date().time)
} }
setRatioAndColorsInScope(manga, coverFile) setRatioAndColorsInScope(manga, coverFile)
return fileLoader(coverFile) return fileLoader(coverFile)
} }
var snapshot = readFromDiskCache() var snapshot = readFromDiskCache()
try { try {
// Fetch from disk cache // Fetch from disk cache
@ -130,7 +120,7 @@ class MangaCoverFetcher(
} }
// Read from disk cache // Read from disk cache
snapshot = writeToDiskCache(snapshot, response) snapshot = writeToDiskCache(response)
if (snapshot != null) { if (snapshot != null) {
return SourceFetchResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
@ -160,7 +150,7 @@ class MangaCoverFetcher(
val response = client.newCall(newRequest()).await() val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) { if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.close() response.close()
throw Exception(response.message) // FIXME: Should probably use something else other than generic Exception throw IOException(response.message)
} }
return response return response
} }
@ -179,15 +169,15 @@ class MangaCoverFetcher(
val onlyCache = !networkRead && diskRead val onlyCache = !networkRead && diskRead
val forceNetwork = networkRead && !diskRead val forceNetwork = networkRead && !diskRead
when { when {
!networkRead && diskRead -> { onlyCache -> {
request.cacheControl(CacheControl.FORCE_CACHE) request.cacheControl(CacheControl.FORCE_CACHE)
} }
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) { forceNetwork -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK) request.cacheControl(CacheControl.FORCE_NETWORK)
} else { } else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE) request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
} }
!networkRead && !diskRead -> { else -> {
// This causes the request to fail with a 504 Unsatisfiable Request. // This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE) request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
} }
@ -199,11 +189,11 @@ class MangaCoverFetcher(
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? { private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
if (cacheFile == null) return null if (cacheFile == null) return null
return try { return try {
diskCacheLazy.value.run { imageLoader.diskCache?.run {
fileSystem.source(snapshot.data).use { input -> fileSystem.source(snapshot.data).use { input ->
writeSourceToCoverCache(input, cacheFile) writeSourceToCoverCache(input, cacheFile)
} }
remove(diskCacheKey!!) remove(diskCacheKey)
} }
cacheFile.takeIf { it.exists() } cacheFile.takeIf { it.exists() }
} catch (e: Exception) { } catch (e: Exception) {
@ -240,27 +230,19 @@ class MangaCoverFetcher(
private fun readFromDiskCache(): DiskCache.Snapshot? { private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) { return if (options.diskCachePolicy.readEnabled) {
diskCacheLazy.value.openSnapshot(diskCacheKey!!) imageLoader.diskCache?.openSnapshot(diskCacheKey)
} else { } else {
null null
} }
} }
private fun writeToDiskCache( private fun writeToDiskCache(
snapshot: DiskCache.Snapshot?,
response: Response, response: Response,
): DiskCache.Snapshot? { ): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled) { val diskCache = imageLoader.diskCache
snapshot?.close() val editor = diskCache?.openEditor(diskCacheKey) ?: return null
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndOpenEditor()
} else {
diskCacheLazy.value.openEditor(diskCacheKey!!)
} ?: return null
try { try {
diskCacheLazy.value.fileSystem.write(editor.data) { diskCache.fileSystem.write(editor.data) {
response.body.source().readAll(this) response.body.source().readAll(this)
} }
return editor.commitAndOpenSnapshot() return editor.commitAndOpenSnapshot()
@ -339,7 +321,6 @@ class MangaCoverFetcher(
class Factory( class Factory(
private val callFactoryLazy: Lazy<Call.Factory>, private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<Manga> { ) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
@ -351,10 +332,10 @@ class MangaCoverFetcher(
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource }, sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
options = options, options = options,
callFactoryLazy = callFactoryLazy, callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy, diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
diskCacheKeyLazy = lazy { MangaCoverKeyer().key(data, options) },
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnail_url, !data.favorite) }, coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnail_url, !data.favorite) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data) }, customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data) },
imageLoader = imageLoader,
) )
} }
} }