diff --git a/src/all/manhuarm/build.gradle b/src/all/manhuarm/build.gradle index 72dd96a01..8c90e422d 100644 --- a/src/all/manhuarm/build.gradle +++ b/src/all/manhuarm/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.ManhuarmFactory' themePkg = 'madara' baseUrl = 'https://manhuarmmtl.com' - overrideVersionCode = 4 + overrideVersionCode = 5 isNsfw = true } diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt index 074fb8daa..f9835dc59 100644 --- a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt @@ -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 { 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() + .parseAs>() - val dialog = dialogDto.content.parseAs>() + 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" } } diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt index 02ebadbf8..066ec680c 100644 --- a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt @@ -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 = 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 = 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>(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) {