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