[ConstellarScans] changed their key format again (#15076)

* a

* bump version

* shenanigans beget shenanigans

* idk

* what's a lookup string

* make regex less greedy

* fighto!!!!!

* fix more shenanigans that emerged while this stuff isn't merged

* comments

* wrong place to trim

* be more specific with exceptions

* fix conflicting imports

* more generic regex

* dont need regex comments

* fix docs

* fix complaints about shadowing
This commit is contained in:
beerpsi 2023-01-24 17:45:51 +07:00 committed by GitHub
parent ca789eca79
commit 731a406861
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 46 additions and 36 deletions

View File

@ -7,6 +7,7 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.util.Log
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -23,6 +24,7 @@ import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.lang.IllegalArgumentException
import java.security.MessageDigest import java.security.MessageDigest
class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarscans.com", "en") { class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarscans.com", "en") {
@ -64,13 +66,13 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarsca
val data = client.newCall(req).execute().body!!.use { val data = client.newCall(req).execute().body!!.use {
json.parseToJsonElement(it.string()).jsonArray json.parseToJsonElement(it.string()).jsonArray
}.mapNotNull { }.mapNotNull {
it.jsonObject["user-agent"]?.jsonPrimitive?.content?.takeIf { it.jsonObject["user-agent"]?.jsonPrimitive?.content?.takeIf { ua ->
it.startsWith("Mozilla/5.0") && ua.startsWith("Mozilla/5.0") &&
( (
it.contains("iPhone") && ua.contains("iPhone") &&
(it.contains("FxiOS") || it.contains("CriOS")) || (ua.contains("FxiOS") || ua.contains("CriOS")) ||
it.contains("Android") && ua.contains("Android") &&
(it.contains("EdgA") || it.contains("Chrome") || it.contains("Firefox")) (ua.contains("EdgA") || ua.contains("Chrome") || ua.contains("Firefox"))
) )
} }
} }
@ -91,16 +93,28 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarsca
.build() .build()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val pageList = super.pageListParse(document) val tsData = TS_DATA_RE.find(document.select("script").html())?.groupValues?.get(1)
return when { ?: return super.pageListParse(document)
document.selectFirst("script:containsData(_code)") != null -> descramblePageUrls( val descrambledData = descrambleString(tsData).trim()
pageList
) val match = DESCRAMBLING_KEY_RE.find(descrambledData)?.value
document.selectFirst("script:containsData(ts_reader[_)") != null -> decodeDeviceLimitedChapter( if (match != null) {
document Log.d("constellarscans", "device-limited chapter, key: $match")
) return decodeDeviceLimitedChapter(match)
else -> pageList
} }
val imageListJson =
JSON_IMAGE_LIST_REGEX.find(descrambledData)?.groupValues?.get(1).orEmpty()
val imageList = try {
json.parseToJsonElement(imageListJson).jsonArray
} catch (_: IllegalArgumentException) {
emptyList()
}
val scriptPages = imageList.mapIndexed { i, jsonEl ->
Page(i, imageUrl = jsonEl.jsonPrimitive.content)
}
return scriptPages
} }
override fun imageRequest(page: Page): Request = super.imageRequest(page).newBuilder() override fun imageRequest(page: Page): Request = super.imageRequest(page).newBuilder()
@ -110,21 +124,15 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarsca
.header("Sec-Fetch-Site", "same-origin") .header("Sec-Fetch-Site", "same-origin")
.build() .build()
private fun descramblePageUrls(pages: List<Page>): List<Page> { private fun descrambleString(input: String): String =
return pages.map { input.replace(NOT_DIGIT_RE, "")
val lastSegment = it.imageUrl!!.split('/').last() .chunked(2)
val filename = lastSegment .joinToString("") { (it.toInt() + 32).toChar().toString() }
.replace(NOT_DIGIT_RE, "")
.chunked(2)
.joinToString("") { LOOKUP_STRING[it.toInt()].toString() }
Page(it.index, imageUrl = it.imageUrl!!.replace(lastSegment, filename))
}
}
private fun decodeDeviceLimitedChapter(document: Document): List<Page> { private fun decodeDeviceLimitedChapter(fullKey: String): List<Page> {
val script = document.selectFirst("script:containsData(ts_reader[_)").data() if (!DESCRAMBLING_KEY_RE.matches(fullKey)) {
val fullKey = DESCRAMBLING_KEY_RE.find(script)?.groupValues?.get(1) throw IllegalArgumentException("Did not receive suitable decryption key. Try opening the chapter again.")
?: throw Exception("Did not receive suitable decryption key. Try opening the chapter again.") }
val shiftBy = fullKey.substring(32..33).toInt(16) val shiftBy = fullKey.substring(32..33).toInt(16)
val key = fullKey.substring(0..31) + fullKey.substring(34) val key = fullKey.substring(0..31) + fullKey.substring(34)
@ -204,19 +212,21 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarsca
companion object { companion object {
const val DESCRAMBLE = "descramble" const val DESCRAMBLE = "descramble"
const val UA_DB_URL = const val UA_DB_URL =
"https://cdn.jsdelivr.net/gh/mimmi20/browscap-helper@30a83c095688f40b9eaca0165a479c661e5a7fbe/tests/0002999.json" "https://cdn.jsdelivr.net/gh/mimmi20/browscap-helper@30a83c095688f40b9eaca0165a479c661e5a7fbe/tests/0002999.json"
const val LOOKUP_STRING =
" !\"#${'$'}%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}"
const val LOOKUP_STRING_ALNUM = const val LOOKUP_STRING_ALNUM =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
val NOT_DIGIT_RE = Regex("""\D""") val NOT_DIGIT_RE = Regex("""\D""")
// We know that `ts_reader.run` accepts a JSON object, which contains `{"`, or in Constellar's
// encoding scheme, 91 02.
val TS_DATA_RE = Regex(
"""['"]([\da-z]*?9[a-z]*?1[a-z]*?0[a-z]*?2[\da-z]+?)['"]""",
RegexOption.IGNORE_CASE
)
// The decoding algorithm looks for a hex number in 32..33, so we write our regex accordingly // The decoding algorithm looks for a hex number in 32..33, so we write our regex accordingly
val DESCRAMBLING_KEY_RE = val DESCRAMBLING_KEY_RE =
Regex("""'([\da-z]{32}[\da-f]{2}[\da-z]+)'""", RegexOption.IGNORE_CASE) Regex("""[\da-z]{32}[\da-f]{2}[\da-z]+""", RegexOption.IGNORE_CASE)
} }
} }

