|  | @@ -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"
 | 
	
		
			
				|  |  |  
 |