From 23e385128e39e62c61b9908f816e8b8797197966 Mon Sep 17 00:00:00 2001 From: beerpsi <92439990+beerpiss@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:46:25 +0700 Subject: [PATCH] Add MangaFun + LZString library (#1057) * Add MangaFun + LZString library * Mark as NSFW * Reverse using :lib:lzstring on Manhuagui * Add ending newline * Replace QuickJS in Manhuagui with LZString + Unpacker * Bump ManhuaGui version * remove unncessary .lets * optimize icons * Apply suggestion --- lib/lzstring/build.gradle.kts | 12 + .../kanade/tachiyomi/lib/lzstring/LZString.kt | 294 +++++++++++++++++ src/en/mangafun/AndroidManifest.xml | 21 ++ src/en/mangafun/build.gradle | 13 + .../mangafun/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3445 bytes .../mangafun/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1759 bytes .../mangafun/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4785 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 9489 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 13441 bytes .../extension/en/mangafun/DecompressJson.kt | 134 ++++++++ .../extension/en/mangafun/MangaFun.kt | 296 ++++++++++++++++++ .../extension/en/mangafun/MangaFunDto.kt | 70 +++++ .../extension/en/mangafun/MangaFunFilters.kt | 149 +++++++++ .../en/mangafun/MangaFunUrlActivity.kt | 33 ++ .../extension/en/mangafun/MangaFunUtils.kt | 77 +++++ src/zh/manhuagui/build.gradle | 7 +- .../extension/zh/manhuagui/Manhuagui.kt | 59 ++-- 17 files changed, 1133 insertions(+), 32 deletions(-) create mode 100644 lib/lzstring/build.gradle.kts create mode 100644 lib/lzstring/src/main/java/eu/kanade/tachiyomi/lib/lzstring/LZString.kt create mode 100644 src/en/mangafun/AndroidManifest.xml create mode 100644 src/en/mangafun/build.gradle create mode 100644 src/en/mangafun/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/en/mangafun/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/en/mangafun/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/en/mangafun/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/en/mangafun/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/DecompressJson.kt create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFun.kt create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunDto.kt create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunFilters.kt create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUrlActivity.kt create mode 100644 src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUtils.kt diff --git a/lib/lzstring/build.gradle.kts b/lib/lzstring/build.gradle.kts new file mode 100644 index 000000000..3aa58a627 --- /dev/null +++ b/lib/lzstring/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `java-library` + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(libs.kotlin.stdlib) +} diff --git a/lib/lzstring/src/main/java/eu/kanade/tachiyomi/lib/lzstring/LZString.kt b/lib/lzstring/src/main/java/eu/kanade/tachiyomi/lib/lzstring/LZString.kt new file mode 100644 index 000000000..01474914e --- /dev/null +++ b/lib/lzstring/src/main/java/eu/kanade/tachiyomi/lib/lzstring/LZString.kt @@ -0,0 +1,294 @@ +package eu.kanade.tachiyomi.lib.lzstring + +typealias getCharFromIntFn = (it: Int) -> String +typealias getNextValueFn = (it: Int) -> Int + +/** + * Reimplementation of [lz-string](https://github.com/pieroxy/lz-string) compression/decompression. + */ +object LZString { + private fun compress( + uncompressed: String, + bitsPerChar: Int, + getCharFromInt: getCharFromIntFn, + ): String { + val context = CompressionContext(uncompressed.length, bitsPerChar, getCharFromInt) + + for (ii in uncompressed.indices) { + context.c = uncompressed[ii].toString() + + if (!context.dictionary.containsKey(context.c)) { + context.dictionary[context.c] = context.dictSize++ + context.dictionaryToCreate[context.c] = true + } + + context.wc = context.w + context.c + + if (context.dictionary.containsKey(context.wc)) { + context.w = context.wc + continue + } + + context.outputCodeForW() + + context.decrementEnlargeIn() + context.dictionary[context.wc] = context.dictSize++ + context.w = context.c + } + + if (context.w.isNotEmpty()) { + context.outputCodeForW() + context.decrementEnlargeIn() + } + + // Mark the end of the stream + context.value = 2 + for (i in 0 until context.numBits) { + context.dataVal = (context.dataVal shl 1) or (context.value and 1) + context.appendDataOrAdvancePosition() + context.value = context.value shr 1 + } + + while (true) { + context.dataVal = context.dataVal shl 1 + + if (context.dataPosition == bitsPerChar - 1) { + context.data.append(getCharFromInt(context.dataVal)) + break + } + + context.dataPosition++ + } + + return context.data.toString() + } + + private fun decompress(length: Int, resetValue: Int, getNextValue: getNextValueFn): String { + val dictionary = mutableListOf() + val result = StringBuilder() + val data = DecompressionContext(resetValue, getNextValue) + var enlargeIn = 4 + var numBits = 3 + var entry: String + var c: Char? = null + + for (i in 0 until 3) { + dictionary.add(i.toString()) + } + + data.loopUntilMaxPower() + + when (data.bits) { + 0 -> { + data.bits = 0 + data.maxPower = 1 shl 8 + data.power = 1 + data.loopUntilMaxPower() + c = data.bits.toChar() + } + 1 -> { + data.bits = 0 + data.maxPower = 1 shl 16 + data.power = 1 + data.loopUntilMaxPower() + c = data.bits.toChar() + } + 2 -> throw IllegalArgumentException("Invalid LZString") + } + + if (c == null) { + throw Exception("No character found") + } + + dictionary.add(c.toString()) + var w = c.toString() + result.append(c.toString()) + + while (true) { + if (data.index > length) { + throw IllegalArgumentException("Invalid LZString") + } + + data.bits = 0 + data.maxPower = 1 shl numBits + data.power = 1 + data.loopUntilMaxPower() + + var cc = data.bits + + when (data.bits) { + 0 -> { + data.bits = 0 + data.maxPower = 1 shl 8 + data.power = 1 + data.loopUntilMaxPower() + dictionary.add(data.bits.toChar().toString()) + cc = dictionary.size - 1 + enlargeIn-- + } + 1 -> { + data.bits = 0 + data.maxPower = 1 shl 16 + data.power = 1 + data.loopUntilMaxPower() + dictionary.add(data.bits.toChar().toString()) + cc = dictionary.size - 1 + enlargeIn-- + } + 2 -> return result.toString() + } + + if (enlargeIn == 0) { + enlargeIn = 1 shl numBits + numBits++ + } + + entry = if (cc < dictionary.size) { + dictionary[cc] + } else { + if (cc == dictionary.size) { + w + w[0] + } else { + throw Exception("Invalid LZString") + } + } + result.append(entry) + dictionary.add(w + entry[0]) + enlargeIn-- + w = entry + + if (enlargeIn == 0) { + enlargeIn = 1 shl numBits + numBits++ + } + } + } + + private const val base64KeyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + + fun compressToBase64(input: String): String = + compress(input, 6) { base64KeyStr[it].toString() }.let { + return when (it.length % 4) { + 0 -> it + 1 -> "$it===" + 2 -> "$it==" + 3 -> "$it=" + else -> throw IllegalStateException("Modulo of 4 should not exceed 3.") + } + } + + fun decompressFromBase64(input: String): String = + decompress(input.length, 32) { + base64KeyStr.indexOf(input[it]) + } +} + +private data class DecompressionContext( + val resetValue: Int, + val getNextValue: getNextValueFn, + var value: Int = getNextValue(0), + var position: Int = resetValue, + var index: Int = 1, + var bits: Int = 0, + var maxPower: Int = 1 shl 2, + var power: Int = 1, +) { + fun loopUntilMaxPower() { + while (power != maxPower) { + val resb = value and position + + position = position shr 1 + + if (position == 0) { + position = resetValue + value = getNextValue(index++) + } + + bits = bits or ((if (resb > 0) 1 else 0) * power) + power = power shl 1 + } + } +} + +private data class CompressionContext( + val uncompressedLength: Int, + val bitsPerChar: Int, + val getCharFromInt: getCharFromIntFn, + var value: Int = 0, + val dictionary: MutableMap = HashMap(), + val dictionaryToCreate: MutableMap = HashMap(), + var c: String = "", + var wc: String = "", + var w: String = "", + var enlargeIn: Int = 2, // Compensate for the first entry which should not count + var dictSize: Int = 3, + var numBits: Int = 2, + val data: StringBuilder = StringBuilder(uncompressedLength / 3), + var dataVal: Int = 0, + var dataPosition: Int = 0, +) { + fun appendDataOrAdvancePosition() { + if (dataPosition == bitsPerChar - 1) { + dataPosition = 0 + data.append(getCharFromInt(dataVal)) + dataVal = 0 + } else { + dataPosition++ + } + } + + fun decrementEnlargeIn() { + enlargeIn-- + if (enlargeIn == 0) { + enlargeIn = 1 shl numBits + numBits++ + } + } + + // Output the code for W. + fun outputCodeForW() { + if (dictionaryToCreate.containsKey(w)) { + if (w[0].code < 256) { + for (i in 0 until numBits) { + dataVal = dataVal shl 1 + appendDataOrAdvancePosition() + } + + value = w[0].code + + for (i in 0 until 8) { + dataVal = (dataVal shl 1) or (value and 1) + appendDataOrAdvancePosition() + value = value shr 1 + } + } else { + value = 1 + + for (i in 0 until numBits) { + dataVal = (dataVal shl 1) or value + appendDataOrAdvancePosition() + value = 0 + } + + value = w[0].code + + for (i in 0 until 16) { + dataVal = (dataVal shl 1) or (value and 1) + appendDataOrAdvancePosition() + value = value shr 1 + } + } + + decrementEnlargeIn() + dictionaryToCreate.remove(w) + } else { + value = dictionary[w]!! + + for (i in 0 until numBits) { + dataVal = (dataVal shl 1) or (value and 1) + appendDataOrAdvancePosition() + value = value shr 1 + } + } + } +} diff --git a/src/en/mangafun/AndroidManifest.xml b/src/en/mangafun/AndroidManifest.xml new file mode 100644 index 000000000..5085f6751 --- /dev/null +++ b/src/en/mangafun/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/en/mangafun/build.gradle b/src/en/mangafun/build.gradle new file mode 100644 index 000000000..a2ed1f945 --- /dev/null +++ b/src/en/mangafun/build.gradle @@ -0,0 +1,13 @@ +ext { + extName = "Manga Fun" + extClass = ".MangaFun" + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation("net.pearx.kasechange:kasechange:1.4.1") + implementation(project(':lib:lzstring')) +} diff --git a/src/en/mangafun/res/mipmap-hdpi/ic_launcher.png b/src/en/mangafun/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db3a210268c3aa3bec47d21539b479fe6df1f829 GIT binary patch literal 3445 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 LNkvXXu0mjfUpFJp literal 0 HcmV?d00001 diff --git a/src/en/mangafun/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangafun/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..faa20eef2a73a6bbd075802989fed036314838c9 GIT binary patch literal 9489 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 decodeBool(content) + "n|" -> decodeNum(content) + "o|" -> decodeObject(values, content) + "a|" -> decodeArray(values, content) + else -> v + } + } + + throw IllegalArgumentException("Unknown data type") + } + + private fun decodeObject(values: JsonArray, s: String): JsonObject { + if (s == "o|") { + return JsonObject(emptyMap()) + } + + val vs = s.split("|") + val keyId = vs[1] + val keys = decode(values, keyId) + val n = vs.size + + val keyArray = try { + keys.jsonArray.map { it.jsonPrimitive.content } + } catch (_: IllegalArgumentException) { + // single-key object using existing value as key + listOf(keys.jsonPrimitive.content) + } + + return buildJsonObject { + for (i in 2 until n) { + val k = keyArray[i - 2] + val v = decode(values, vs[i]) + put(k, v) + } + } + } + + private fun decodeArray(values: JsonArray, s: String): JsonArray { + if (s == "a|") { + return JsonArray(emptyList()) + } + + val vs = s.split("|") + val n = vs.size - 1 + return buildJsonArray { + for (i in 0 until n) { + add(decode(values, vs[i + 1])) + } + } + } + + private fun decodeBool(s: String): JsonPrimitive { + return when (s) { + "b|T" -> JsonPrimitive(true) + "b|F" -> JsonPrimitive(false) + else -> JsonPrimitive(s.isNotEmpty()) + } + } + + private fun decodeNum(s: String): JsonPrimitive = + JsonPrimitive(sToInt(s.substringAfter("n|"))) + + private fun sToInt(s: String): Int { + var acc = 0 + var pow = 1 + + s.reversed().forEach { + acc += stoi[it]!! * pow + pow *= 62 + } + + return acc + } + + private val itos = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + private val stoi = itos.associate { + it to itos.indexOf(it) + } +} diff --git a/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFun.kt b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFun.kt new file mode 100644 index 000000000..bfe19fe49 --- /dev/null +++ b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFun.kt @@ -0,0 +1,296 @@ +package eu.kanade.tachiyomi.extension.en.mangafun + +import android.util.Base64 +import android.util.Log +import eu.kanade.tachiyomi.extension.en.mangafun.MangaFunUtils.toSChapter +import eu.kanade.tachiyomi.extension.en.mangafun.MangaFunUtils.toSManga +import eu.kanade.tachiyomi.lib.lzstring.LZString +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import kotlin.math.min + +class MangaFun : HttpSource() { + + override val name = "Manga Fun" + + override val baseUrl = "https://mangafun.me" + + private val apiUrl = "https://a.mangafun.me/v0" + + override val lang = "en" + + override val supportsLatest = true + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("Origin", baseUrl) + + private val json: Json by injectLazy() + + private val nextBuildId by lazy { + val document = client.newCall(GET(baseUrl, headers)).execute().asJsoup() + + json.parseToJsonElement( + document.selectFirst("#__NEXT_DATA__")!!.data(), + ) + .jsonObject["buildId"]!! + .jsonPrimitive + .content + } + + private lateinit var directory: List + + override fun fetchPopularManga(page: Int): Observable { + return if (page == 1) { + client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { popularMangaParse(it) } + } else { + Observable.just(parseDirectory(page)) + } + } + + override fun popularMangaRequest(page: Int) = GET("$apiUrl/title/all", headers) + + override fun popularMangaParse(response: Response): MangasPage { + directory = response.parseAs>() + .sortedBy { it.rank } + return parseDirectory(1) + } + + override fun fetchLatestUpdates(page: Int): Observable { + return if (page == 1) { + client.newCall(latestUpdatesRequest(page)) + .asObservableSuccess() + .map { latestUpdatesParse(it) } + } else { + Observable.just(parseDirectory(page)) + } + } + + override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page) + + override fun latestUpdatesParse(response: Response): MangasPage { + directory = response.parseAs>() + .sortedByDescending { MangaFunUtils.convertShortTime(it.updatedAt) } + return parseDirectory(1) + } + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { + return if (query.startsWith(PREFIX_ID_SEARCH)) { + val slug = query.removePrefix(PREFIX_ID_SEARCH) + return fetchMangaDetails(SManga.create().apply { url = "/title/$slug" }) + .map { MangasPage(listOf(it), false) } + } else if (page == 1) { + client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { searchMangaParse(it, query, filters) } + } else { + Observable.just(parseDirectory(page)) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = + popularMangaRequest(page) + + override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() + + private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage { + directory = response.parseAs>() + .filter { + it.name.contains(query, false) || + it.alias.any { a -> a.contains(query, false) } + } + + filters.ifEmpty { getFilterList() }.forEach { filter -> + when (filter) { + is GenreFilter -> { + val included = mutableListOf() + val excluded = mutableListOf() + + filter.state.forEach { g -> + when (g.state) { + Filter.TriState.STATE_INCLUDE -> included.add(g.id) + Filter.TriState.STATE_EXCLUDE -> excluded.add(g.id) + } + } + + if (included.isNotEmpty()) { + directory = directory + .filter { it.genres.any { g -> included.contains(g) } } + } + + if (excluded.isNotEmpty()) { + directory = directory + .filterNot { it.genres.any { g -> excluded.contains(g) } } + } + } + is TypeFilter -> { + val included = mutableListOf() + val excluded = mutableListOf() + + filter.state.forEach { g -> + when (g.state) { + Filter.TriState.STATE_INCLUDE -> included.add(g.id) + Filter.TriState.STATE_EXCLUDE -> excluded.add(g.id) + } + } + + if (included.isNotEmpty()) { + directory = directory + .filter { included.any { t -> it.titleType == t } } + } + + if (excluded.isNotEmpty()) { + directory = directory + .filterNot { excluded.any { t -> it.titleType == t } } + } + } + is StatusFilter -> { + val included = mutableListOf() + val excluded = mutableListOf() + + filter.state.forEach { g -> + when (g.state) { + Filter.TriState.STATE_INCLUDE -> included.add(g.id) + Filter.TriState.STATE_EXCLUDE -> excluded.add(g.id) + } + } + + if (included.isNotEmpty()) { + directory = directory + .filter { included.any { t -> it.publishedStatus == t } } + } + + if (excluded.isNotEmpty()) { + directory = directory + .filterNot { excluded.any { t -> it.publishedStatus == t } } + } + } + is SortFilter -> { + directory = when (filter.state?.index) { + 0 -> directory.sortedBy { it.name } + 1 -> directory.sortedBy { it.rank } + 2 -> directory.sortedBy { MangaFunUtils.convertShortTime(it.createdAt) } + 3 -> directory.sortedBy { MangaFunUtils.convertShortTime(it.updatedAt) } + else -> throw IllegalStateException("Unhandled sort option") + } + + if (filter.state?.ascending != true) { + directory = directory.reversed() + } + } + else -> {} + } + } + + return parseDirectory(1) + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}" + + override fun mangaDetailsRequest(manga: SManga): Request { + val slug = manga.url.substringAfterLast("/") + val nextDataUrl = "$baseUrl/_next/data/$nextBuildId/title/$slug.json" + + return GET(nextDataUrl, headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val data = response.parseAs() + .pageProps + .dehydratedState + .queries + .first() + .state + .data + + return json.decodeFromJsonElement(data).toSManga() + } + + override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + val data = response.parseAs() + .pageProps + .dehydratedState + .queries + .first() + .state + .data + + val mangaData = json.decodeFromJsonElement(data) + return mangaData.chapters.map { it.toSChapter(mangaData.id, mangaData.name) }.reversed() + } + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}" + + override fun pageListRequest(chapter: SChapter): Request { + val chapterId = chapter.url.substringAfterLast("/").substringBefore("-") + + return GET("$apiUrl/chapter/$chapterId", headers) + } + + override fun pageListParse(response: Response): List { + val encoded = Base64.encode(response.body.bytes(), Base64.DEFAULT or Base64.NO_WRAP).toString(Charsets.UTF_8) + val decoded = LZString.decompressFromBase64(encoded) + val compressedJson = json.parseToJsonElement(decoded).jsonArray + val decompressedJson = DecompressJson.decompress(compressedJson).jsonObject + + Log.d("MangaFun", Json.encodeToString(decompressedJson)) + + return decompressedJson.jsonObject["p"]!!.jsonArray.mapIndexed { i, it -> + Page(i, imageUrl = MangaFunUtils.getImageUrlFromHash(it.jsonArray[0].jsonPrimitive.content)) + } + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + override fun getFilterList() = FilterList( + GenreFilter(), + TypeFilter(), + StatusFilter(), + SortFilter(), + ) + + private fun parseDirectory(page: Int): MangasPage { + val endRange = min((page * 24), directory.size) + val manga = directory.subList(((page - 1) * 24), endRange).map { it.toSManga() } + val hasNextPage = endRange < directory.lastIndex + + return MangasPage(manga, hasNextPage) + } + + private inline fun Response.parseAs(): T = + json.decodeFromString(body.string()) + + companion object { + internal const val PREFIX_ID_SEARCH = "id:" + internal const val MANGAFUN_EPOCH = 1693473000 + } +} diff --git a/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunDto.kt b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunDto.kt new file mode 100644 index 000000000..c4bcbc31e --- /dev/null +++ b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunDto.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.extension.en.mangafun + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class MinifiedMangaDto( + @SerialName("i") val id: Int, + @SerialName("n") val name: String, + @SerialName("t") val thumbnailUrl: String? = null, + @SerialName("s") val publishedStatus: Int = 0, + @SerialName("tt") val titleType: Int = 0, + @SerialName("a") val alias: List = emptyList(), + @SerialName("g") val genres: List = emptyList(), + @SerialName("au") val author: List = emptyList(), + @SerialName("r") val rank: Int = 999999999, + @SerialName("ca") val createdAt: Int = 0, + @SerialName("ua") val updatedAt: Int = 0, +) + +@Serializable +data class MangaDto( + val id: Int, + val name: String, + val thumbnailURL: String? = null, + val publishedStatus: Int = 0, + val titleType: Int = 0, + val alias: List, + val description: String, + val genres: List, + val artist: List, + val author: List, + val chapters: List, +) + +@Serializable +data class ChapterDto( + val id: Int, + val name: String, + val publishedAt: String, +) + +@Serializable +data class GenreDto(val id: Int, val name: String) + +@Serializable +data class NextPagePropsWrapperDto( + val pageProps: NextPagePropsDto, +) + +@Serializable +data class NextPagePropsDto( + val dehydratedState: DehydratedStateDto, +) + +@Serializable +data class DehydratedStateDto( + val queries: List, +) + +@Serializable +data class QueriesDto( + val state: StateDto, +) + +@Serializable +data class StateDto( + val data: JsonElement, +) diff --git a/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunFilters.kt b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunFilters.kt new file mode 100644 index 000000000..02f1c5be4 --- /dev/null +++ b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunFilters.kt @@ -0,0 +1,149 @@ +package eu.kanade.tachiyomi.extension.en.mangafun + +import eu.kanade.tachiyomi.source.model.Filter + +class GenreFilter : Filter.Group("Genre", genreList) + +class TypeFilter : Filter.Group("Type", titleTypeList) + +class StatusFilter : Filter.Group( + "Status", + listOf("Ongoing", "Completed", "Hiatus", "Cancelled").mapIndexed { i, it -> Genre(it, i) }, +) + +class SortFilter : Filter.Sort( + "Order by", + arrayOf("Name", "Rank", "Newest", "Update"), + Selection(1, false), +) + +class Genre(name: String, val id: Int) : Filter.TriState(name) + +val genresMap by lazy { + genreList.associate { it.id to it.name } +} + +val titleTypeMap by lazy { + titleTypeList.associate { it.id to it.name } +} + +val titleTypeList by lazy { + listOf( + Genre("Manga", 0), + Genre("Manhwa", 1), + Genre("Manhua", 2), + Genre("Comic", 3), + Genre("Webtoon", 4), + Genre("One Shot", 6), + Genre("Doujinshi", 7), + Genre("Other", 8), + ) +} + +val genreList by lazy { + listOf( + Genre("Supernatural", 1), + Genre("Action", 2), + Genre("Comedy", 3), + Genre("Josei", 4), + Genre("Martial Arts", 5), + Genre("Romance", 6), + Genre("Ecchi", 7), + Genre("Harem", 8), + Genre("School Life", 9), + Genre("Seinen", 10), + Genre("Adventure", 11), + Genre("Fantasy", 12), + Genre("Demons", 13), + Genre("Magic", 14), + Genre("Military", 15), + Genre("Shounen", 16), + Genre("Shoujo", 17), + Genre("Psychological", 18), + Genre("Drama", 19), + Genre("Mystery", 20), + Genre("Sci-Fi", 21), + Genre("Slice of Life", 22), + Genre("Doujinshi", 23), + Genre("Police", 24), + Genre("Mecha", 25), + Genre("Yaoi", 26), + Genre("Horror", 27), + Genre("Historical", 28), + Genre("Thriller", 29), + Genre("Shounen Ai", 30), + Genre("Game", 31), + Genre("Gender Bender", 32), + Genre("Sports", 33), + Genre("Yuri", 34), + Genre("Music", 35), + Genre("Shoujo Ai", 36), + Genre("Vampires", 37), + Genre("Parody", 38), + Genre("Kids", 40), + Genre("Super Power", 41), + Genre("Space", 43), + Genre("Adult", 46), + Genre("Webtoons", 47), + Genre("Mature", 48), + Genre("Smut", 49), + Genre("Tragedy", 51), + Genre("One Shot", 53), + Genre("4-koma", 56), + Genre("Isekai", 58), + Genre("Food", 60), + Genre("Crime", 63), + Genre("Superhero", 67), + Genre("Animals", 69), + Genre("Manhwa", 74), + Genre("Manhua", 75), + Genre("Cooking", 78), + Genre("Medical", 79), + Genre("Magical Girls", 88), + Genre("Monsters", 89), + Genre("Shotacon", 90), + Genre("Philosophical", 91), + Genre("Wuxia", 92), + Genre("Adaptation", 95), + Genre("Full Color", 96), + Genre("Korean", 97), + Genre("Chinese", 98), + Genre("Reincarnation", 100), + Genre("Manga", 102), + Genre("Comic", 104), + Genre("Japanese", 105), + Genre("Time Travel", 108), + Genre("Erotica", 111), + Genre("Survival", 114), + Genre("Gore", 118), + Genre("Monster Girls", 120), + Genre("Dungeons", 123), + Genre("System", 124), + Genre("Cultivation", 125), + Genre("Murim", 128), + Genre("Suggestive", 131), + Genre("Fighting", 134), + Genre("Blood", 140), + Genre("Op-Mc", 142), + Genre("Revenge", 144), + Genre("Overpowered", 146), + Genre("Returner", 150), + Genre("Office", 152), + Genre("Loli", 163), + Genre("Video Games", 173), + Genre("Monster", 199), + Genre("Mafia", 203), + Genre("Anthology", 206), + Genre("Villainess", 207), + Genre("Aliens", 213), + Genre("Zombies", 216), + Genre("Violence", 217), + Genre("Delinquents", 219), + Genre("Post apocalyptic", 255), + Genre("Ghost", 260), + Genre("Virtual Reality", 263), + Genre("Cheat", 324), + Genre("Girls", 374), + Genre("Gender Swap", 384), + ) +} diff --git a/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUrlActivity.kt b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUrlActivity.kt new file mode 100644 index 000000000..ffbdad214 --- /dev/null +++ b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUrlActivity.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.extension.en.mangafun + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class MangaFunUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + try { + startActivity( + Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${MangaFun.PREFIX_ID_SEARCH}${pathSegments[1]}") + putExtra("filter", packageName) + }, + ) + } catch (e: ActivityNotFoundException) { + Log.e("MangaFunUrlActivity", "Could not start activity", e) + } + } else { + Log.e("MangaFunUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUtils.kt b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUtils.kt new file mode 100644 index 000000000..5ede3d2c6 --- /dev/null +++ b/src/en/mangafun/src/eu/kanade/tachiyomi/extension/en/mangafun/MangaFunUtils.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.extension.en.mangafun + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import net.pearx.kasechange.toKebabCase +import java.text.SimpleDateFormat +import java.util.Locale + +object MangaFunUtils { + private const val cdnUrl = "https://mimg.bid" + + private val notAlnumRegex = Regex("""[^0-9A-Za-z\s]""") + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT) + + private fun String.slugify(): String = + this.replace(notAlnumRegex, "").toKebabCase() + + private fun publishedStatusToStatus(ps: Int) = when (ps) { + 0 -> SManga.ONGOING + 1 -> SManga.COMPLETED + 2 -> SManga.ON_HIATUS + 3 -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + + fun convertShortTime(value: Int): Int { + return if (value < MangaFun.MANGAFUN_EPOCH) { + value + MangaFun.MANGAFUN_EPOCH + } else { + value + } + } + + fun getImageUrlFromHash(hash: String?): String? { + if (hash == null) { + return null + } + + return "$cdnUrl/${hash.substring(0, 2)}/${hash.substring(2, 5)}/${hash.substring(5)}.webp" + } + + fun MinifiedMangaDto.toSManga() = SManga.create().apply { + url = "/title/$id-${name.slugify()}" + title = name + author = this@toSManga.author.joinToString() + thumbnail_url = getImageUrlFromHash(thumbnailUrl) + status = publishedStatusToStatus(publishedStatus) + genre = buildList { + titleTypeMap[titleType]?.let { add(it) } + addAll(genres.mapNotNull { genresMap[it] }) + }.joinToString() + } + + fun MangaDto.toSManga() = SManga.create().apply { + url = "/title/$id-${name.slugify()}" + title = name + author = this@toSManga.author.filterNotNull().joinToString() + artist = this@toSManga.artist.filterNotNull().joinToString() + description = this@toSManga.description + genre = genres.mapNotNull { genresMap[it.id] }.joinToString() + status = publishedStatusToStatus(publishedStatus) + thumbnail_url = thumbnailURL + genre = buildList { + titleTypeMap[titleType]?.let { add(it) } + addAll(genres.mapNotNull { genresMap[it.id] }) + }.joinToString() + } + + fun ChapterDto.toSChapter(mangaId: Int, mangaName: String) = SChapter.create().apply { + url = "/title/$mangaId-${mangaName.slugify()}/$id-${this@toSChapter.name.slugify()}" + name = this@toSChapter.name + date_upload = runCatching { + dateFormat.parse(publishedAt)!!.time + }.getOrDefault(0L) + } +} diff --git a/src/zh/manhuagui/build.gradle b/src/zh/manhuagui/build.gradle index 66a65355f..292c910d0 100644 --- a/src/zh/manhuagui/build.gradle +++ b/src/zh/manhuagui/build.gradle @@ -1,7 +1,12 @@ ext { extName = 'ManHuaGui' extClass = '.Manhuagui' - extVersionCode = 19 + extVersionCode = 20 } apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:lzstring")) + implementation(project(":lib:unpacker")) +} diff --git a/src/zh/manhuagui/src/eu/kanade/tachiyomi/extension/zh/manhuagui/Manhuagui.kt b/src/zh/manhuagui/src/eu/kanade/tachiyomi/extension/zh/manhuagui/Manhuagui.kt index c9e7f15f4..ef017935b 100644 --- a/src/zh/manhuagui/src/eu/kanade/tachiyomi/extension/zh/manhuagui/Manhuagui.kt +++ b/src/zh/manhuagui/src/eu/kanade/tachiyomi/extension/zh/manhuagui/Manhuagui.kt @@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.extension.zh.manhuagui import android.app.Application import android.content.SharedPreferences -import app.cash.quickjs.QuickJs +import eu.kanade.tachiyomi.lib.lzstring.LZString +import eu.kanade.tachiyomi.lib.unpacker.Unpacker import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess @@ -295,19 +296,13 @@ class Manhuagui( if (hiddenEncryptedChapterList != null) { if (getShowR18()) { // Hidden chapter list is LZString encoded - val decodedHiddenChapterList = QuickJs.create().use { - it.evaluate( - jsDecodeFunc + - """LZString.decompressFromBase64('${hiddenEncryptedChapterList.`val`()}');""", - ) as String - } + val decodedHiddenChapterList = LZString.decompressFromBase64(hiddenEncryptedChapterList.`val`()) val hiddenChapterList = Jsoup.parse(decodedHiddenChapterList, response.request.url.toString()) - if (hiddenChapterList != null) { - // Replace R18 warning with actual chapter list - document.select("#erroraudit_show").first()!!.replaceWith(hiddenChapterList) - // Remove hidden chapter list element - document.select("#__VIEWSTATE").first()!!.remove() - } + + // Replace R18 warning with actual chapter list + document.select("#erroraudit_show").first()!!.replaceWith(hiddenChapterList) + // Remove hidden chapter list element + document.select("#__VIEWSTATE").first()!!.remove() } else { // "You need to enable R18 switch and restart Tachiyomi to read this manga" error("您需要打开R18作品显示开关并重启软件才能阅读此作品") @@ -372,22 +367,18 @@ class Manhuagui( return manga } - private val jsDecodeFunc = - """ - var LZString=(function(){var f=String.fromCharCode;var keyStrBase64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var baseReverseDic={};function getBaseValue(alphabet,character){if(!baseReverseDic[alphabet]){baseReverseDic[alphabet]={};for(var i=0;i>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}switch(next=bits){case 0:bits=0;maxpower=Math.pow(2,8);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}c=f(bits);break;case 1:bits=0;maxpower=Math.pow(2,16);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}c=f(bits);break;case 2:return""}dictionary[3]=c;w=c;result.push(c);while(true){if(data.index>length){return""}bits=0;maxpower=Math.pow(2,numBits);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}switch(c=bits){case 0:bits=0;maxpower=Math.pow(2,8);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}dictionary[dictSize++]=f(bits);c=dictSize-1;enlargeIn--;break;case 1:bits=0;maxpower=Math.pow(2,16);power=1;while(power!=maxpower){resb=data.val&data.position;data.position>>=1;if(data.position==0){data.position=resetValue;data.val=getNextValue(data.index++)}bits|=(resb>0?1:0)*power;power<<=1}dictionary[dictSize++]=f(bits);c=dictSize-1;enlargeIn--;break;case 2:return result.join('')}if(enlargeIn==0){enlargeIn=Math.pow(2,numBits);numBits++}if(dictionary[c]){entry=dictionary[c]}else{if(c===dictSize){entry=w+w.charAt(0)}else{return null}}result.push(entry);dictionary[dictSize++]=w+entry.charAt(0);enlargeIn--;w=entry;if(enlargeIn==0){enlargeIn=Math.pow(2,numBits);numBits++}}}};return LZString})();String.prototype.splic=function(f){return LZString.decompressFromBase64(this).split(f)}; - """ - - // Page list is javascript eval encoded and LZString encoded, these website: - // http://www.oicqzone.com/tool/eval/ , https://www.w3xue.com/tools/jseval/ , - // https://www.w3cschool.cn/tools/index?name=evalencode can try to decode javascript eval encoded content, - // jsDecodeFunc's LZString.decompressFromBase64() can decode LZString. - + // Page list is inside [packed](http://dean.edwards.name/packer/) JavaScript with a special twist: + // the normal content array (`'a|b|c'.split('|')`) is replaced with LZString and base64-encoded + // version. + // // These "\" can't be remove: "\}", more info in pull request 3926. @Suppress("RegExpRedundantEscape") - private val re = Regex("""window\[".*?"\](\(.*\)\s*\{[\s\S]+\}\s*\(.*\))""") + private val packedRegex = Regex("""window\[".*?"\](\(.*\)\s*\{[\s\S]+\}\s*\(.*\))""") @Suppress("RegExpRedundantEscape") - private val re2 = Regex("""\{.*\}""") + private val blockCcArgRegex = Regex("""\{.*\}""") + + private val packedContentRegex = Regex("""['"]([0-9A-Za-z+/=]+)['"]\[['"].*?['"]]\(['"].*?['"]\)""") override fun pageListParse(document: Document): List { // R18 warning element (#erroraudit_show) is remove by web page javascript, so here the warning element @@ -398,13 +389,19 @@ class Manhuagui( } val html = document.html() - val imgCode = re.find(html)?.groups?.get(1)?.value - val imgDecode = QuickJs.create().use { - it.evaluate(jsDecodeFunc + imgCode) as String - } + val imgCode = packedRegex.find(html)!!.groupValues[1].let { + // Make the packed content normal again so :lib:unpacker can do its job + it.replace(packedContentRegex) { match -> + val lzs = match.groupValues[1] + val decoded = LZString.decompressFromBase64(lzs).replace("'", "\\'") - val imgJsonStr = re2.find(imgDecode)?.groups?.get(0)?.value - val imageJson: Comic = json.decodeFromString(imgJsonStr!!) + "'$decoded'.split('|')" + } + } + val imgDecode = Unpacker.unpack(imgCode) + + val imgJsonStr = blockCcArgRegex.find(imgDecode)!!.groupValues[0] + val imageJson: Comic = json.decodeFromString(imgJsonStr) return imageJson.files!!.mapIndexed { i, imgStr -> val imgurl = "${imageServer[0]}${imageJson.path}$imgStr?e=${imageJson.sl?.e}&m=${imageJson.sl?.m}"