View File

@ -25,7 +25,7 @@ class MangaThemesiaGenerator : ThemeSourceGenerator {
SingleLang("Azure Scans", "https://azuremanga.com", "en", overrideVersionCode = 1), SingleLang("Azure Scans", "https://azuremanga.com", "en", overrideVersionCode = 1),
SingleLang("Boosei", "https://boosei.net", "id", overrideVersionCode = 2), SingleLang("Boosei", "https://boosei.net", "id", overrideVersionCode = 2),
SingleLang("Clayrer", "https://clayrer.net", "es"), SingleLang("Clayrer", "https://clayrer.net", "es"),
SingleLang("Constellar Scans", "https://constellarscans.com", "en", isNsfw = true, overrideVersionCode = 5), SingleLang("Constellar Scans", "https://constellarscans.com", "en", isNsfw = true, overrideVersionCode = 6),
SingleLang("Cosmic Scans", "https://cosmicscans.com", "en", overrideVersionCode = 1), SingleLang("Cosmic Scans", "https://cosmicscans.com", "en", overrideVersionCode = 1),
SingleLang("Diskus Scan", "https://diskusscan.com", "pt-BR", overrideVersionCode = 7), SingleLang("Diskus Scan", "https://diskusscan.com", "pt-BR", overrideVersionCode = 7),
SingleLang("Dojing.net", "https://dojing.net", "id", isNsfw = true, className = "DojingNet"), SingleLang("Dojing.net", "https://dojing.net", "id", isNsfw = true, className = "DojingNet"),