Support custom headers for page requests

pull/26/head
Koitharu 5 years ago
parent 96d437b2a8
commit bb1dd74277

@ -5,7 +5,6 @@ import android.net.Uri
import android.util.Size import android.util.Size
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@ -26,8 +25,8 @@ object MangaUtils : KoinComponent {
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? { suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageFullUrl(page) val pageRequest = page.source.repository.getPageRequest(page)
val uri = Uri.parse(url) val uri = Uri.parse(pageRequest.url)
val size = if (uri.scheme == "cbz") { val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart) val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
@ -36,9 +35,7 @@ object MangaUtils : KoinComponent {
} }
} else { } else {
val client = get<OkHttpClient>() val client = get<OkHttpClient>()
val request = Request.Builder() val request = pageRequest.newBuilder()
.url(url)
.get()
.build() .build()
client.newCall(request).await().use { client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream()) getBitmapSize(it.body?.byteStream())

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.model
import okhttp3.Headers
import okhttp3.Request
data class RequestDraft(
val url: String,
val headers: Headers
) {
val isValid: Boolean
get() = url.isNotEmpty()
fun newBuilder(): Request.Builder = Request.Builder()
.url(url)
.get()
.headers(headers)
}

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.network
object CommonHeaders {
const val REFERER = "Referer"
const val USER_AGENT = "User-Agent"
const val ACCEPT = "Accept"
const val CONTENT_DISPOSITION = "Content-Disposition"
}

@ -11,9 +11,9 @@ class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val request = chain.request()
return chain.proceed( return chain.proceed(
if (request.header(HEADER_USER_AGENT) == null) { if (request.header(CommonHeaders.USER_AGENT) == null) {
request.newBuilder() request.newBuilder()
.header(HEADER_USER_AGENT, userAgent) .addHeader(CommonHeaders.USER_AGENT, userAgent)
.build() .build()
} else request } else request
) )
@ -21,8 +21,6 @@ class UserAgentInterceptor : Interceptor {
companion object { companion object {
private const val HEADER_USER_AGENT = "User-Agent"
val userAgent val userAgent
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format( get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME, BuildConfig.VERSION_NAME,

@ -17,7 +17,7 @@ interface MangaRepository {
suspend fun getPages(chapter: MangaChapter): List<MangaPage> suspend fun getPages(chapter: MangaChapter): List<MangaPage>
suspend fun getPageFullUrl(page: MangaPage): String suspend fun getPageRequest(page: MangaPage): RequestDraft
suspend fun getTags(): Set<MangaTag> suspend fun getTags(): Set<MangaTag>
} }

@ -1,10 +1,9 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import okhttp3.Headers
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
abstract class RemoteMangaRepository( abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext protected val loaderContext: MangaLoaderContext
@ -18,7 +17,12 @@ abstract class RemoteMangaRepository(
override val sortOrders: Set<SortOrder> get() = emptySet() override val sortOrders: Set<SortOrder> get() = emptySet()
override suspend fun getPageFullUrl(page: MangaPage): String = page.url override suspend fun getPageRequest(page: MangaPage): RequestDraft {
return RequestDraft(
url = page.url,
headers = Headers.headersOf(CommonHeaders.REFERER, page.referer)
)
}
override suspend fun getTags(): Set<MangaTag> = emptySet() override suspend fun getTags(): Set<MangaTag> = emptySet()

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import okhttp3.Headers
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@ -140,11 +142,12 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
} }
} }
override suspend fun getPageFullUrl(page: MangaPage): String { override suspend fun getPageRequest(page: MangaPage): RequestDraft {
val domain = conf.getDomain(DOMAIN) val domain = conf.getDomain(DOMAIN)
val ssl = conf.isUseSsl(false) val ssl = conf.isUseSsl(false)
val doc = loaderContext.httpGet(page.url).parseHtml() val doc = loaderContext.httpGet(page.url).parseHtml()
return doc.getElementById("image").attr("src").withDomain(domain, ssl) val url = doc.getElementById("image").attr("src").withDomain(domain, ssl)
return RequestDraft(url, Headers.headersOf(CommonHeaders.REFERER, page.referer))
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {

@ -1,9 +1,11 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import okhttp3.Headers
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@ -116,9 +118,10 @@ class NudeMoonRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
} }
} }
override suspend fun getPageFullUrl(page: MangaPage): String { override suspend fun getPageRequest(page: MangaPage): RequestDraft {
val doc = loaderContext.httpGet(page.url).parseHtml() val doc = loaderContext.httpGet(page.url).parseHtml()
return doc.body().getElementById("gallery").attr("src").inContextOf(doc) val url = doc.body().getElementById("gallery").attr("src").inContextOf(doc)
return RequestDraft(url, Headers.headersOf(CommonHeaders.REFERER, page.referer))
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {

@ -12,8 +12,8 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okio.IOException import okio.IOException
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -23,6 +23,8 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseService import org.koitharu.kotatsu.base.ui.BaseService
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.RequestDraft
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.MangaZip import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
@ -105,7 +107,12 @@ class DownloadService : BaseService() {
output = MangaZip.findInDir(destination, data) output = MangaZip.findInDir(destination, data)
output.prepare(data) output.prepare(data)
val coverUrl = data.largeCoverUrl ?: data.coverUrl val coverUrl = data.largeCoverUrl ?: data.coverUrl
downloadPage(coverUrl, destination).let { file -> downloadPage(
RequestDraft(
coverUrl,
Headers.headersOf(CommonHeaders.REFERER, data.url)
), destination
).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
} }
val chapters = if (chaptersIds == null) { val chapters = if (chaptersIds == null) {
@ -119,13 +126,14 @@ class DownloadService : BaseService() {
for ((pageIndex, page) in pages.withIndex()) { for ((pageIndex, page) in pages.withIndex()) {
failsafe@ do { failsafe@ do {
try { try {
val url = repo.getPageFullUrl(page) val request = repo.getPageRequest(page)
val file = cache[url] ?: downloadPage(url, destination) val file =
cache[request.url] ?: downloadPage(request, destination)
output.addPage( output.addPage(
chapter, chapter,
file, file,
pageIndex, pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url) MimeTypeMap.getFileExtensionFromUrl(request.url)
) )
} catch (e: IOException) { } catch (e: IOException) {
notification.setWaitingForNetwork() notification.setWaitingForNetwork()
@ -187,9 +195,8 @@ class DownloadService : BaseService() {
} }
} }
private suspend fun downloadPage(url: String, destination: File): File { private suspend fun downloadPage(requestDraft: RequestDraft, destination: File): File {
val request = Request.Builder() val request = requestDraft.newBuilder()
.url(url)
.cacheControl(CacheUtils.CONTROL_DISABLED) .cacheControl(CacheUtils.CONTROL_DISABLED)
.get() .get()
.build() .build()

@ -7,6 +7,7 @@ import android.webkit.MimeTypeMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import okhttp3.internal.EMPTY_HEADERS
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.CbzFilter
@ -156,7 +157,9 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
override val sortOrders = emptySet<SortOrder>() override val sortOrders = emptySet<SortOrder>()
override suspend fun getPageFullUrl(page: MangaPage) = page.url override suspend fun getPageRequest(page: MangaPage): RequestDraft {
return RequestDraft(page.url, EMPTY_HEADERS)
}
override suspend fun getTags() = emptySet<MangaTag>() override suspend fun getTags() = emptySet<MangaTag>()

@ -8,7 +8,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import org.koitharu.kotatsu.core.model.RequestDraft
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.await
@ -24,30 +25,28 @@ class PageLoader(
private val tasks = ArrayMap<String, Deferred<File>>() private val tasks = ArrayMap<String, Deferred<File>>()
private val convertLock = Mutex() private val convertLock = Mutex()
suspend fun loadFile(url: String, referer: String, force: Boolean): File { suspend fun loadFile(requestDraft: RequestDraft, force: Boolean): File {
if (!force) { if (!force) {
cache[url]?.let { cache[requestDraft.url]?.let {
return it return it
} }
} }
val task = tasks[url]?.takeUnless { it.isCancelled || (force && it.isCompleted) } val task =
return (task ?: loadAsync(url, referer).also { tasks[url] = it }).await() tasks[requestDraft.url]?.takeUnless { it.isCancelled || (force && it.isCompleted) }
return (task ?: loadAsync(requestDraft).also { tasks[requestDraft.url] = it }).await()
} }
private fun loadAsync(url: String, referer: String) = async(Dispatchers.IO) { private fun loadAsync(requestDraft: RequestDraft) = async(Dispatchers.IO) {
val uri = Uri.parse(url) val uri = Uri.parse(requestDraft.url)
if (uri.scheme == "cbz") { if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart) val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use { zip.getInputStream(entry).use {
cache.put(url, it) cache.put(requestDraft.url, it)
} }
} else { } else {
val request = Request.Builder() val request = requestDraft.newBuilder()
.url(url) .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.get()
.header("Accept", "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.header("Referer", referer)
.cacheControl(CacheUtils.CONTROL_DISABLED) .cacheControl(CacheUtils.CONTROL_DISABLED)
.build() .build()
okHttp.newCall(request).await().use { response -> okHttp.newCall(request).await().use { response ->
@ -58,7 +57,7 @@ class PageLoader(
"Null response" "Null response"
} }
body.byteStream().use { body.byteStream().use {
cache.put(url, it) cache.put(requestDraft.url, it)
} }
} }
} }

@ -9,7 +9,6 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
@ -154,15 +153,14 @@ class ReaderViewModel(
it.chapterId == state.chapterId && it.index == state.page it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage() ?: error("Page not found") }?.toMangaPage() ?: error("Page not found")
val repo = page.source.repository val repo = page.source.repository
val url = repo.getPageFullUrl(page) val pageRequest = repo.getPageRequest(page)
val request = Request.Builder() val request = pageRequest.newBuilder()
.url(url)
.get() .get()
.build() .build()
val uri = get<OkHttpClient>().newCall(request).await().use { response -> val uri = get<OkHttpClient>().newCall(request).await().use { response ->
val fileName = val fileName =
URLUtil.guessFileName( URLUtil.guessFileName(
url, pageRequest.url,
response.contentDisposition, response.contentDisposition,
response.mimeType response.mimeType
) )

@ -89,9 +89,9 @@ class PageHolderDelegate(
callback.onLoadingStarted() callback.onLoadingStarted()
try { try {
val file = withContext(Dispatchers.IO) { val file = withContext(Dispatchers.IO) {
val pageUrl = data.source.repository.getPageFullUrl(data) val pageRequest = data.source.repository.getPageRequest(data)
check(pageUrl.isNotEmpty()) { "Cannot obtain full image url" } check(pageRequest.isValid) { "Cannot obtain full image url" }
loader.loadFile(pageUrl, data.referer, force) loader.loadFile(pageRequest, force)
} }
this@PageHolderDelegate.file = file this@PageHolderDelegate.file = file
state = State.LOADED state = State.LOADED

@ -6,9 +6,12 @@ import coil.request.ImageRequest
import coil.size.PixelSize import coil.size.PixelSize
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.Headers
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.RequestDraft
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
@ -42,13 +45,18 @@ fun pageThumbnailAD(
text = (item.number).toString() text = (item.number).toString()
} }
job = scope.launch(Dispatchers.Default + IgnoreErrors) { job = scope.launch(Dispatchers.Default + IgnoreErrors) {
val url = item.page.preview ?: item.page.url.let { val pageRequest = item.page.preview?.let {
val pageUrl = item.repository.getPageFullUrl(item.page) RequestDraft(it, Headers.headersOf(CommonHeaders.REFERER, item.page.referer))
cache[pageUrl]?.toUri()?.toString() ?: pageUrl } ?: item.page.url.let {
val pageRequest = item.repository.getPageRequest(item.page)
cache[pageRequest.url]?.toUri()?.toString()?.let {
pageRequest.copy(url = it)
} ?: pageRequest
} }
val drawable = coil.execute( val drawable = coil.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(url) .data(pageRequest.url)
.headers(pageRequest.headers)
.size(thumbSize) .size(thumbSize)
.allowRgb565(true) .allowRgb565(true)
.build() .build()

@ -7,6 +7,7 @@ import coil.request.ErrorResult
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.ImageResult import coil.request.ImageResult
import coil.request.SuccessResult import coil.request.SuccessResult
import org.koitharu.kotatsu.core.network.CommonHeaders
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun ImageView.newImageRequest(url: String) = ImageRequest.Builder(context) inline fun ImageView.newImageRequest(url: String) = ImageRequest.Builder(context)
@ -31,4 +32,6 @@ fun ImageResult.toBitmapOrNull() = when (this) {
} }
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun ImageRequest.Builder.referer(referer: String) = this.setHeader("Referer", referer) inline fun ImageRequest.Builder.referer(referer: String): ImageRequest.Builder {
return setHeader(CommonHeaders.REFERER, referer)
}

@ -1,9 +1,10 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders
val Response.mimeType: String? val Response.mimeType: String?
get() = body?.contentType()?.run { "$type/$subtype" } get() = body?.contentType()?.run { "$type/$subtype" }
val Response.contentDisposition: String? val Response.contentDisposition: String?
get() = header("Content-Disposition") get() = header(CommonHeaders.CONTENT_DISPOSITION)

@ -82,7 +82,7 @@ class RemoteRepositoryTest(source: MangaSource) : KoinTest {
val pages = runBlocking { repo.getPages(details.chapters!!.random()) } val pages = runBlocking { repo.getPages(details.chapters!!.random()) }
Assert.assertFalse(pages.isEmpty()) Assert.assertFalse(pages.isEmpty())
val page = pages.random() val page = pages.random()
val fullUrl = runBlocking { repo.getPageFullUrl(page) } val fullUrl = runBlocking { repo.getPageRequest(page) }
AssertX.assertContentType(fullUrl, "image/*") AssertX.assertContentType(fullUrl, "image/*")
} }

Loading…
Cancel
Save