xCaliBR Scans: Update anti scrap logic (#13265)

* xCaliBR Scans: Update anti scrap logic

Also handles HTTP 103 through WebView

Fixes #10528

* Remove unused import

* Remove Http103Interceptor
This commit is contained in:
AntsyLich 2022-08-31 22:25:15 +06:00 committed by GitHub
parent fc3e6cd9c3
commit f80dab6f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 114 additions and 193 deletions

View File

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.extension.en.xcalibrscans
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class AntiScrapInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.fragment != ANTI_SCRAP_FRAGMENT) {
return chain.proceed(request)
}
val imageUrls = request.url
.queryParameter("urls").orEmpty()
.split(IMAGE_URLS_SEPARATOR)
var width = 0
var height = 0
val imageBitmaps = imageUrls.map { imageUrl ->
val newRequest = request.newBuilder().url(imageUrl).build()
val response = chain.proceed(newRequest)
val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
response.close()
width += bitmap.width
height = bitmap.height
bitmap
}
val mergedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
Canvas(mergedBitmap).apply {
// Will mirror everything that are applied afterwards
scale(-1F, 1F, width / 2F, height / 2F)
// Merge the bitmaps vertically
var left = 0
imageBitmaps.forEach { bitmap ->
val srcRect = Rect(0, 0, bitmap.width, bitmap.height)
val dstRect = Rect(left, 0, left + bitmap.width, bitmap.height)
drawBitmap(bitmap, srcRect, dstRect, null)
left += bitmap.width
}
}
val baos = ByteArrayOutputStream()
mergedBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
return Response.Builder()
.code(200)
.protocol(Protocol.HTTP_1_1)
.request(request)
.message("OK")
.body(baos.toByteArray().toResponseBody(pngMediaType))
.build()
}
companion object {
const val ANTI_SCRAP_FRAGMENT = "ANTI_SCRAP"
const val IMAGE_URLS_SEPARATOR = "|"
val pngMediaType = "image/png".toMediaType()
}
}

View File

@ -1,61 +0,0 @@
package eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class MirrorImageInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (!chain.request().url.toString().endsWith(MIRRORED_IMAGE_SUFFIX)) {
return chain.proceed(chain.request())
}
val imageUrl = chain.request().url.toString()
.removeSuffix(MIRRORED_IMAGE_SUFFIX)
val request = chain.request().newBuilder().url(imageUrl).build()
val response = chain.proceed(request)
val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
val result = bitmap.flipHorizontally()
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.PNG, 100, output)
val responseBody = output.toByteArray().toResponseBody("image/png".toMediaType())
return Response.Builder()
.code(200)
.protocol(Protocol.HTTP_1_1)
.request(chain.request())
.message("OK")
.body(responseBody)
.build()
}
private fun Bitmap.flipHorizontally(): Bitmap {
val matrix = Matrix().apply {
postScale(
-1F,
1F,
this@flipHorizontally.width / 2F,
this@flipHorizontally.height / 2F
)
}
return Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
}
}
const val MIRRORED_IMAGE_SUFFIX = "?mirrored"
fun String.prepareMirrorImageForInterceptor(): String {
return "$this$MIRRORED_IMAGE_SUFFIX"
}

View File

@ -1,73 +0,0 @@
package eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class SplittedImageInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (!chain.request().url.toString().endsWith(SPLITTED_IMAGE_SUFFIX)) {
return chain.proceed(chain.request())
}
val imageUrls = chain.request().url.toString()
.removeSuffix(SPLITTED_IMAGE_SUFFIX)
.split("%7C")
var width = 0
var height = 0
val imageBitmaps = imageUrls.map { imageUrl ->
val request = chain.request().newBuilder().url(imageUrl).build()
val response = chain.proceed(request)
val bitmap = BitmapFactory.decodeStream(response.body!!.byteStream())
width += bitmap.width
height = bitmap.height
bitmap
}
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
var left = 0
imageBitmaps.forEach { bitmap ->
val srcRect = Rect(0, 0, bitmap.width, bitmap.height)
val dstRect = Rect(left, 0, left + bitmap.width, bitmap.height)
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
left += bitmap.width
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.PNG, 100, output)
val responseBody = output.toByteArray().toResponseBody("image/png".toMediaType())
return Response.Builder()
.code(200)
.protocol(Protocol.HTTP_1_1)
.request(chain.request())
.message("OK")
.body(responseBody)
.build()
}
}
const val SPLITTED_IMAGE_SUFFIX = "?splitted"
fun List<String>.prepareSplittedImageForInterceptor(): String {
return "${this.joinToString("|")}$SPLITTED_IMAGE_SUFFIX"
}

View File

