|
@@ -0,0 +1,172 @@
|
|
|
+package eu.kanade.tachiyomi.data.coil
|
|
|
+
|
|
|
+import coil.bitmap.BitmapPool
|
|
|
+import coil.decode.DataSource
|
|
|
+import coil.decode.Options
|
|
|
+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 eu.kanade.tachiyomi.data.cache.CoverCache
|
|
|
+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
|
|
|
+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 uy.kohesive.injekt.injectLazy
|
|
|
+import java.io.File
|
|
|
+import java.util.Date
|
|
|
+
|
|
|
+/**
|
|
|
+ * Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
|
|
+ *
|
|
|
+ * 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 {
|
|
|
+ // Use custom cover if exists
|
|
|
+ val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
|
|
+ val customCoverFile = coverCache.getCustomCoverFile(data)
|
|
|
+ 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)
|
|
|
+ null -> error("Invalid image")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
|
|
+ val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
|
|
+
|
|
|
+ // Use previously cached cover if exist
|
|
|
+ if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
|
|
+ if (!manga.favorite) {
|
|
|
+ coverFile.setLastModified(Date().time)
|
|
|
+ }
|
|
|
+ return fileLoader(coverFile)
|
|
|
+ }
|
|
|
+
|
|
|
+ val (response, body) = awaitGetCall(manga, options)
|
|
|
+ if (!response.isSuccessful) {
|
|
|
+ body.close()
|
|
|
+ throw HttpException(response)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Write to disk for future use
|
|
|
+ if (options.diskCachePolicy.writeEnabled) {
|
|
|
+ response.peekBody(Long.MAX_VALUE).source().use { input ->
|
|
|
+ val tmpFile = File(coverFile.absolutePath + "_tmp")
|
|
|
+ tmpFile.parentFile?.mkdirs()
|
|
|
+ tmpFile.sink().buffer().use { output ->
|
|
|
+ output.writeAll(input)
|
|
|
+ }
|
|
|
+ if (coverFile.exists()) {
|
|
|
+ coverFile.delete()
|
|
|
+ }
|
|
|
+ tmpFile.renameTo(coverFile)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 fun getCall(manga: Manga, options: Options): Call {
|
|
|
+ val source = sourceManager.get(manga.source) as? HttpSource
|
|
|
+ val client = source?.client ?: defaultClient
|
|
|
+
|
|
|
+ val newClient = client.newBuilder().build()
|
|
|
+
|
|
|
+ val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
|
+ if (source != null) {
|
|
|
+ it.headers(source.headers)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }.build()
|
|
|
+
|
|
|
+ return newClient.newCall(request)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun fileLoader(manga: Manga): FetchResult {
|
|
|
+ return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun fileLoader(file: File): FetchResult {
|
|
|
+ return SourceResult(
|
|
|
+ source = file.source().buffer(),
|
|
|
+ mimeType = "image/*",
|
|
|
+ dataSource = DataSource.DISK
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getResourceType(cover: String?): Type? {
|
|
|
+ return when {
|
|
|
+ cover.isNullOrEmpty() -> null
|
|
|
+ cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
|
|
+ cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
|
|
+ else -> null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private enum class Type {
|
|
|
+ File, URL
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ const val USE_CUSTOM_COVER = "use_custom_cover"
|
|
|
+
|
|
|
+ private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
|
|
+ private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
|
|
+ }
|
|
|
+}
|