mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-24 19:32:41 +01:00
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
This commit is contained in:
parent
df859e3604
commit
eb6d05a54f
12
lib/lzstring/build.gradle.kts
Normal file
12
lib/lzstring/build.gradle.kts
Normal file
@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.kotlin.stdlib)
|
||||
}
|
@ -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<String>()
|
||||
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<String, Int> = HashMap(),
|
||||
val dictionaryToCreate: MutableMap<String, Boolean> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
src/en/mangafun/AndroidManifest.xml
Normal file
21
src/en/mangafun/AndroidManifest.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity android:name=".en.mangafun.MangaFunUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="mangafun.me"
|
||||
android:pathPattern="/title/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
13
src/en/mangafun/build.gradle
Normal file
13
src/en/mangafun/build.gradle
Normal file
@ -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'))
|
||||
}
|
BIN
src/en/mangafun/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/mangafun/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
src/en/mangafun/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/mangafun/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/en/mangafun/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/mangafun/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
src/en/mangafun/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangafun/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
BIN
src/en/mangafun/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangafun/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,134 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangafun
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
/**
|
||||
* A somewhat direct port of the decoding parts of
|
||||
* [compress-json](https://github.com/beenotung/compress-json).
|
||||
*/
|
||||
object DecompressJson {
|
||||
fun decompress(c: JsonArray): JsonElement {
|
||||
val values = c[0].jsonArray
|
||||
val key = c[1].jsonPrimitive.content
|
||||
|
||||
return decode(values, key)
|
||||
}
|
||||
|
||||
private fun decode(values: JsonArray, key: String): JsonElement {
|
||||
if (key.isEmpty() || key == "_") {
|
||||
return JsonPrimitive(null)
|
||||
}
|
||||
|
||||
val id = sToInt(key)
|
||||
val v = values[id]
|
||||
|
||||
try {
|
||||
v.jsonNull
|
||||
return v
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// v is not null, we continue on.
|
||||
}
|
||||
|
||||
val vNum = v.jsonPrimitive.intOrNull
|
||||
|
||||
if (vNum != null) {
|
||||
return v
|
||||
}
|
||||
|
||||
if (v.jsonPrimitive.isString) {
|
||||
val content = v.jsonPrimitive.content
|
||||
|
||||
if (content.length < 2) {
|
||||
return v
|
||||
}
|
||||
|
||||
return when (content.substring(0..1)) {
|
||||
"b|" -> 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)
|
||||
}
|
||||
}
|
@ -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<MinifiedMangaDto>
|
||||
|
||||
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 popularMangaRequest(page: Int) = GET("$apiUrl/title/all", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
directory = response.parseAs<List<MinifiedMangaDto>>()
|
||||
.sortedBy { it.rank }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
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<List<MinifiedMangaDto>>()
|
||||
.sortedByDescending { MangaFunUtils.convertShortTime(it.updatedAt) }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
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<List<MinifiedMangaDto>>()
|
||||
.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<Int>()
|
||||
val excluded = mutableListOf<Int>()
|
||||
|
||||
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<Int>()
|
||||
val excluded = mutableListOf<Int>()
|
||||
|
||||
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<Int>()
|
||||
val excluded = mutableListOf<Int>()
|
||||
|
||||
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<NextPagePropsWrapperDto>()
|
||||
.pageProps
|
||||
.dehydratedState
|
||||
.queries
|
||||
.first()
|
||||
.state
|
||||
.data
|
||||
|
||||
return json.decodeFromJsonElement<MangaDto>(data).toSManga()
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = response.parseAs<NextPagePropsWrapperDto>()
|
||||
.pageProps
|
||||
.dehydratedState
|
||||
.queries
|
||||
.first()
|
||||
.state
|
||||
.data
|
||||
|
||||
val mangaData = json.decodeFromJsonElement<MangaDto>(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<Page> {
|
||||
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 <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString(body.string())
|
||||
|
||||
companion object {
|
||||
internal const val PREFIX_ID_SEARCH = "id:"
|
||||
internal const val MANGAFUN_EPOCH = 1693473000
|
||||
}
|
||||
}
|
@ -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<String> = emptyList(),
|
||||
@SerialName("g") val genres: List<Int> = emptyList(),
|
||||
@SerialName("au") val author: List<String> = 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<String>,
|
||||
val description: String,
|
||||
val genres: List<GenreDto>,
|
||||
val artist: List<String?>,
|
||||
val author: List<String?>,
|
||||
val chapters: List<ChapterDto>,
|
||||
)
|
||||
|
||||
@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<QueriesDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QueriesDto(
|
||||
val state: StateDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StateDto(
|
||||
val data: JsonElement,
|
||||
)
|
@ -0,0 +1,149 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangafun
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class GenreFilter : Filter.Group<Genre>("Genre", genreList)
|
||||
|
||||
class TypeFilter : Filter.Group<Genre>("Type", titleTypeList)
|
||||
|
||||
class StatusFilter : Filter.Group<Genre>(
|
||||
"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),
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
@ -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<alphabet.length;i++){baseReverseDic[alphabet][alphabet.charAt(i)]=i}}return baseReverseDic[alphabet][character]}var LZString={decompressFromBase64:function(input){if(input==null)return"";if(input=="")return null;return LZString._0(input.length,32,function(index){return getBaseValue(keyStrBase64,input.charAt(index))})},_0:function(length,resetValue,getNextValue){var dictionary=[],next,enlargeIn=4,dictSize=4,numBits=3,entry="",result=[],i,w,bits,resb,maxpower,power,c,data={val:getNextValue(0),position:resetValue,index:1};for(i=0;i<3;i+=1){dictionary[i]=i}bits=0;maxpower=Math.pow(2,2);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(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<Page> {
|
||||
// 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}"
|
||||
|
Loading…
Reference in New Issue
Block a user