Support Referer header for image requests
This commit is contained in:
@@ -7,6 +7,7 @@ import kotlinx.parcelize.Parcelize
|
||||
data class MangaPage(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val referer: String,
|
||||
val preview: String? = null,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||
@@ -12,6 +13,7 @@ class CloudFlareInterceptor : Interceptor {
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
||||
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
|
||||
response.closeQuietly()
|
||||
throw CloudFlareProtectedException(chain.request().url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ class UserAgentInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain) = chain.proceed(
|
||||
chain.request().newBuilder()
|
||||
.header("User-Agent", userAgent)
|
||||
.header(HEADER_USER_AGENT, userAgent)
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
private const val HEADER_USER_AGENT = "User-Agent"
|
||||
|
||||
val userAgent
|
||||
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
|
||||
BuildConfig.VERSION_NAME,
|
||||
|
||||
@@ -112,6 +112,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
|
||||
MangaPage(
|
||||
id = url.longHashCode(),
|
||||
url = url,
|
||||
referer = chapter.url,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
|
||||
return json.getJSONObject("pages").getJSONArray("list").map {
|
||||
MangaPage(
|
||||
id = it.getLong("id"),
|
||||
referer = chapter.url,
|
||||
source = chapter.source,
|
||||
url = it.getString("img")
|
||||
)
|
||||
|
||||
@@ -154,6 +154,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
|
||||
MangaPage(
|
||||
id = url.longHashCode(),
|
||||
url = url,
|
||||
referer = chapter.url,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,8 +168,9 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
|
||||
val pageUrl = "$domain$url${x.getString("u")}"
|
||||
MangaPage(
|
||||
id = pageUrl.longHashCode(),
|
||||
source = source,
|
||||
url = pageUrl
|
||||
url = pageUrl,
|
||||
referer = chapter.url,
|
||||
source = source
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
|
||||
MangaPage(
|
||||
id = href.longHashCode(),
|
||||
url = href,
|
||||
referer = chapter.url,
|
||||
source = MangaSource.MANGATOWN
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ class MangareadRepository(
|
||||
MangaPage(
|
||||
id = url.longHashCode(),
|
||||
url = url,
|
||||
referer = chapter.url,
|
||||
source = MangaSource.MANGAREAD
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ class NudeMoonRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
|
||||
MangaPage(
|
||||
id = url.longHashCode(),
|
||||
url = url,
|
||||
referer = chapter.url,
|
||||
preview = a.selectFirst("img")?.absUrl("src"),
|
||||
source = source
|
||||
)
|
||||
|
||||
@@ -51,6 +51,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
with(binding) {
|
||||
imageViewCover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
|
||||
.referer(manga.url)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.placeholderMemoryCacheKey(CoilUtils.metadata(imageViewCover)?.memoryCacheKey)
|
||||
.lifecycle(viewLifecycleOwner)
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
|
||||
fun mangaGridItemAD(
|
||||
coil: ImageLoader,
|
||||
@@ -35,6 +36,7 @@ fun mangaGridItemAD(
|
||||
binding.textViewTitle.text = item.title
|
||||
imageRequest?.dispose()
|
||||
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
|
||||
.referer(item.manga.url)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
|
||||
fun mangaListDetailedItemAD(
|
||||
@@ -37,6 +38,7 @@ fun mangaListDetailedItemAD(
|
||||
binding.textViewTitle.text = item.title
|
||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
|
||||
.referer(item.manga.url)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
|
||||
fun mangaListItemAD(
|
||||
@@ -37,6 +38,7 @@ fun mangaListItemAD(
|
||||
binding.textViewTitle.text = item.title
|
||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||
imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl)
|
||||
.referer(item.manga.url)
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.sub
|
||||
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class PagesCache(context: Context) {
|
||||
@@ -19,6 +20,7 @@ class PagesCache(context: Context) {
|
||||
return lruCache.get(url)?.takeIfReadable()
|
||||
}
|
||||
|
||||
@Deprecated("Useless lambda")
|
||||
fun put(url: String, writer: (OutputStream) -> Unit): File {
|
||||
val file = cacheDir.sub(url.longHashCode().toString())
|
||||
file.outputStream().use(writer)
|
||||
@@ -26,4 +28,14 @@ class PagesCache(context: Context) {
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
|
||||
fun put(url: String, inputStream: InputStream): File {
|
||||
val file = cacheDir.sub(url.longHashCode().toString())
|
||||
file.outputStream().use { out ->
|
||||
inputStream.copyTo(out)
|
||||
}
|
||||
val res = lruCache.put(url, file)
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
|
||||
MangaPage(
|
||||
id = entryUri.longHashCode(),
|
||||
url = entryUri,
|
||||
referer = chapter.url,
|
||||
source = MangaSource.LOCAL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,43 +24,41 @@ class PageLoader(
|
||||
private val tasks = ArrayMap<String, Deferred<File>>()
|
||||
private val convertLock = Mutex()
|
||||
|
||||
suspend fun loadFile(url: String, force: Boolean): File {
|
||||
suspend fun loadFile(url: String, referer: String, force: Boolean): File {
|
||||
if (!force) {
|
||||
cache[url]?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
val task = tasks[url]?.takeUnless { it.isCancelled || (force && it.isCompleted) }
|
||||
return (task ?: loadAsync(url).also { tasks[url] = it }).await()
|
||||
return (task ?: loadAsync(url, referer).also { tasks[url] = it }).await()
|
||||
}
|
||||
|
||||
private fun loadAsync(url: String) = async(Dispatchers.IO) {
|
||||
private fun loadAsync(url: String, referer: String) = async(Dispatchers.IO) {
|
||||
val uri = Uri.parse(url)
|
||||
if (uri.scheme == "cbz") {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
cache.put(url) { out ->
|
||||
it.copyTo(out)
|
||||
}
|
||||
cache.put(url, it)
|
||||
}
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header("Accept", "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
||||
.header("Referer", referer)
|
||||
.cacheControl(CacheUtils.CONTROL_DISABLED)
|
||||
.build()
|
||||
okHttp.newCall(request).await().use { response ->
|
||||
val body = response.body
|
||||
check(response.isSuccessful) {
|
||||
"Invalid response: ${response.code} ${response.message}"
|
||||
}
|
||||
checkNotNull(body) {
|
||||
val body = checkNotNull(response.body) {
|
||||
"Null response"
|
||||
}
|
||||
cache.put(url) { out ->
|
||||
body.byteStream().use { it.copyTo(out) }
|
||||
body.byteStream().use {
|
||||
cache.put(url, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class PageHolderDelegate(
|
||||
val file = withContext(Dispatchers.IO) {
|
||||
val pageUrl = data.source.repository.getPageFullUrl(data)
|
||||
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" }
|
||||
loader.loadFile(pageUrl, force)
|
||||
loader.loadFile(pageUrl, data.referer, force)
|
||||
}
|
||||
this@PageHolderDelegate.file = file
|
||||
state = State.LOADED
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
|
||||
data class ReaderPage(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val referer: String,
|
||||
val preview: String?,
|
||||
val chapterId: Long,
|
||||
val index: Int,
|
||||
@@ -18,6 +19,7 @@ data class ReaderPage(
|
||||
fun toMangaPage() = MangaPage(
|
||||
id = id,
|
||||
url = url,
|
||||
referer = referer,
|
||||
preview = preview,
|
||||
source = source
|
||||
)
|
||||
@@ -27,6 +29,7 @@ data class ReaderPage(
|
||||
fun from(page: MangaPage, index: Int, chapterId: Long) = ReaderPage(
|
||||
id = page.id,
|
||||
url = page.url,
|
||||
referer = page.referer,
|
||||
preview = page.preview,
|
||||
chapterId = chapterId,
|
||||
index = index,
|
||||
|
||||
@@ -28,4 +28,7 @@ fun ImageResult.toBitmapOrNull() = when (this) {
|
||||
null
|
||||
}
|
||||
is ErrorResult -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun ImageRequest.Builder.referer(referer: String) = this.setHeader("Referer", referer)
|
||||
@@ -24,7 +24,7 @@ class RemoteRepositoryTest(source: MangaSource) : KoinTest {
|
||||
|
||||
private val repo = try {
|
||||
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
|
||||
.newInstance(get())
|
||||
.newInstance(get<MangaLoaderContext>())
|
||||
} catch (e: NoSuchMethodException) {
|
||||
source.cls.newInstance()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user