Parse favicons in runtime
parent
9dc3ad38fc
commit
9c66f74a5b
@ -1,20 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import android.net.Uri
|
||||
import coil.map.Mapper
|
||||
import coil.request.Options
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class FaviconMapper : Mapper<Uri, HttpUrl> {
|
||||
|
||||
override fun map(data: Uri, options: Options): HttpUrl? {
|
||||
if (data.scheme != "favicon") {
|
||||
return null
|
||||
}
|
||||
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
|
||||
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
|
||||
return repo.getFaviconUrl().toHttpUrl()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package org.koitharu.kotatsu.core.parser.favicon
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
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.Options
|
||||
import coil.size.Size
|
||||
import coil.size.pxOrElse
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
private const val FALLBACK_SIZE = 9999 // largest icon
|
||||
|
||||
class FaviconFetcher(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val diskCache: Lazy<DiskCache?>,
|
||||
private val mangaSource: MangaSource,
|
||||
private val options: Options,
|
||||
) : Fetcher {
|
||||
|
||||
private val diskCacheKey
|
||||
get() = options.diskCacheKey ?: "${mangaSource.name}[${mangaSource.ordinal}]x${options.size.toCacheKey()}"
|
||||
|
||||
private val fileSystem
|
||||
get() = checkNotNull(diskCache.value).fileSystem
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
getCached(options)?.let { return it }
|
||||
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
|
||||
val favicons = repo.getFavicons()
|
||||
val sizePx = maxOf(
|
||||
options.size.width.pxOrElse { FALLBACK_SIZE },
|
||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||
)
|
||||
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
|
||||
val response = loadIcon(icon.url, favicons.referer)
|
||||
val responseBody = response.requireBody()
|
||||
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource()
|
||||
return SourceResult(
|
||||
source = source,
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
|
||||
dataSource = response.toDataSource(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun loadIcon(url: String, referer: String): Response {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header(CommonHeaders.REFERER, referer)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
|
||||
val response = okHttpClient.newCall(request.build()).await()
|
||||
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
response.body?.closeQuietly()
|
||||
throw HttpException(response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private fun getCached(options: Options): SourceResult? {
|
||||
if (!options.diskCachePolicy.readEnabled) {
|
||||
return null
|
||||
}
|
||||
val snapshot = diskCache.value?.get(diskCacheKey) ?: return null
|
||||
return SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
|
||||
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
|
||||
return null
|
||||
}
|
||||
val editor = diskCache.value?.edit(diskCacheKey) ?: return null
|
||||
try {
|
||||
fileSystem.write(editor.data) {
|
||||
body.source().readAll(this)
|
||||
}
|
||||
return editor.commitAndGet()
|
||||
} catch (e: Throwable) {
|
||||
try {
|
||||
editor.abort()
|
||||
} catch (abortingError: Throwable) {
|
||||
e.addSuppressed(abortingError)
|
||||
}
|
||||
body.closeQuietly()
|
||||
throw e
|
||||
} finally {
|
||||
body.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(data, fileSystem, diskCacheKey, this)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toImageSource(): ImageSource {
|
||||
return ImageSource(source(), options.context, FaviconMetadata(mangaSource))
|
||||
}
|
||||
|
||||
private fun Response.toDataSource(): DataSource {
|
||||
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
||||
}
|
||||
|
||||
private fun Response.requireBody(): ResponseBody {
|
||||
return checkNotNull(body) { "response body == null" }
|
||||
}
|
||||
|
||||
private fun Size.toCacheKey() = buildString {
|
||||
append(width.toString())
|
||||
append('x')
|
||||
append(height.toString())
|
||||
}
|
||||
|
||||
class Factory(
|
||||
context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) : Fetcher.Factory<Uri> {
|
||||
|
||||
private val diskCache = lazy {
|
||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||
DiskCache.Builder()
|
||||
.directory(rootDir.resolve(CacheDir.FAVICONS.dir))
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||
return if (data.scheme == URI_SCHEME_FAVICON) {
|
||||
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
|
||||
FaviconFetcher(okHttpClient, diskCache, mangaSource, options)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FaviconMetadata(val source: MangaSource) : ImageSource.Metadata()
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.parser.favicon
|
||||
|
||||
import android.net.Uri
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
const val URI_SCHEME_FAVICON = "favicon"
|
||||
|
||||
fun MangaSource.faviconUri(): Uri = Uri.fromParts(URI_SCHEME_FAVICON, name, null)
|
||||
Loading…
Reference in New Issue