Manhuarm Fixes (#11342)

* Manhuarm.kt

* ManhuarmDto.kt

fixed error bad base64 and chapters not loading for some entries

* Update Manhuarm.kt

Improved nonce extraction by scanning <script> tags for const nonce and adding a fallback value for better reliability.

* Manhuarm.kt

* Update build.gradle

* Update Manhuarm.kt

Fixed popular and latest not showing
Added more robust Nonce regex
Added Custom User Agent feature

* Update ManhuarmDto.kt

* Update build.gradle

Changed BaseUrl

* Update Manhuarm.kt

Changed Url

* Update build.gradle

* removed base64 method since it's not used anymore

* Updated new ocr data fetch

* changed ratelimit
This commit is contained in:
fadiajlil2099 2025-11-12 06:36:27 +01:00 committed by Draff
parent 9128e25848
commit 6bf6da4db8
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 111 additions and 49 deletions

View File

@ -3,7 +3,7 @@ ext {
extClass = '.ManhuarmFactory'
themePkg = 'madara'
baseUrl = 'https://manhuarmmtl.com'
overrideVersionCode = 4
overrideVersionCode = 5
isNsfw = true
}

View File

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
@ -16,19 +17,21 @@ import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.lib.i18n.Intl.Companion.createDefaultMessageFileName
import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.encodeToString
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit
@ -80,6 +83,10 @@ class Manhuarm(
get() = preferences.getBoolean(DISABLE_TRANSLATOR_PREF, language.disableTranslator)
set(value) = preferences.edit().putBoolean(DISABLE_TRANSLATOR_PREF, value).apply()
private var customUserAgent: String
get() = preferences.getString(CUSTOM_UA_PREF, "")!!
set(value) = preferences.edit().putString(CUSTOM_UA_PREF, value).apply()
private val i18n = Intl(
language = language.lang,
baseLanguage = "en",
@ -133,7 +140,7 @@ class Manhuarm(
return network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES)
.rateLimit(3)
.rateLimit(2, 1)
.addInterceptorIf(
!disableTranslator && language.lang != language.origin,
TranslationInterceptor(settings, translator),
@ -141,6 +148,15 @@ class Manhuarm(
.addInterceptor(ComposedImageInterceptor(settings))
}
override fun headersBuilder(): Headers.Builder {
val builder = super.headersBuilder()
val ua = customUserAgent.trim()
if (ua.isNotEmpty()) {
builder.set("User-Agent", ua)
}
return builder
}
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
}
@ -159,21 +175,14 @@ class Manhuarm(
override fun pageListParse(document: Document): List<Page> {
val pages = super.pageListParse(document)
val chapterId = document.selectFirst("#wp-manga-current-chap")!!.attr("data-id")
val nonce = document.selectFirst("#manga-ocr-display-script-js-extra")!!.data().let {
NONCE_REGEX.find(it)!!.groupValues.last()
}
val form = FormBody.Builder()
.add("action", "get_ocr_data")
.add("chapter_id", chapterId)
.add("nonce", nonce)
.build()
val dialogDto = client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, form))
val dialog = client.newCall(GET("$baseUrl/wp-content/uploads/ocr-data/$chapterId.json", headers))
.execute()
.parseAs<DialogDto>()
.parseAs<List<PageDto>>()
val dialog = dialogDto.content.parseAs<List<PageDto>>()
if (dialog.isEmpty()) {
return pages
}
return dialog.mapIndexed { index, dto ->
val page = pages.first { it.imageUrl?.contains(dto.imageUrl, true)!! }
@ -188,14 +197,54 @@ class Manhuarm(
}
}
private fun String.fixJsonFormat(): String {
return JSON_FORMAT_REGEX.replace(this) { matchResult ->
val content = matchResult.groupValues.last()
val modifiedContent = content.replace("\"", "'")
""""text": "${modifiedContent.trimIndent()}", "box""""
override fun popularMangaRequest(page: Int): okhttp3.Request {
val url = if (page == 1) {
"$baseUrl/manga/?m_orderby=trending"
} else {
"$baseUrl/manga/page/$page/?m_orderby=trending"
}
return GET(url, headers)
}
override fun popularMangaSelector(): String = ".page-item-detail, .manga-card"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
val titleEl = element.selectFirst(".post-title a, .manga-title a")
val thumbEl = element.selectFirst(".item-thumb img, .manga-thumb img, img")
manga.setUrlWithoutDomain(titleEl!!.attr("href"))
manga.title = titleEl.text()
manga.thumbnail_url = thumbEl?.absUrl("data-src") ?: thumbEl?.absUrl("src")
return manga
}
override fun popularMangaNextPageSelector(): String? = "a.next, a.nextpostslink, .pagination a.next, .navigation-ajax #navigation-ajax"
override fun latestUpdatesRequest(page: Int): okhttp3.Request {
val url = if (page == 1) {
"$baseUrl/manga/?m_orderby=latest"
} else {
"$baseUrl/manga/page/$page/?m_orderby=latest"
}
return GET(url, headers)
}
override fun latestUpdatesSelector(): String = ".page-item-detail, .manga-card"
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val titleEl = element.selectFirst(".manga-title a")
?: element.selectFirst(".post-title a, h3.h5 a, .post-title h3 a")
val thumbEl = element.selectFirst(".manga-thumb img")
?: element.selectFirst(".item-thumb img, img")
manga.setUrlWithoutDomain(titleEl!!.attr("href"))
manga.title = titleEl.text()
manga.thumbnail_url = thumbEl?.absUrl("src")
return manga
}
override fun latestUpdatesNextPageSelector(): String? = "a.next, a.nextpostslink, .pagination a.next, .navigation-ajax #navigation-ajax"
// Prevent bad fragments
fun String.toFragment(): String = "#${this.replace("#", "*")}"
@ -329,6 +378,17 @@ class Manhuarm(
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = CUSTOM_UA_PREF
title = "Custom User-Agent"
summary = "Set a custom User-Agent for requests. Leave blank to use the default."
setDefaultValue(customUserAgent)
setOnPreferenceChange { _, newValue ->
customUserAgent = (newValue as String).trim()
true
}
}.also(screen::addPreference)
if (language.target == language.origin) {
return
}
@ -391,8 +451,7 @@ class Manhuarm(
companion object {
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
val JSON_FORMAT_REGEX = """(?:"text":\s+?".*?)([\s\S]*?)(?:",\s+?"box")""".toRegex()
val NONCE_REGEX = """(?:nonce":")([^"]+)""".toRegex()
val NONCE_REGEX = """(?:const\s+nonce\s*=\s*'|\"nonce\"\s*:\s*\")(.*?)['\"]""".toRegex()
const val DEVICE_FONT = "device:"
private const val FONT_SIZE_PREF = "fontSizePref"
@ -401,6 +460,7 @@ class Manhuarm(
private const val DISABLE_WORD_BREAK_PREF = "disableWordBreakPref"
private const val DISABLE_TRANSLATOR_PREF = "disableTranslatorPref"
private const val TRANSLATOR_PROVIDER_PREF = "translatorProviderPref"
private const val CUSTOM_UA_PREF = "customUserAgentPref"
private const val DEFAULT_FONT_SIZE = "28"
}
}

View File

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.extension.all.manhuarm
import android.os.Build
import android.util.Base64
import androidx.annotation.RequiresApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
@ -21,23 +18,12 @@ import java.io.IOException
class PageDto(
@SerialName("image")
val imageUrl: String,
@SerialName("texts")
@Serializable(with = DialogListSerializer::class)
val dialogues: List<Dialog> = emptyList(),
)
@Serializable
class DialogDto(
private val data: String,
) {
val content: String by lazy {
Base64.decode(data, Base64.DEFAULT).toString(Charsets.UTF_8)
}
}
@Serializable
@RequiresApi(Build.VERSION_CODES.O)
data class Dialog(
val x: Float,
val y: Float,
@ -47,7 +33,6 @@ data class Dialog(
val textByLanguage: Map<String, String> = emptyMap(),
) {
var scale: Float = 1F
val height: Float get() = scale * _height
val width: Float get() = scale * _width
@ -62,18 +47,34 @@ data class Dialog(
private object DialogListSerializer :
JsonTransformingSerializer<List<Dialog>>(ListSerializer(Dialog.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray(
element.jsonArray.map { jsonElement ->
val coordinates = getCoordinates(jsonElement)
val textByLanguage = getDialogs(jsonElement)
buildJsonObject {
put("x", coordinates[0])
put("y", coordinates[1])
put("_width", coordinates[2])
put("_height", coordinates[3])
put("textByLanguage", textByLanguage)
override fun transformDeserialize(element: JsonElement): JsonElement {
if (element !is JsonArray) {
return JsonArray(emptyList())
}
if (element.jsonArray.isEmpty()) {
return JsonArray(emptyList())
}
return JsonArray(
element.jsonArray.mapNotNull { jsonElement ->
try {
val coordinates = getCoordinates(jsonElement) ?: return@mapNotNull null
val textByLanguage = getDialogs(jsonElement)
// Validate coordinates array has at least 4 elements
if (coordinates.size < 4) return@mapNotNull null
buildJsonObject {
put("x", coordinates[0])
put("y", coordinates[1])
put("_width", coordinates[2])
put("_height", coordinates[3])
put("textByLanguage", textByLanguage)
}
} catch (e: Exception) {
null
}
},
)
@ -86,6 +87,7 @@ private object DialogListSerializer :
?: throw IOException("Dialog box position not found")
}
}
private fun getDialogs(element: JsonElement): JsonObject {
return buildJsonObject {
when (element) {