MangaCoverFetcher.kt 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package eu.kanade.tachiyomi.data.coil
  2. import coil.bitmap.BitmapPool
  3. import coil.decode.DataSource
  4. import coil.decode.Options
  5. import coil.fetch.FetchResult
  6. import coil.fetch.Fetcher
  7. import coil.fetch.SourceResult
  8. import coil.network.HttpException
  9. import coil.request.get
  10. import coil.size.Size
  11. import eu.kanade.tachiyomi.data.cache.CoverCache
  12. import eu.kanade.tachiyomi.data.database.models.Manga
  13. import eu.kanade.tachiyomi.network.NetworkHelper
  14. import eu.kanade.tachiyomi.network.await
  15. import eu.kanade.tachiyomi.source.SourceManager
  16. import eu.kanade.tachiyomi.source.online.HttpSource
  17. import okhttp3.CacheControl
  18. import okhttp3.Call
  19. import okhttp3.Request
  20. import okhttp3.Response
  21. import okhttp3.ResponseBody
  22. import okio.buffer
  23. import okio.sink
  24. import okio.source
  25. import uy.kohesive.injekt.Injekt
  26. import uy.kohesive.injekt.api.get
  27. import uy.kohesive.injekt.injectLazy
  28. import java.io.File
  29. /**
  30. * Coil component that fetches [Manga] cover while using the cached file in disk when available.
  31. *
  32. * Available request parameter:
  33. * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
  34. */
  35. class MangaCoverFetcher : Fetcher<Manga> {
  36. private val coverCache: CoverCache by injectLazy()
  37. private val sourceManager: SourceManager by injectLazy()
  38. private val defaultClient = Injekt.get<NetworkHelper>().coilClient
  39. override fun key(data: Manga): String? {
  40. if (data.thumbnail_url.isNullOrBlank()) return null
  41. return data.thumbnail_url!!
  42. }
  43. override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
  44. // Use custom cover if exists
  45. val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
  46. val customCoverFile = coverCache.getCustomCoverFile(data)
  47. if (useCustomCover && customCoverFile.exists()) {
  48. return fileLoader(customCoverFile)
  49. }
  50. val cover = data.thumbnail_url
  51. return when (getResourceType(cover)) {
  52. Type.URL -> httpLoader(data, options)
  53. Type.File -> fileLoader(data)
  54. null -> error("Invalid image")
  55. }
  56. }
  57. private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
  58. // Only cache separately if it's a library item
  59. val coverCacheFile = if (manga.favorite) {
  60. coverCache.getCoverFile(manga) ?: error("No cover specified")
  61. } else {
  62. null
  63. }
  64. if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
  65. return fileLoader(coverCacheFile)
  66. }
  67. val (response, body) = awaitGetCall(manga, options)
  68. if (!response.isSuccessful) {
  69. body.close()
  70. throw HttpException(response)
  71. }
  72. if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
  73. @Suppress("BlockingMethodInNonBlockingContext")
  74. response.peekBody(Long.MAX_VALUE).source().use { input ->
  75. coverCacheFile.parentFile?.mkdirs()
  76. if (coverCacheFile.exists()) {
  77. coverCacheFile.delete()
  78. }
  79. coverCacheFile.sink().buffer().use { output ->
  80. output.writeAll(input)
  81. }
  82. }
  83. }
  84. return SourceResult(
  85. source = body.source(),
  86. mimeType = "image/*",
  87. dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
  88. )
  89. }
  90. private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
  91. val call = getCall(manga, options)
  92. val response = call.await()
  93. return response to checkNotNull(response.body) { "Null response source" }
  94. }
  95. private fun getCall(manga: Manga, options: Options): Call {
  96. val source = sourceManager.get(manga.source) as? HttpSource
  97. val request = Request.Builder().url(manga.thumbnail_url!!).also {
  98. if (source != null) {
  99. it.headers(source.headers)
  100. }
  101. val networkRead = options.networkCachePolicy.readEnabled
  102. val diskRead = options.diskCachePolicy.readEnabled
  103. when {
  104. !networkRead && diskRead -> {
  105. it.cacheControl(CacheControl.FORCE_CACHE)
  106. }
  107. networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
  108. it.cacheControl(CacheControl.FORCE_NETWORK)
  109. } else {
  110. it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
  111. }
  112. !networkRead && !diskRead -> {
  113. // This causes the request to fail with a 504 Unsatisfiable Request.
  114. it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
  115. }
  116. }
  117. }.build()
  118. val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
  119. return client.newCall(request)
  120. }
  121. private fun fileLoader(manga: Manga): FetchResult {
  122. return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
  123. }
  124. private fun fileLoader(file: File): FetchResult {
  125. return SourceResult(
  126. source = file.source().buffer(),
  127. mimeType = "image/*",
  128. dataSource = DataSource.DISK
  129. )
  130. }
  131. private fun getResourceType(cover: String?): Type? {
  132. return when {
  133. cover.isNullOrEmpty() -> null
  134. cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
  135. cover.startsWith("/") || cover.startsWith("file://") -> Type.File
  136. else -> null
  137. }
  138. }
  139. private enum class Type {
  140. File, URL
  141. }
  142. companion object {
  143. const val USE_CUSTOM_COVER = "use_custom_cover"
  144. private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
  145. private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
  146. }
  147. }