Support Referer header for image requests

This commit is contained in:
Koitharu
2021-01-07 14:48:35 +02:00
parent 22e7bab879
commit 1a0986212b
22 changed files with 55 additions and 19 deletions

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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,

View File

@@ -112,6 +112,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
MangaPage(
id = url.longHashCode(),
url = url,
referer = chapter.url,
source = source
)
}

View File

@@ -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")
)

View File

@@ -154,6 +154,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
MangaPage(
id = url.longHashCode(),
url = url,
referer = chapter.url,
source = source
)
}

View File

@@ -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
)
}
}

View File

@@ -134,6 +134,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
MangaPage(
id = href.longHashCode(),
url = href,
referer = chapter.url,
source = MangaSource.MANGATOWN
)
}

View File

@@ -146,6 +146,7 @@ class MangareadRepository(
MangaPage(
id = url.longHashCode(),
url = url,
referer = chapter.url,
source = MangaSource.MANGAREAD
)
}

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -71,6 +71,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
MangaPage(
id = entryUri.longHashCode(),
url = entryUri,
referer = chapter.url,
source = MangaSource.LOCAL
)
}

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()
}