diff --git a/src/zh/boylove/build.gradle b/src/zh/boylove/build.gradle index 9af163271..5031959e6 100644 --- a/src/zh/boylove/build.gradle +++ b/src/zh/boylove/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'BoyLove' extClass = '.BoyLove' - extVersionCode = 9 + extVersionCode = 10 isNsfw = true } diff --git a/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/BoyLove.kt b/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/BoyLove.kt index c0b1a425f..b5be5f88d 100644 --- a/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/BoyLove.kt +++ b/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/BoyLove.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.extension.zh.boylove import android.app.Application -import android.content.SharedPreferences import android.util.Log import androidx.preference.ListPreference import androidx.preference.PreferenceScreen @@ -19,8 +18,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response +import org.jsoup.nodes.Document import org.jsoup.select.Evaluator import rx.Observable import uy.kohesive.injekt.Injekt @@ -37,8 +38,8 @@ class BoyLove : HttpSource(), ConfigurableSource { private val json: Json by injectLazy() - override val baseUrl = run { - val preferences: SharedPreferences = + override val baseUrl by lazy { + val preferences = Injekt.get().getSharedPreferences("source_$id", 0x0000) val mirrors = MIRRORS @@ -48,6 +49,7 @@ class BoyLove : HttpSource(), ConfigurableSource { override val client = network.cloudflareClient.newBuilder() .rateLimit(2) + .addInterceptor(UnscramblerInterceptor()) .build() override fun popularMangaRequest(page: Int): Request = @@ -111,7 +113,8 @@ class BoyLove : HttpSource(), ConfigurableSource { private fun fetchPageList(chapterUrl: String): Observable> = client.newCall(GET(baseUrl + chapterUrl, headers)).asObservableSuccess().map { response -> - val root = response.asJsoup().selectFirst(Evaluator.Tag("section"))!! + val doc = response.asJsoup() + val root = doc.selectFirst(Evaluator.Tag("section"))!! val images = root.select(Evaluator.Class("reader-cartoon-image")) val urlList = if (images.isEmpty()) { root.select(Evaluator.Tag("img")).map { it.attr("src").trim().toImageUrl() } @@ -121,9 +124,31 @@ class BoyLove : HttpSource(), ConfigurableSource { .filter { it.attr("src").endsWith("load.png") } .map { it.attr("data-original").trim().toImageUrl() } } - urlList.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) } + val parts = doc.getPartsCount() + urlList.mapIndexed { index, imageUrl -> + val url = if (parts == null) { + imageUrl + } else { + imageUrl.toHttpUrl().newBuilder() + .addQueryParameter(UnscramblerInterceptor.PARTS_COUNT_PARAM, parts.toString()) + .build() + .toString() + } + Page(index, imageUrl = url) + } } + private fun Document.getPartsCount(): Int? { + return selectFirst("script:containsData(do_mergeImg):containsData(context0 =)")?.data()?.run { + substringBefore("canvas0.width") + .substringAfterLast("var ") + .substringBefore(';') + .trim() + .substringAfterLast(" ") + .toIntOrNull() + } + } + override fun pageListParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() diff --git a/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/UnscramblerInterceptor.kt b/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/UnscramblerInterceptor.kt new file mode 100644 index 000000000..e4e95d01e --- /dev/null +++ b/src/zh/boylove/src/eu/kanade/tachiyomi/extension/zh/boylove/UnscramblerInterceptor.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.extension.zh.boylove + +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.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class UnscramblerInterceptor : Interceptor { + companion object { + const val PARTS_COUNT_PARAM = "scrambled_parts_count" + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val parts = request.url.queryParameter(PARTS_COUNT_PARAM)?.toIntOrNull() + return if (parts == null) { + chain.proceed(request) + } else { + val newRequest = request.newBuilder() + .url(request.url.newBuilder().removeAllQueryParameters(PARTS_COUNT_PARAM).build()) + .build() + val response = chain.proceed(newRequest) + + val image = response.body.byteStream().use { descramble(it, parts) } + + val body = image.toResponseBody("image/jpeg".toMediaType()) + response.newBuilder().body(body).build() + } + } + + private fun descramble(image: InputStream, partsCount: Int): ByteArray { + val srcBitmap = BitmapFactory.decodeStream(image) + val width = srcBitmap.width + val height = srcBitmap.height + + val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(result) + + for (partIndex in 1..partsCount) { + if (height >= 4000) { + val stripWidth = width / partsCount + val x1 = stripWidth * (partIndex - 1) + val rect = Rect(x1, 0, x1 + stripWidth, height) + canvas.drawBitmap(srcBitmap, rect, rect, null) + } else if (partIndex == partsCount) { + val stripWidth = width - ((width / partsCount) * (partsCount - 1)) + val rectSrc = Rect(0, 0, stripWidth, height) + val rectDst = Rect(width - stripWidth, 0, width, height) + canvas.drawBitmap(srcBitmap, rectSrc, rectDst, null) + } else { + val stripWidth = width / partsCount + val xSrc = width - (stripWidth * partIndex) + val rectSrc = Rect(xSrc, 0, xSrc + stripWidth, height) + val xDst = stripWidth * (partIndex - 1) + val rectDst = Rect(xDst, 0, xDst + stripWidth, height) + canvas.drawBitmap(srcBitmap, rectSrc, rectDst, null) + } + } + + val output = ByteArrayOutputStream() + result.compress(Bitmap.CompressFormat.JPEG, 90, output) + return output.toByteArray() + } +}