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' extClass = '.ManhuarmFactory'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://manhuarmmtl.com' baseUrl = 'https://manhuarmmtl.com'
overrideVersionCode = 4 overrideVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceScreen 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.lib.i18n.Intl.Companion.createDefaultMessageFileName
import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine
import eu.kanade.tachiyomi.multisrc.madara.Madara 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.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs import keiyoushi.utils.parseAs
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import okhttp3.FormBody import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -80,6 +83,10 @@ class Manhuarm(
get() = preferences.getBoolean(DISABLE_TRANSLATOR_PREF, language.disableTranslator) get() = preferences.getBoolean(DISABLE_TRANSLATOR_PREF, language.disableTranslator)
set(value) = preferences.edit().putBoolean(DISABLE_TRANSLATOR_PREF, value).apply() 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( private val i18n = Intl(
language = language.lang, language = language.lang,
baseLanguage = "en", baseLanguage = "en",
@ -133,7 +140,7 @@ class Manhuarm(
return network.cloudflareClient.newBuilder() return network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES) .connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES) .readTimeout(2, TimeUnit.MINUTES)
.rateLimit(3) .rateLimit(2, 1)
.addInterceptorIf( .addInterceptorIf(
!disableTranslator && language.lang != language.origin, !disableTranslator && language.lang != language.origin,
TranslationInterceptor(settings, translator), TranslationInterceptor(settings, translator),
@ -141,6 +148,15 @@ class Manhuarm(
.addInterceptor(ComposedImageInterceptor(settings)) .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 { private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor) return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
} }
@ -159,21 +175,14 @@ class Manhuarm(
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val pages = super.pageListParse(document) val pages = super.pageListParse(document)
val chapterId = document.selectFirst("#wp-manga-current-chap")!!.attr("data-id") 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() val dialog = client.newCall(GET("$baseUrl/wp-content/uploads/ocr-data/$chapterId.json", headers))
.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))
.execute() .execute()
.parseAs<DialogDto>() .parseAs<List<PageDto>>()
val dialog = dialogDto.content.parseAs<List<PageDto>>() if (dialog.isEmpty()) {
return pages
}
return dialog.mapIndexed { index, dto -> return dialog.mapIndexed { index, dto ->
val page = pages.first { it.imageUrl?.contains(dto.imageUrl, true)!! } val page = pages.first { it.imageUrl?.contains(dto.imageUrl, true)!! }
@ -188,14 +197,54 @@ class Manhuarm(
} }
} }
private fun String.fixJsonFormat(): String { override fun popularMangaRequest(page: Int): okhttp3.Request {
return JSON_FORMAT_REGEX.replace(this) { matchResult -> val url = if (page == 1) {
val content = matchResult.groupValues.last() "$baseUrl/manga/?m_orderby=trending"
val modifiedContent = content.replace("\"", "'") } else {
""""text": "${modifiedContent.trimIndent()}", "box"""" "$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 // Prevent bad fragments
fun String.toFragment(): String = "#${this.replace("#", "*")}" fun String.toFragment(): String = "#${this.replace("#", "*")}"
@ -329,6 +378,17 @@ class Manhuarm(
} }
}.also(screen::addPreference) }.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) { if (language.target == language.origin) {
return return
} }
@ -391,8 +451,7 @@ class Manhuarm(
companion object { companion object {
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE) 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 = """(?:const\s+nonce\s*=\s*'|\"nonce\"\s*:\s*\")(.*?)['\"]""".toRegex()
val NONCE_REGEX = """(?:nonce":")([^"]+)""".toRegex()
const val DEVICE_FONT = "device:" const val DEVICE_FONT = "device:"
private const val FONT_SIZE_PREF = "fontSizePref" 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_WORD_BREAK_PREF = "disableWordBreakPref"
private const val DISABLE_TRANSLATOR_PREF = "disableTranslatorPref" private const val DISABLE_TRANSLATOR_PREF = "disableTranslatorPref"
private const val TRANSLATOR_PROVIDER_PREF = "translatorProviderPref" private const val TRANSLATOR_PROVIDER_PREF = "translatorProviderPref"
private const val CUSTOM_UA_PREF = "customUserAgentPref"
private const val DEFAULT_FONT_SIZE = "28" private const val DEFAULT_FONT_SIZE = "28"
} }
} }

View File

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.extension.all.manhuarm 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
@ -21,23 +18,12 @@ import java.io.IOException
class PageDto( class PageDto(
@SerialName("image") @SerialName("image")
val imageUrl: String, val imageUrl: String,
@SerialName("texts") @SerialName("texts")
@Serializable(with = DialogListSerializer::class) @Serializable(with = DialogListSerializer::class)
val dialogues: List<Dialog> = emptyList(), val dialogues: List<Dialog> = emptyList(),
) )
@Serializable @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( data class Dialog(
val x: Float, val x: Float,
val y: Float, val y: Float,
@ -47,7 +33,6 @@ data class Dialog(
val textByLanguage: Map<String, String> = emptyMap(), val textByLanguage: Map<String, String> = emptyMap(),
) { ) {
var scale: Float = 1F var scale: Float = 1F
val height: Float get() = scale * _height val height: Float get() = scale * _height
val width: Float get() = scale * _width val width: Float get() = scale * _width
@ -62,18 +47,34 @@ data class Dialog(
private object DialogListSerializer : private object DialogListSerializer :
JsonTransformingSerializer<List<Dialog>>(ListSerializer(Dialog.serializer())) { 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 { override fun transformDeserialize(element: JsonElement): JsonElement {
put("x", coordinates[0]) if (element !is JsonArray) {
put("y", coordinates[1]) return JsonArray(emptyList())
put("_width", coordinates[2]) }
put("_height", coordinates[3])
put("textByLanguage", textByLanguage) 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") ?: throw IOException("Dialog box position not found")
} }
} }
private fun getDialogs(element: JsonElement): JsonObject { private fun getDialogs(element: JsonElement): JsonObject {
return buildJsonObject { return buildJsonObject {
when (element) { when (element) {