diff --git a/src/id/mangaku/build.gradle b/src/id/mangaku/build.gradle index e460563f1..6a72c4a58 100644 --- a/src/id/mangaku/build.gradle +++ b/src/id/mangaku/build.gradle @@ -5,7 +5,11 @@ ext { extName = 'Mangaku' pkgNameSuffix = 'id.mangaku' extClass = '.Mangaku' - extVersionCode = 3 + extVersionCode = 4 } apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(':lib-cryptoaes')) +} diff --git a/src/id/mangaku/src/eu/kanade/tachiyomi/extension/id/mangaku/Mangaku.kt b/src/id/mangaku/src/eu/kanade/tachiyomi/extension/id/mangaku/Mangaku.kt index 5b7a29e75..d6ba28732 100644 --- a/src/id/mangaku/src/eu/kanade/tachiyomi/extension/id/mangaku/Mangaku.kt +++ b/src/id/mangaku/src/eu/kanade/tachiyomi/extension/id/mangaku/Mangaku.kt @@ -1,7 +1,12 @@ 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.POST +import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage 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.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.FormBody import okhttp3.Headers import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import rx.Observable +import java.security.MessageDigest class Mangaku : ParsedHttpSource() { + override val name = "Mangaku" - override val baseUrl = "https://mangaku.site/" + + override val baseUrl = "https://mangaku.vip" + override val lang = "id" + override val supportsLatest = true - private var searchQuery = "" - override fun popularMangaRequest(page: Int): Request { - return GET(baseUrl + "daftar-komik-bahasa-indonesia/", headers) - } + override val client = network.cloudflareClient - override fun latestUpdatesRequest(page: Int): Request { - return GET(baseUrl, headers) - } + private lateinit var directory: Elements - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - searchQuery = query - return GET(baseUrl + "daftar-komik-bahasa-indonesia/", headers) - } + override fun headersBuilder(): Headers.Builder = + super.headersBuilder().add("Referer", "$baseUrl/") - override fun popularMangaSelector() = "a.screenshot" - override fun latestUpdatesSelector() = "div.kiri_anime div.utao" - override fun searchMangaSelector() = popularMangaSelector() - - override fun popularMangaFromElement(element: Element): SManga { - val manga = SManga.create() - manga.thumbnail_url = element.attr("rel") - 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("
") - val manga = SManga.create() - infoString.forEach { - if (it.contains("")) { - val info = it.split("") - val key = info[0].replace(":", "").replace("", "").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 - } - } + override fun fetchPopularManga(page: Int): Observable { + return if (page == 1) { + client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { popularMangaParse(it) } + } else { + Observable.just(parseDirectory(page)) } - 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 popularMangaRequest(page: Int): Request = + 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" - 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 + private fun parseDirectory(page: Int): MangasPage { + val manga = mutableListOf() + 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 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 popularMangaSelector() = "#data .npx .an a" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.ownText() + 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 { - return GET(chapter.url, headers) + override fun chapterListSelector() = "#content-b > div > a" + + 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 { - 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()) { - pageList = document.select("div.entry-content img") + val wpRoutineJs = client.newCall(GET(wpRoutineUrl, headers)).execute().use { + 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() - var i = 0 - pageList.forEach { element -> - val imageUrl = element.attr("src") - i++ - if (imageUrl.isNotEmpty()) { - pages.add(Page(i, "", imageUrl)) + 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() + } + + 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 } - } - return pages + output + }.joinToString("") } - override fun imageUrlParse(document: Document) = "" - override fun imageRequest(page: Page): Request { - var mainUrl = baseUrl - var imageUrl = page.imageUrl.toString() - if (imageUrl.contains("mangaku.co")) { - mainUrl = "https://mangaku.co" - } - if (imageUrl.startsWith("//")) { - imageUrl = "https:$imageUrl" - } else if (imageUrl.startsWith("/")) { - imageUrl = mainUrl + imageUrl - } - val imgHeader = Headers.Builder().apply { - 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") - add("referer", mainUrl) - }.build() - return GET(imageUrl, imgHeader) + private fun upt4(appMgk: String, key: String): String { + val fullKey = key + appMgk.map { + (it.code xor 71).toChar() + }.joinToString("") + + val b64FullKey = btoa(fullKey) + + val sixLastChars = b64FullKey.substring(b64FullKey.length - 7, b64FullKey.length - 1) + val elevenFirstChars = b64FullKey.substring(0, 12) + val keyFragment = btoa(sixLastChars + elevenFirstChars).trim() + val firstDigest = digest("SHA384", keyFragment) + + val uniqueLetters = firstDigest.replace(noLetterRe, "").distinct() + val uniqueNumbers = firstDigest.replace(noNumberRe, "").distinct() + val joined = uniqueNumbers + uniqueLetters + + val secondDigest = digest("SHA1", joined) + val secondDigestReversed = secondDigest.reversed() + + val rotated = stringRotator(secondDigestReversed) + "-$key" + return keyFragment + joined + secondDigestReversed + rotated } - override fun popularMangaNextPageSelector() = "next" - override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() - override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() - - @SuppressLint("DefaultLocale") - override fun searchMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - val mangas = arrayListOf() - document.select(searchMangaSelector()).forEach { element -> - val manga = popularMangaFromElement(element) - if (manga.title.lowercase().contains(searchQuery.lowercase())) { - mangas.add(manga) - } + private fun stringRotator( + input: String, + multiplier: Int = 73, + adder: Int = 93, + length: Int = 20, + strings: List = 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() } - 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) }