@ -1,17 +1,13 @@
package eu.kanade.tachiyomi.extension.en.xcalibrscans package eu.kanade.tachiyomi.extension.en.xcalibrscans
import eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor.MirrorImageInterceptor import android.util.Log
import eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor.SplittedImageInterceptor
import eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor.prepareMirrorImageForInterceptor
import eu.kanade.tachiyomi.extension.en.xcalibrscans.interceptor.prepareSplittedImageForInterceptor
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import kotlinx.serialization.json.jsonArray import okhttp3.HttpUrl.Companion.toHttpUrl
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import java.lang.IllegalArgumentException import org.jsoup.nodes.Element
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class xCaliBRScans : MangaThemesia("xCaliBR Scans", "https://xcalibrscans.com", "en") { class xCaliBRScans : MangaThemesia("xCaliBR Scans", "https://xcalibrscans.com", "en") {
@ -19,69 +15,50 @@ class xCaliBRScans : MangaThemesia("xCaliBR Scans", "https://xcalibrscans.com",
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(AntiScrapInterceptor())
.rateLimit(2) .rateLimit(2)
.addNetworkInterceptor(SplittedImageInterceptor())
.addNetworkInterceptor(MirrorImageInterceptor())
.build() .build()
override val hasProjectPage = true override val hasProjectPage = true
override val pageSelector = "div#readerarea > p, div#readerarea > div"
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val htmlPages = mutableListOf<Page>() document.selectFirst("div#readerarea .sword_box") ?: return super.pageListParse(document)
document.select(pageSelector) val imgUrls = mutableListOf<String>()
.filterNot {
it.select("img").all { imgEl -> // Selects all direct descendant of "div#readerarea"
imgEl.attr("abs:src").isNullOrEmpty() document.select("div#readerarea > *")
} .forEach { element ->
}
.map { el ->
if (el.tagName() == "div") {
when { when {
el.hasClass("kage") -> { element.tagName() == "p" -> {
el.select("img").map { imgEl -> val imgUrl = element.selectFirst("img").imgAttr()
val index = htmlPages.size imgUrls.add(imgUrl)
val imageUrl =
imgEl.attr("abs:src").prepareMirrorImageForInterceptor()
htmlPages.add(Page(index, "", imageUrl))
} }
} element.tagName() == "div" && element.hasClass("kage") -> {
el.hasClass("row") -> { parseAntiScrapScramble(element, imgUrls)
val index = htmlPages.size
val imageUrls = el.select("img").map { imgEl ->
imgEl.attr("abs:src")
}.prepareSplittedImageForInterceptor()
htmlPages.add(Page(index, "", imageUrls))
} }
else -> { else -> {
val index = htmlPages.size Log.d("xCaliBR Scans", "Unknown element for page parsing $element")
Page(index, "", el.select("img").attr("abs:src"))
} }
} }
} else {
val index = htmlPages.size
Page(index, "", el.select("img").attr("abs:src"))
}
} }
countViews(document) return imgUrls.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
// Some sites also loads pages via javascript
if (htmlPages.isNotEmpty()) { return htmlPages }
val docString = document.toString()
val imageListJson = JSON_IMAGE_LIST_REGEX.find(docString)?.destructured?.toList()?.get(0).orEmpty()
val imageList = try {
json.parseToJsonElement(imageListJson).jsonArray
} catch (_: IllegalArgumentException) {
emptyList()
}
val scriptPages = imageList.mapIndexed { i, jsonEl ->
Page(i, "", jsonEl.jsonPrimitive.content)
} }
return scriptPages private fun parseAntiScrapScramble(element: Element, destination: MutableList<String>) {
element.select("div.sword")
.forEach { swordDiv ->
val imgUrls = swordDiv.select("img").map { it.imgAttr() }
val urls = imgUrls.joinToString(AntiScrapInterceptor.IMAGE_URLS_SEPARATOR)
val url = baseUrl.toHttpUrl()
.newBuilder()
.addQueryParameter("urls", urls)
.fragment(AntiScrapInterceptor.ANTI_SCRAP_FRAGMENT)
.build()
.toString()
destination.add(url)
}
} }
} }

View File

@ -105,7 +105,7 @@ class MangaThemesiaGenerator : ThemeSourceGenerator {
SingleLang("West Manga", "https://westmanga.info", "id", overrideVersionCode = 1), SingleLang("West Manga", "https://westmanga.info", "id", overrideVersionCode = 1),
SingleLang("White Cloud Pavilion (New)", "https://www.whitecloudpavilion.com", "en", pkgName = "whitecloudpavilionnew", className = "WhiteCloudPavilion"), SingleLang("White Cloud Pavilion (New)", "https://www.whitecloudpavilion.com", "en", pkgName = "whitecloudpavilionnew", className = "WhiteCloudPavilion"),
SingleLang("World Romance Translation", "https://wrt.my.id", "id", overrideVersionCode = 10), SingleLang("World Romance Translation", "https://wrt.my.id", "id", overrideVersionCode = 10),
SingleLang("xCaliBR Scans", "https://xcalibrscans.com", "en", overrideVersionCode = 3), SingleLang("xCaliBR Scans", "https://xcalibrscans.com", "en", overrideVersionCode = 4),
) )
companion object { companion object {