|
@@ -1,18 +1,18 @@
|
|
|
package eu.kanade.tachiyomi.data.coil
|
|
|
|
|
|
-import coil.bitmap.BitmapPool
|
|
|
+import coil.ImageLoader
|
|
|
import coil.decode.DataSource
|
|
|
-import coil.decode.Options
|
|
|
+import coil.decode.ImageSource
|
|
|
+import coil.disk.DiskCache
|
|
|
import coil.fetch.FetchResult
|
|
|
import coil.fetch.Fetcher
|
|
|
import coil.fetch.SourceResult
|
|
|
import coil.network.HttpException
|
|
|
-import coil.request.get
|
|
|
-import coil.size.Size
|
|
|
+import coil.request.Options
|
|
|
+import coil.request.Parameters
|
|
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
|
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
|
-import eu.kanade.tachiyomi.network.NetworkHelper
|
|
|
import eu.kanade.tachiyomi.network.await
|
|
|
import eu.kanade.tachiyomi.source.SourceManager
|
|
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
@@ -20,130 +20,181 @@ import okhttp3.CacheControl
|
|
|
import okhttp3.Call
|
|
|
import okhttp3.Request
|
|
|
import okhttp3.Response
|
|
|
-import okhttp3.ResponseBody
|
|
|
-import okio.buffer
|
|
|
-import okio.sink
|
|
|
-import okio.source
|
|
|
-import uy.kohesive.injekt.Injekt
|
|
|
-import uy.kohesive.injekt.api.get
|
|
|
+import okhttp3.internal.closeQuietly
|
|
|
+import okio.Path.Companion.toOkioPath
|
|
|
import uy.kohesive.injekt.injectLazy
|
|
|
import java.io.File
|
|
|
+import java.net.HttpURLConnection
|
|
|
|
|
|
/**
|
|
|
- * Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
|
|
+ * A [Fetcher] that fetches cover image for [Manga] object.
|
|
|
+ *
|
|
|
+ * It uses [Manga.thumbnail_url] if custom cover is not set by the user.
|
|
|
+ * Disk caching for library items is handled by [CoverCache], otherwise
|
|
|
+ * handled by Coil's [DiskCache].
|
|
|
*
|
|
|
* Available request parameter:
|
|
|
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
|
|
*/
|
|
|
-class MangaCoverFetcher : Fetcher<Manga> {
|
|
|
- private val coverCache: CoverCache by injectLazy()
|
|
|
- private val sourceManager: SourceManager by injectLazy()
|
|
|
- private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
|
|
-
|
|
|
- override fun key(data: Manga): String? {
|
|
|
- if (data.thumbnail_url.isNullOrBlank()) return null
|
|
|
- return data.thumbnail_url!!
|
|
|
- }
|
|
|
-
|
|
|
- override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
|
|
+class MangaCoverFetcher(
|
|
|
+ private val manga: Manga,
|
|
|
+ private val sourceLazy: Lazy<HttpSource?>,
|
|
|
+ private val options: Options,
|
|
|
+ private val coverCache: CoverCache,
|
|
|
+ private val callFactoryLazy: Lazy<Call.Factory>,
|
|
|
+ private val diskCacheLazy: Lazy<DiskCache>
|
|
|
+) : Fetcher {
|
|
|
+
|
|
|
+ // For non-custom cover
|
|
|
+ private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
|
|
|
+ private lateinit var url: String
|
|
|
+
|
|
|
+ override suspend fun fetch(): FetchResult {
|
|
|
// Use custom cover if exists
|
|
|
- val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
|
|
- val customCoverFile = coverCache.getCustomCoverFile(data)
|
|
|
+ val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
|
|
|
+ val customCoverFile = coverCache.getCustomCoverFile(manga)
|
|
|
if (useCustomCover && customCoverFile.exists()) {
|
|
|
return fileLoader(customCoverFile)
|
|
|
}
|
|
|
|
|
|
- val cover = data.thumbnail_url
|
|
|
- return when (getResourceType(cover)) {
|
|
|
- Type.URL -> httpLoader(data, options)
|
|
|
- Type.File -> fileLoader(data)
|
|
|
+ // diskCacheKey is thumbnail_url
|
|
|
+ url = diskCacheKey ?: error("No cover specified")
|
|
|
+ return when (getResourceType(url)) {
|
|
|
+ Type.URL -> httpLoader()
|
|
|
+ Type.File -> fileLoader(File(url.substringAfter("file://")))
|
|
|
null -> error("Invalid image")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
|
|
+ private fun fileLoader(file: File): FetchResult {
|
|
|
+ return SourceResult(
|
|
|
+ source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
|
|
|
+ mimeType = "image/*",
|
|
|
+ dataSource = DataSource.DISK
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private suspend fun httpLoader(): FetchResult {
|
|
|
// Only cache separately if it's a library item
|
|
|
val coverCacheFile = if (manga.favorite) {
|
|
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
|
|
} else {
|
|
|
null
|
|
|
}
|
|
|
-
|
|
|
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
|
|
return fileLoader(coverCacheFile)
|
|
|
}
|
|
|
|
|
|
- val (response, body) = awaitGetCall(manga, options)
|
|
|
- if (!response.isSuccessful) {
|
|
|
- body.close()
|
|
|
- throw HttpException(response)
|
|
|
- }
|
|
|
+ var snapshot = readFromDiskCache()
|
|
|
+ try {
|
|
|
+ // Fetch from disk cache
|
|
|
+ if (snapshot != null) {
|
|
|
+ return SourceResult(
|
|
|
+ source = snapshot.toImageSource(),
|
|
|
+ mimeType = "image/*",
|
|
|
+ dataSource = DataSource.DISK
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
|
|
- @Suppress("BlockingMethodInNonBlockingContext")
|
|
|
- response.peekBody(Long.MAX_VALUE).source().use { input ->
|
|
|
- coverCacheFile.parentFile?.mkdirs()
|
|
|
- if (coverCacheFile.exists()) {
|
|
|
- coverCacheFile.delete()
|
|
|
- }
|
|
|
- coverCacheFile.sink().buffer().use { output ->
|
|
|
- output.writeAll(input)
|
|
|
+ // Fetch from network
|
|
|
+ val response = executeNetworkRequest()
|
|
|
+ val responseBody = checkNotNull(response.body) { "Null response source" }
|
|
|
+ try {
|
|
|
+ snapshot = writeToDiskCache(snapshot, response)
|
|
|
+ // Read from disk cache
|
|
|
+ if (snapshot != null) {
|
|
|
+ return SourceResult(
|
|
|
+ source = snapshot.toImageSource(),
|
|
|
+ mimeType = "image/*",
|
|
|
+ dataSource = DataSource.NETWORK
|
|
|
+ )
|
|
|
}
|
|
|
+
|
|
|
+ // Read from response if cache is unused or unusable
|
|
|
+ return SourceResult(
|
|
|
+ source = ImageSource(source = responseBody.source(), context = options.context),
|
|
|
+ mimeType = "image/*",
|
|
|
+ dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
|
+ )
|
|
|
+ } catch (e: Exception) {
|
|
|
+ responseBody.closeQuietly()
|
|
|
+ throw e
|
|
|
+ } finally {
|
|
|
+ response.close()
|
|
|
}
|
|
|
+ } catch (e: Exception) {
|
|
|
+ snapshot?.closeQuietly()
|
|
|
+ throw e
|
|
|
}
|
|
|
-
|
|
|
- return SourceResult(
|
|
|
- source = body.source(),
|
|
|
- mimeType = "image/*",
|
|
|
- dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
|
- )
|
|
|
}
|
|
|
|
|
|
- private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
|
|
- val call = getCall(manga, options)
|
|
|
- val response = call.await()
|
|
|
- return response to checkNotNull(response.body) { "Null response source" }
|
|
|
+ private suspend fun executeNetworkRequest(): Response {
|
|
|
+ val client = sourceLazy.value?.client ?: callFactoryLazy.value
|
|
|
+ val response = client.newCall(newRequest()).await()
|
|
|
+ if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
|
|
+ response.body?.closeQuietly()
|
|
|
+ throw HttpException(response)
|
|
|
+ }
|
|
|
+ return response
|
|
|
}
|
|
|
|
|
|
- private fun getCall(manga: Manga, options: Options): Call {
|
|
|
- val source = sourceManager.get(manga.source) as? HttpSource
|
|
|
- val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
|
- if (source != null) {
|
|
|
- it.headers(source.headers)
|
|
|
+ private fun newRequest(): Request {
|
|
|
+ val request = Request.Builder()
|
|
|
+ .url(url)
|
|
|
+ .headers(options.headers)
|
|
|
+ // Support attaching custom data to the network request.
|
|
|
+ .tag(Parameters::class.java, options.parameters)
|
|
|
+
|
|
|
+ val diskRead = options.diskCachePolicy.readEnabled
|
|
|
+ val networkRead = options.networkCachePolicy.readEnabled
|
|
|
+ when {
|
|
|
+ !networkRead && diskRead -> {
|
|
|
+ request.cacheControl(CacheControl.FORCE_CACHE)
|
|
|
}
|
|
|
-
|
|
|
- val networkRead = options.networkCachePolicy.readEnabled
|
|
|
- val diskRead = options.diskCachePolicy.readEnabled
|
|
|
- when {
|
|
|
- !networkRead && diskRead -> {
|
|
|
- it.cacheControl(CacheControl.FORCE_CACHE)
|
|
|
- }
|
|
|
- networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
|
- it.cacheControl(CacheControl.FORCE_NETWORK)
|
|
|
- } else {
|
|
|
- it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
|
- }
|
|
|
- !networkRead && !diskRead -> {
|
|
|
- // This causes the request to fail with a 504 Unsatisfiable Request.
|
|
|
- it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
|
- }
|
|
|
+ networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
|
+ request.cacheControl(CacheControl.FORCE_NETWORK)
|
|
|
+ } else {
|
|
|
+ request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
|
}
|
|
|
- }.build()
|
|
|
+ !networkRead && !diskRead -> {
|
|
|
+ // This causes the request to fail with a 504 Unsatisfiable Request.
|
|
|
+ request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
|
|
- return client.newCall(request)
|
|
|
+ return request.build()
|
|
|
}
|
|
|
|
|
|
- private fun fileLoader(manga: Manga): FetchResult {
|
|
|
- return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
|
|
+ private fun readFromDiskCache(): DiskCache.Snapshot? {
|
|
|
+ return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
|
|
|
}
|
|
|
|
|
|
- private fun fileLoader(file: File): FetchResult {
|
|
|
- return SourceResult(
|
|
|
- source = file.source().buffer(),
|
|
|
- mimeType = "image/*",
|
|
|
- dataSource = DataSource.DISK
|
|
|
- )
|
|
|
+ private fun writeToDiskCache(snapshot: DiskCache.Snapshot?, response: Response): DiskCache.Snapshot? {
|
|
|
+ if (!options.diskCachePolicy.writeEnabled) {
|
|
|
+ snapshot?.closeQuietly()
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ val editor = if (snapshot != null) {
|
|
|
+ snapshot.closeAndEdit()
|
|
|
+ } else {
|
|
|
+ diskCacheLazy.value.edit(diskCacheKey!!)
|
|
|
+ } ?: return null
|
|
|
+ try {
|
|
|
+ diskCacheLazy.value.fileSystem.write(editor.data) {
|
|
|
+ response.body!!.source().readAll(this)
|
|
|
+ }
|
|
|
+ return editor.commitAndGet()
|
|
|
+ } catch (e: Exception) {
|
|
|
+ try {
|
|
|
+ editor.abort()
|
|
|
+ } catch (ignored: Exception) {
|
|
|
+ }
|
|
|
+ throw e
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
|
|
+ return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
|
|
|
}
|
|
|
|
|
|
private fun getResourceType(cover: String?): Type? {
|
|
@@ -159,6 +210,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|
|
File, URL
|
|
|
}
|
|
|
|
|
|
+ class Factory(
|
|
|
+ private val callFactoryLazy: Lazy<Call.Factory>,
|
|
|
+ private val diskCacheLazy: Lazy<DiskCache>
|
|
|
+ ) : Fetcher.Factory<Manga> {
|
|
|
+
|
|
|
+ private val coverCache: CoverCache by injectLazy()
|
|
|
+ private val sourceManager: SourceManager by injectLazy()
|
|
|
+
|
|
|
+ override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
|
|
|
+ val source = lazy { sourceManager.get(data.source) as? HttpSource }
|
|
|
+ return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
companion object {
|
|
|
const val USE_CUSTOM_COVER = "use_custom_cover"
|
|
|
|