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:
parent
fc3e6cd9c3
commit
f80dab6f73
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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 ->
|
||||||
}
|
when {
|
||||||
.map { el ->
|
element.tagName() == "p" -> {
|
||||||
if (el.tagName() == "div") {
|
val imgUrl = element.selectFirst("img").imgAttr()
|
||||||
when {
|
imgUrls.add(imgUrl)
|
||||||
el.hasClass("kage") -> {
|
}
|
||||||
el.select("img").map { imgEl ->
|
element.tagName() == "div" && element.hasClass("kage") -> {
|
||||||
val index = htmlPages.size
|
parseAntiScrapScramble(element, imgUrls)
|
||||||
val imageUrl =
|
}
|
||||||
imgEl.attr("abs:src").prepareMirrorImageForInterceptor()
|
else -> {
|
||||||
htmlPages.add(Page(index, "", imageUrl))
|
Log.d("xCaliBR Scans", "Unknown element for page parsing $element")
|
||||||
}
|
|
||||||
}
|
|
||||||
el.hasClass("row") -> {
|
|
||||||
val index = htmlPages.size
|
|
||||||
val imageUrls = el.select("img").map { imgEl ->
|
|
||||||
imgEl.attr("abs:src")
|
|
||||||
}.prepareSplittedImageForInterceptor()
|
|
||||||
htmlPages.add(Page(index, "", imageUrls))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val index = htmlPages.size
|
|
||||||
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
|
private fun parseAntiScrapScramble(element: Element, destination: MutableList<String>) {
|
||||||
if (htmlPages.isNotEmpty()) { return htmlPages }
|
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()
|
||||||
|
|
||||||
val docString = document.toString()
|
destination.add(url)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue