[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)
}
override fun latestUpdatesRequest(page: Int): Request { private lateinit var directory: Elements
return GET(baseUrl, headers)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun headersBuilder(): Headers.Builder =
searchQuery = query super.headersBuilder().add("Referer", "$baseUrl/")
return GET(baseUrl + "daftar-komik-bahasa-indonesia/", headers)
}
override fun popularMangaSelector() = "a.screenshot" override fun fetchPopularManga(page: Int): Observable<MangasPage> {
override fun latestUpdatesSelector() = "div.kiri_anime div.utao" return if (page == 1) {
override fun searchMangaSelector() = popularMangaSelector() client.newCall(popularMangaRequest(page))
.asObservableSuccess()
override fun popularMangaFromElement(element: Element): SManga { .map { popularMangaParse(it) }
val manga = SManga.create() } else {
manga.thumbnail_url = element.attr("rel") Observable.just(parseDirectory(page))
manga.url = element.attr("href")
manga.title = element.text()
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga {
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")
manga.url = mangaUrl.replace("hhtps", "https")
manga.title = element.select("div.uta div.luf a.series").text()
return manga
}
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
@SuppressLint("DefaultLocale")
override fun mangaDetailsParse(document: Document): SManga {
val infoString = document.select("#abc > p > span > small").html().toString().split("<br> ")
val manga = SManga.create()
infoString.forEach {
if (it.contains("</b>")) {
val info = it.split("</b>")
val key = info[0].replace(":", "").replace("<b>", "").trim().lowercase()
val value = info[1].replace(":", "").trim()
when (key) {
"genre" -> manga.genre = value.replace("", ", ").replace("-", ", ").trim()
"author" -> manga.author = value
"artist" -> manga.artist = value
"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 { override fun popularMangaRequest(page: Int): Request =
return GET(manga.url, 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 chapterListSelector() = "div.entry > div > table > tbody > tr > td:nth-child(1) > small > div:nth-child(2) > a" private fun parseDirectory(page: Int): MangasPage {
override fun chapterFromElement(element: Element): SChapter { val manga = mutableListOf<SManga>()
val chapter = SChapter.create() val end = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
val chapterUrl = element.attr("href")
chapter.url = chapterUrl for (i in (((page - 1) * 24)..end)) {
chapter.name = element.text() manga.add(popularMangaFromElement(directory[i]))
if (chapter.name.contains("")) }
chapter.name = chapter.name.split("")[1].trim() return MangasPage(manga, end < directory.lastIndex)
return chapter
} }
override fun prepareNewChapter(chapter: SChapter, manga: SManga) { override fun popularMangaSelector() = "#data .npx .an a"
val basic = Regex("""Chapter\s([0-9]+)""")
when { override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
basic.containsMatchIn(chapter.name) -> { setUrlWithoutDomain(element.attr("href"))
basic.find(chapter.name)?.let { title = element.ownText()
chapter.chapter_number = it.groups[1]?.value!!.toFloat() thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
} }
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
override fun latestUpdatesSelector() = "div.kiri_anime div.utao, div.proyek div.utao"
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 latestUpdatesNextPageSelector(): String? = null
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
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")
}
override fun searchMangaNextPageSelector(): String? = null
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select(".post.singlep .titles a").text().replace("Bahasa Indonesia", "").trim()
thumbnail_url = document.select(".post.singlep img").attr("abs:src")
document.select("#wrapper-a #content-a .inf").forEach {
val key = it.select(".infx").text()
when (key) {
"Genre" -> genre = it.select("p a[rel=tag]").joinToString { it.text() }
"Author" -> author = it.select("p").text()
"Sinopsis" -> description = it.select("p").text()
} }
} }
} }
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 pages = mutableListOf<Page>() val dtxScript = document.selectFirst("script:containsData(var dtx =)").html()
var i = 0 val dtxIsEqualTo = dtxScript
pageList.forEach { element -> .substringAfter("var dtx = ")
val imageUrl = element.attr("src") .substringBefore(";")
i++ val dtx = dtxScript
if (imageUrl.isNotEmpty()) { .substringAfter("var $dtxIsEqualTo= \"")
pages.add(Page(i, "", imageUrl)) .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()
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
private val rsxxxRe = Regex(""".............?\+.......""")
private val noLetterRe = Regex("""[^a-z]""")
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,
@SuppressLint("DefaultLocale") length: Int = 20,
override fun searchMangaParse(response: Response): MangasPage { strings: List<String> = listOf("PEWAW", "MJKJG", "SGWRT", "KUIQ"),
val document = response.asJsoup() ): String {
val mangas = arrayListOf<SManga>() val firstPass = input
document.select(searchMangaSelector()).forEach { element -> .map { input.length * multiplier + (adder + it.code) }
val manga = popularMangaFromElement(element) .joinToString("") + adder.toString()
if (manga.title.lowercase().contains(searchQuery.lowercase())) { return firstPass.map {
mangas.add(manga) val idx = it.toString().toInt()
} if (idx < strings.size) strings[idx] else idx.toString()
} }
return MangasPage(mangas, false) .joinToString("")
.padEnd(length)
.substring(0, length)
} }
private fun btoa(input: String): String =
Base64.encode(input.toByteArray(), Base64.DEFAULT).toString(Charsets.UTF_8)
private fun digest(digest: String, input: String): String =
MessageDigest.getInstance(digest).digest(input.toByteArray())
.joinToString("") { "%02x".format(it) }
private fun String.distinct(): String = toCharArray().distinct().joinToString("")
private fun String.percentDecode(): String = Uri.decode(this)
} }