|
|
|
@ -1,6 +1,5 @@
|
|
|
|
package org.koitharu.kotatsu.core.parser.favicon
|
|
|
|
package org.koitharu.kotatsu.core.parser.favicon
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
|
|
|
import android.graphics.Color
|
|
|
|
import android.graphics.Color
|
|
|
|
import android.graphics.drawable.AdaptiveIconDrawable
|
|
|
|
import android.graphics.drawable.AdaptiveIconDrawable
|
|
|
|
import android.graphics.drawable.ColorDrawable
|
|
|
|
import android.graphics.drawable.ColorDrawable
|
|
|
|
@ -8,67 +7,43 @@ import android.graphics.drawable.Drawable
|
|
|
|
import android.graphics.drawable.LayerDrawable
|
|
|
|
import android.graphics.drawable.LayerDrawable
|
|
|
|
import android.net.Uri
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Build
|
|
|
|
import android.webkit.MimeTypeMap
|
|
|
|
import coil3.ImageLoader
|
|
|
|
import coil.ImageLoader
|
|
|
|
import coil3.asImage
|
|
|
|
import coil.decode.DataSource
|
|
|
|
import coil3.decode.DataSource
|
|
|
|
import coil.decode.ImageSource
|
|
|
|
import coil3.fetch.FetchResult
|
|
|
|
import coil.disk.DiskCache
|
|
|
|
import coil3.fetch.Fetcher
|
|
|
|
import coil.fetch.DrawableResult
|
|
|
|
import coil3.fetch.ImageFetchResult
|
|
|
|
import coil.fetch.FetchResult
|
|
|
|
import coil3.request.Options
|
|
|
|
import coil.fetch.Fetcher
|
|
|
|
import coil3.size.pxOrElse
|
|
|
|
import coil.fetch.SourceResult
|
|
|
|
import coil3.toAndroidUri
|
|
|
|
import coil.network.HttpException
|
|
|
|
|
|
|
|
import coil.request.Options
|
|
|
|
|
|
|
|
import coil.size.Size
|
|
|
|
|
|
|
|
import coil.size.pxOrElse
|
|
|
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
|
|
|
import kotlinx.coroutines.ensureActive
|
|
|
|
import kotlinx.coroutines.ensureActive
|
|
|
|
import kotlinx.coroutines.runInterruptible
|
|
|
|
import kotlinx.coroutines.runInterruptible
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okio.IOException
|
|
|
|
import okhttp3.Request
|
|
|
|
|
|
|
|
import okhttp3.Response
|
|
|
|
|
|
|
|
import okhttp3.ResponseBody
|
|
|
|
|
|
|
|
import okhttp3.internal.closeQuietly
|
|
|
|
|
|
|
|
import okio.Closeable
|
|
|
|
|
|
|
|
import okio.buffer
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
|
|
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.fetch
|
|
|
|
import org.koitharu.kotatsu.local.data.CacheDir
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.await
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.requireBody
|
|
|
|
|
|
|
|
import java.net.HttpURLConnection
|
|
|
|
|
|
|
|
import kotlin.coroutines.coroutineContext
|
|
|
|
import kotlin.coroutines.coroutineContext
|
|
|
|
|
|
|
|
import coil3.Uri as CoilUri
|
|
|
|
private const val FALLBACK_SIZE = 9999 // largest icon
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FaviconFetcher(
|
|
|
|
class FaviconFetcher(
|
|
|
|
private val okHttpClient: OkHttpClient,
|
|
|
|
private val uri: Uri,
|
|
|
|
private val diskCache: Lazy<DiskCache?>,
|
|
|
|
|
|
|
|
private val mangaSource: MangaSource,
|
|
|
|
|
|
|
|
private val options: Options,
|
|
|
|
private val options: Options,
|
|
|
|
|
|
|
|
private val imageLoader: ImageLoader,
|
|
|
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
) : Fetcher {
|
|
|
|
) : Fetcher {
|
|
|
|
|
|
|
|
|
|
|
|
private val diskCacheKey
|
|
|
|
|
|
|
|
get() = options.diskCacheKey ?: "${mangaSource.name}x${options.size.toCacheKey()}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private val fileSystem
|
|
|
|
|
|
|
|
get() = checkNotNull(diskCache.value).fileSystem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun fetch(): FetchResult {
|
|
|
|
override suspend fun fetch(): FetchResult {
|
|
|
|
getCached(options)?.let { return it }
|
|
|
|
val mangaSource = MangaSource(uri.schemeSpecificPart)
|
|
|
|
|
|
|
|
|
|
|
|
return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
|
|
|
|
return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
|
|
|
|
is ParserMangaRepository -> fetchParserFavicon(repo)
|
|
|
|
is ParserMangaRepository -> fetchParserFavicon(repo)
|
|
|
|
is ExternalMangaRepository -> fetchPluginIcon(repo)
|
|
|
|
is ExternalMangaRepository -> fetchPluginIcon(repo)
|
|
|
|
is EmptyMangaRepository -> DrawableResult(
|
|
|
|
is EmptyMangaRepository -> ImageFetchResult(
|
|
|
|
drawable = ColorDrawable(Color.WHITE),
|
|
|
|
image = ColorDrawable(Color.WHITE).asImage(),
|
|
|
|
isSampled = false,
|
|
|
|
isSampled = false,
|
|
|
|
dataSource = DataSource.MEMORY,
|
|
|
|
dataSource = DataSource.MEMORY,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -77,163 +52,81 @@ class FaviconFetcher(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchParserFavicon(repo: ParserMangaRepository): FetchResult {
|
|
|
|
private suspend fun fetchParserFavicon(repository: ParserMangaRepository): FetchResult {
|
|
|
|
val sizePx = maxOf(
|
|
|
|
val sizePx = maxOf(
|
|
|
|
options.size.width.pxOrElse { FALLBACK_SIZE },
|
|
|
|
options.size.width.pxOrElse { FALLBACK_SIZE },
|
|
|
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
|
|
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
|
|
|
)
|
|
|
|
)
|
|
|
|
var favicons = repo.getFavicons()
|
|
|
|
var favicons = repository.getFavicons()
|
|
|
|
var lastError: Exception? = null
|
|
|
|
var lastError: Exception? = null
|
|
|
|
while (favicons.isNotEmpty()) {
|
|
|
|
while (favicons.isNotEmpty()) {
|
|
|
|
coroutineContext.ensureActive()
|
|
|
|
coroutineContext.ensureActive()
|
|
|
|
val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
|
|
|
|
val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
|
|
|
|
val response = try {
|
|
|
|
try {
|
|
|
|
loadIcon(icon.url, mangaSource)
|
|
|
|
val result = imageLoader.fetch(icon.url, options)
|
|
|
|
|
|
|
|
if (result != null) {
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
favicons -= icon
|
|
|
|
|
|
|
|
}
|
|
|
|
} catch (e: CloudFlareProtectedException) {
|
|
|
|
} catch (e: CloudFlareProtectedException) {
|
|
|
|
throw e
|
|
|
|
throw e
|
|
|
|
} catch (e: HttpException) {
|
|
|
|
} catch (e: IOException) {
|
|
|
|
lastError = e
|
|
|
|
lastError = e
|
|
|
|
favicons -= icon
|
|
|
|
favicons -= icon
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val responseBody = response.requireBody()
|
|
|
|
|
|
|
|
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
|
|
|
|
|
|
|
|
response.closeQuietly()
|
|
|
|
|
|
|
|
} ?: responseBody.toImageSource(response)
|
|
|
|
|
|
|
|
return SourceResult(
|
|
|
|
|
|
|
|
source = source,
|
|
|
|
|
|
|
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
|
|
|
|
|
|
|
|
dataSource = response.toDataSource(),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throwNSEE(lastError)
|
|
|
|
throwNSEE(lastError)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun loadIcon(url: String, source: MangaSource): Response {
|
|
|
|
|
|
|
|
val request = Request.Builder()
|
|
|
|
|
|
|
|
.url(url)
|
|
|
|
|
|
|
|
.get()
|
|
|
|
|
|
|
|
.tag(MangaSource::class.java, source)
|
|
|
|
|
|
|
|
request.tag(MangaSource::class.java, source)
|
|
|
|
|
|
|
|
@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.closeQuietly()
|
|
|
|
|
|
|
|
throw HttpException(response)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult {
|
|
|
|
private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult {
|
|
|
|
val source = repository.source
|
|
|
|
val source = repository.source
|
|
|
|
val pm = options.context.packageManager
|
|
|
|
val pm = options.context.packageManager
|
|
|
|
val icon = runInterruptible(Dispatchers.IO) {
|
|
|
|
val icon = runInterruptible {
|
|
|
|
val provider = pm.resolveContentProvider(source.authority, 0)
|
|
|
|
val provider = pm.resolveContentProvider(source.authority, 0)
|
|
|
|
provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName)
|
|
|
|
provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return DrawableResult(
|
|
|
|
return ImageFetchResult(
|
|
|
|
drawable = icon.nonAdaptive(),
|
|
|
|
image = icon.nonAdaptive().asImage(),
|
|
|
|
isSampled = false,
|
|
|
|
isSampled = false,
|
|
|
|
dataSource = DataSource.DISK,
|
|
|
|
dataSource = DataSource.DISK,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getCached(options: Options): SourceResult? {
|
|
|
|
class Factory(
|
|
|
|
if (!options.diskCachePolicy.readEnabled) {
|
|
|
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
return null
|
|
|
|
) : Fetcher.Factory<CoilUri> {
|
|
|
|
}
|
|
|
|
|
|
|
|
val snapshot = diskCache.value?.openSnapshot(diskCacheKey) ?: return null
|
|
|
|
override fun create(
|
|
|
|
return SourceResult(
|
|
|
|
data: CoilUri,
|
|
|
|
source = snapshot.toImageSource(),
|
|
|
|
options: Options,
|
|
|
|
mimeType = null,
|
|
|
|
imageLoader: ImageLoader
|
|
|
|
dataSource = DataSource.DISK,
|
|
|
|
): Fetcher? = if (data.scheme == URI_SCHEME_FAVICON) {
|
|
|
|
)
|
|
|
|
FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
|
|
|
|
|
|
|
|
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
fileSystem.write(editor.data) {
|
|
|
|
|
|
|
|
writeAllCancellable(body.source())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return editor.commitAndOpenSnapshot()
|
|
|
|
|
|
|
|
} 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(response: Closeable): ImageSource {
|
|
|
|
|
|
|
|
return ImageSource(
|
|
|
|
|
|
|
|
source().withExtraCloseable(response).buffer(),
|
|
|
|
|
|
|
|
options.context,
|
|
|
|
|
|
|
|
FaviconMetadata(mangaSource),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun Response.toDataSource(): DataSource {
|
|
|
|
|
|
|
|
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun Size.toCacheKey() = buildString {
|
|
|
|
|
|
|
|
append(width.toString())
|
|
|
|
|
|
|
|
append('x')
|
|
|
|
|
|
|
|
append(height.toString())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun throwNSEE(lastError: Exception?): Nothing {
|
|
|
|
|
|
|
|
if (lastError != null) {
|
|
|
|
|
|
|
|
throw lastError
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
throw NoSuchElementException("No favicons found")
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun Drawable.nonAdaptive() =
|
|
|
|
private companion object {
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this is AdaptiveIconDrawable) {
|
|
|
|
|
|
|
|
LayerDrawable(arrayOf(background, foreground))
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
this
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Factory(
|
|
|
|
const val FALLBACK_SIZE = 9999 // largest icon
|
|
|
|
context: Context,
|
|
|
|
|
|
|
|
okHttpClientLazy: Lazy<OkHttpClient>,
|
|
|
|
|
|
|
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
|
|
|
|
) : Fetcher.Factory<Uri> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private val okHttpClient by okHttpClientLazy
|
|
|
|
private fun throwNSEE(lastError: Exception?): Nothing {
|
|
|
|
private val diskCache = lazy {
|
|
|
|
if (lastError != null) {
|
|
|
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
|
|
|
throw lastError
|
|
|
|
DiskCache.Builder()
|
|
|
|
} else {
|
|
|
|
.directory(rootDir.resolve(CacheDir.FAVICONS.dir))
|
|
|
|
throw NoSuchElementException("No favicons found")
|
|
|
|
.build()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
|
|
|
private fun Drawable.nonAdaptive() =
|
|
|
|
return if (data.scheme == URI_SCHEME_FAVICON) {
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this is AdaptiveIconDrawable) {
|
|
|
|
val mangaSource = MangaSource(data.schemeSpecificPart)
|
|
|
|
LayerDrawable(arrayOf(background, foreground))
|
|
|
|
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
this
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FaviconMetadata(val source: MangaSource) : ImageSource.Metadata()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|