[Mangaku] rewrite (#14968)

* Mangaku rewrite

* fix: trim chapter names that start with -

* replace android.net.Uri.decode with something simpler

* small fixes + found culprit of lag

* fix: decryption not taking 20 seconds anymore

* use different hyphen character

* in case they start changing around stuff now that tachi got their nose
This commit is contained in:
beerpsi 2023-01-17 18:37:02 +07:00 committed by GitHub
parent b6dac7f8eb
commit bd4ab2a925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 245 additions and 124 deletions

View File

@ -5,7 +5,11 @@ ext {
extName = 'Mangaku' extName = 'Mangaku'
pkgNameSuffix = 'id.mangaku' pkgNameSuffix = 'id.mangaku'
extClass = '.Mangaku' extClass = '.Mangaku'
extVersionCode = 3 extVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib-cryptoaes'))
}

View File

@ -1,7 +1,12 @@
package eu.kanade.tachiyomi.extension.id.mangaku package eu.kanade.tachiyomi.extension.id.mangaku
import android.annotation.SuppressLint import android.net.Uri
import android.util.Base64
import android.util.Log
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -9,164 +14,276 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import rx.Observable
import java.security.MessageDigest
class Mangaku : ParsedHttpSource() { class Mangaku : ParsedHttpSource() {
override val name = "Mangaku" override val name = "Mangaku"
override val baseUrl = "https://mangaku.site/"
override val baseUrl = "https://mangaku.vip"
override val lang = "id" override val lang = "id"
override val supportsLatest = true override val supportsLatest = true
private var searchQuery = ""
override fun popularMangaRequest(page: Int): Request { override val client = network.cloudflareClient
return GET(baseUrl + "daftar-komik-bahasa-indonesia/", headers)
private lateinit var directory: Elements
override fun headersBuilder(): Headers.Builder =
super.headersBuilder().add("Referer", "$baseUrl/")
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { popularMangaParse(it) }
} else {
Observable.just(parseDirectory(page))
}
} }
override fun latestUpdatesRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET(baseUrl, headers) POST(
"$baseUrl/daftar-komik-bahasa-indonesia/",
headers,
FormBody.Builder().add("ritem", "hot").build()
)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
directory = document.select(popularMangaSelector())
return parseDirectory(1)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { private fun parseDirectory(page: Int): MangasPage {
searchQuery = query val manga = mutableListOf<SManga>()
return GET(baseUrl + "daftar-komik-bahasa-indonesia/", headers) val end = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
for (i in (((page - 1) * 24)..end)) {
manga.add(popularMangaFromElement(directory[i]))
}
return MangasPage(manga, end < directory.lastIndex)
} }
override fun popularMangaSelector() = "a.screenshot" override fun popularMangaSelector() = "#data .npx .an a"
override fun latestUpdatesSelector() = "div.kiri_anime div.utao"
override fun searchMangaSelector() = popularMangaSelector()
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
val manga = SManga.create() setUrlWithoutDomain(element.attr("href"))
manga.thumbnail_url = element.attr("rel") title = element.ownText()
manga.url = element.attr("href") thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
manga.title = element.text()
return manga
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun popularMangaNextPageSelector(): String? = null
val manga = SManga.create()
manga.thumbnail_url = element.select("div.uta div.imgu img").attr("src")
val mangaUrl = element.select("div.uta div.luf a.series").attr("href").replace("hhtps", "http") override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
manga.url = mangaUrl.replace("hhtps", "https")
manga.title = element.select("div.uta div.luf a.series").text() override fun latestUpdatesSelector() = "div.kiri_anime div.utao, div.proyek div.utao"
return manga
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.select("div.uta div.luf a.series").attr("href"))
title = element.select("div.uta div.luf a.series").attr("title")
thumbnail_url = element.select("div.uta div.imgu img").attr("abs:data-src")
} }
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) override fun latestUpdatesNextPageSelector(): String? = null
override fun mangaDetailsRequest(manga: SManga): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
return GET(manga.url, headers) GET("$baseUrl/search/$query/", headers)
override fun searchMangaSelector() = ".listupd .bs"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select(".bsx a").attr("href"))
title = element.select(".bigor .tt a").text()
thumbnail_url = element.select(".bsx img").attr("abs:data-src")
} }
@SuppressLint("DefaultLocale") override fun searchMangaNextPageSelector(): String? = null
override fun mangaDetailsParse(document: Document): SManga {
val infoString = document.select("#abc > p > span > small").html().toString().split("<br> ") override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val manga = SManga.create() title = document.select(".post.singlep .titles a").text().replace("Bahasa Indonesia", "").trim()
infoString.forEach { thumbnail_url = document.select(".post.singlep img").attr("abs:src")
if (it.contains("</b>")) { document.select("#wrapper-a #content-a .inf").forEach {
val info = it.split("</b>") val key = it.select(".infx").text()
val key = info[0].replace(":", "").replace("<b>", "").trim().lowercase()
val value = info[1].replace(":", "").trim()
when (key) { when (key) {
"genre" -> manga.genre = value.replace("", ", ").replace("-", ", ").trim() "Genre" -> genre = it.select("p a[rel=tag]").joinToString { it.text() }
"author" -> manga.author = value "Author" -> author = it.select("p").text()
"artist" -> manga.artist = value "Sinopsis" -> description = it.select("p").text()
"sinopsis" -> manga.description = value
}
}
}
manga.status = SManga.UNKNOWN
manga.thumbnail_url = document.select("#abc > div > span > small > a > img").attr("src")
return manga
}
override fun chapterListRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
override fun chapterListSelector() = "div.entry > div > table > tbody > tr > td:nth-child(1) > small > div:nth-child(2) > a"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
val chapterUrl = element.attr("href")
chapter.url = chapterUrl
chapter.name = element.text()
if (chapter.name.contains(""))
chapter.name = chapter.name.split("")[1].trim()
return chapter
}
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
val basic = Regex("""Chapter\s([0-9]+)""")
when {
basic.containsMatchIn(chapter.name) -> {
basic.find(chapter.name)?.let {
chapter.chapter_number = it.groups[1]?.value!!.toFloat()
}
} }
} }
} }
override fun pageListRequest(chapter: SChapter): Request { override fun chapterListSelector() = "#content-b > div > a"
return GET(chapter.url, headers)
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.text().let {
if (it.contains(""))
it.split("")[1].trim()
else
it
}
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
var pageList = document.select("div.entry img") val wpRoutineUrl = document.selectFirst("script[src*=wp-routine]").attr("abs:src")
Log.d("mangaku", "wp-routine: $wpRoutineUrl")
if (pageList.isEmpty()) { val wpRoutineJs = client.newCall(GET(wpRoutineUrl, headers)).execute().use {
pageList = document.select("div.entry-content img") it.body!!.string()
}
val upt3 = wpRoutineJs
.substringAfterLast("upt3(")
.substringBefore(");")
val appMgk = wpRoutineJs
.substringAfter("const $upt3 = '")
.substringBefore("'")
.reversed()
Log.d("mangaku", "app-mgk: $appMgk")
val dtxScript = document.selectFirst("script:containsData(var dtx =)").html()
val dtxIsEqualTo = dtxScript
.substringAfter("var dtx = ")
.substringBefore(";")
val dtx = dtxScript
.substringAfter("var $dtxIsEqualTo= \"")
.substringBefore("\"")
val mainScriptTag = document.selectFirst("script:containsData(await jrsx)").html()
val jrsxArgs = mainScriptTag
.substringAfter("await jrsx(")
.substringBefore(");")
.split(",")
Log.d("mangaku", "args: $jrsxArgs")
val thirdArgValue = mainScriptTag
.substringAfter("const ${jrsxArgs[2]} = '")
.substringBefore("'")
Log.d("mangaku", "arg2: $thirdArgValue")
val encodedAttr = jrsxArgs[4].removeSurrounding("'")
val upt4arg = mainScriptTag
.substringAfter("const ${jrsxArgs[3]} = await upt4('")
.substringBefore("'")
Log.d("mangaku", "upt4arg: $upt4arg")
val upt4value = upt4(appMgk, upt4arg)
val decrypted = CryptoAES.decrypt(dtx, huzorttshj(thirdArgValue, upt4value))
.replace(rsxxxRe, "")
.replace("_", "=")
.reversed()
.replace("+", "%20")
val htmImageList = Base64.decode(decrypted, Base64.DEFAULT)
.toString(Charsets.UTF_8)
.percentDecode()
val attr = stringRotator(encodedAttr, 23, 69, 9).lowercase()
val fifthArgValueDigest =
stringRotator(digest("SHA384", encodedAttr), 23, 69, 20).lowercase()
val re = Regex("""$attr=['"](.*?)['"]""")
return re.findAll(htmImageList).mapIndexed { idx, it ->
val url = Base64.decode(
CryptoAES.decrypt(it.groupValues[1], fifthArgValueDigest),
Base64.DEFAULT
)
.toString(Charsets.UTF_8)
.replace("+", "%20")
.percentDecode()
Page(idx, imageUrl = url)
}.toList()
} }
val pages = mutableListOf<Page>() override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
var i = 0
pageList.forEach { element -> private val rsxxxRe = Regex(""".............?\+.......""")
val imageUrl = element.attr("src")
i++ private val noLetterRe = Regex("""[^a-z]""")
if (imageUrl.isNotEmpty()) {
pages.add(Page(i, "", imageUrl)) private val noNumberRe = Regex("""[^0-9]""")
private val whitespaceRe = Regex("""\s+""")
private fun huzorttshj(key: String, upt4val: String): String {
val mapping = "-ABCDEFGHIJKLMNOPQRSTUVWXYZ=0123456789abcdefghijklmnopqrstuvwxyz+"
val b64upt4 = btoa(upt4val).replace(whitespaceRe, "")
var idx = 0
return b64upt4.map {
val upt4idx = mapping.indexOf(it)
val keyidx = mapping.indexOf(key[idx])
val output = (mapping.substring(keyidx) + mapping.substring(0, keyidx))[upt4idx]
if (idx == key.length - 1) {
idx = 0
} else {
idx += 1
} }
} output
return pages }.joinToString("")
} }
override fun imageUrlParse(document: Document) = "" private fun upt4(appMgk: String, key: String): String {
override fun imageRequest(page: Page): Request { val fullKey = key + appMgk.map {
var mainUrl = baseUrl (it.code xor 71).toChar()
var imageUrl = page.imageUrl.toString() }.joinToString("")
if (imageUrl.contains("mangaku.co")) {
mainUrl = "https://mangaku.co" val b64FullKey = btoa(fullKey)
}
if (imageUrl.startsWith("//")) { val sixLastChars = b64FullKey.substring(b64FullKey.length - 7, b64FullKey.length - 1)
imageUrl = "https:$imageUrl" val elevenFirstChars = b64FullKey.substring(0, 12)
} else if (imageUrl.startsWith("/")) { val keyFragment = btoa(sixLastChars + elevenFirstChars).trim()
imageUrl = mainUrl + imageUrl val firstDigest = digest("SHA384", keyFragment)
}
val imgHeader = Headers.Builder().apply { val uniqueLetters = firstDigest.replace(noLetterRe, "").distinct()
add("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36") val uniqueNumbers = firstDigest.replace(noNumberRe, "").distinct()
add("referer", mainUrl) val joined = uniqueNumbers + uniqueLetters
}.build()
return GET(imageUrl, imgHeader) val secondDigest = digest("SHA1", joined)
val secondDigestReversed = secondDigest.reversed()
val rotated = stringRotator(secondDigestReversed) + "-$key"
return keyFragment + joined + secondDigestReversed + rotated
} }
override fun popularMangaNextPageSelector() = "next" private fun stringRotator(
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() input: String,
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() multiplier: Int = 73,
adder: Int = 93,
length: Int = 20,
strings: List<String> = listOf("PEWAW", "MJKJG", "SGWRT", "KUIQ"),
): String {
val firstPass = input
.map { input.length * multiplier + (adder + it.code) }
.joinToString("") + adder.toString()
return firstPass.map {
val idx = it.toString().toInt()
if (idx < strings.size) strings[idx] else idx.toString()
}
.joinToString("")
.padEnd(length)
.substring(0, length)
}
@SuppressLint("DefaultLocale") private fun btoa(input: String): String =
override fun searchMangaParse(response: Response): MangasPage { Base64.encode(input.toByteArray(), Base64.DEFAULT).toString(Charsets.UTF_8)
val document = response.asJsoup()
val mangas = arrayListOf<SManga>() private fun digest(digest: String, input: String): String =
document.select(searchMangaSelector()).forEach { element -> MessageDigest.getInstance(digest).digest(input.toByteArray())
val manga = popularMangaFromElement(element) .joinToString("") { "%02x".format(it) }
if (manga.title.lowercase().contains(searchQuery.lowercase())) {
mangas.add(manga) private fun String.distinct(): String = toCharArray().distinct().joinToString("")
}
} private fun String.percentDecode(): String = Uri.decode(this)
return MangasPage(mangas, false)
}
} }