MachineTranslations & Snowmtl: Fix the regex in the translator engine and add font size settings (#7465)

* Fix the regex in the translator engine and add font size settings

* Remove extra lines

* Remove rateLimit

* Remove init pref

* Use lazy statement in the snowmtl client

* Relax the exception and show pages without dialog

* Fix the translator's bad formatted response for some cases

* Change listener return to false

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Chopper 2025-02-03 11:12:57 -03:00 committed by Draff
parent e080f6fd1b
commit 0505a26934
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
13 changed files with 123 additions and 18 deletions

View File

@ -0,0 +1,4 @@
font_size_title=Font size
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
font_size_message=Font size changed to %s
default_font_size=Default

View File

@ -0,0 +1 @@
font_size_title=Tamaño de letra

View File

@ -0,0 +1 @@
font_size_title=Taille de la police

View File

@ -0,0 +1 @@
font_size_title=Ukuran font

View File

@ -0,0 +1 @@
font_size_title=Dimensione del carattere

View File

@ -0,0 +1,4 @@
font_size_title=Tamanho da fonte
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
font_size_message=Tamanho da fonte foi alterada para %s
default_font_size=Padrão

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3
dependencies {
api(project(":lib:i18n"))
}

View File

@ -1,9 +1,16 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Application
import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -15,10 +22,13 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -29,7 +39,7 @@ abstract class MachineTranslations(
override val name: String, override val name: String,
override val baseUrl: String, override val baseUrl: String,
val language: Language, val language: Language,
) : ParsedHttpSource() { ) : ParsedHttpSource(), ConfigurableSource {
override val supportsLatest = true override val supportsLatest = true
@ -37,9 +47,26 @@ abstract class MachineTranslations(
override val lang = language.lang override val lang = language.lang
override val client = network.cloudflareClient.newBuilder() private val preferences: SharedPreferences by lazy {
.addInterceptor(ComposedImageInterceptor(baseUrl, language)) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected var fontSize: Int
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
private val intl = Intl(
language = language.lang,
baseLanguage = "en",
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
override val client: OkHttpClient by lazy {
network.cloudflareClient.newBuilder()
.addInterceptor(ComposedImageInterceptor(baseUrl, language, fontSize))
.build() .build()
}
// ============================== Popular =============================== // ============================== Popular ===============================
@ -203,9 +230,49 @@ abstract class MachineTranslations(
return FilterList(filters) return FilterList(filters)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Some libreoffice font sizes
val sizes = arrayOf(
"24", "26", "28",
"32", "36", "40",
"42", "44", "48",
"54", "60", "72",
"80", "88", "96",
)
ListPreference(screen.context).apply {
key = FONT_SIZE_PREF
title = intl["font_size_title"]
entries = sizes.map {
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
}.toTypedArray()
entryValues = sizes
summary = intl["font_size_summary"]
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entries[index] as String
fontSize = selected.toInt()
Toast.makeText(
screen.context,
intl["font_size_message"].format(entry),
Toast.LENGTH_LONG,
).show()
false
}
}.also(screen::addPreference)
}
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)
const val PREFIX_SEARCH = "id:" const val PREFIX_SEARCH = "id:"
const val FONT_SIZE_PREF = "fontSizePref"
const val DEFAULT_FONT_SIZE = "24"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US) private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
} }
} }

View File

@ -26,7 +26,6 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@ -36,6 +35,7 @@ import kotlin.math.sqrt
class ComposedImageInterceptor( class ComposedImageInterceptor(
baseUrl: String, baseUrl: String,
val language: Language, val language: Language,
val fontSize: Int = 24,
) : Interceptor { ) : Interceptor {
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -55,7 +55,7 @@ class ComposedImageInterceptor(
} }
val dialogues = request.url.fragment?.parseAs<List<Dialog>>() val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
?: throw IOException("Dialogues not found") ?: emptyList()
val imageRequest = request.newBuilder() val imageRequest = request.newBuilder()
.url(url) .url(url)
@ -104,7 +104,7 @@ class ComposedImageInterceptor(
} }
private fun createTextPaint(font: Typeface?): TextPaint { private fun createTextPaint(font: Typeface?): TextPaint {
val defaultTextSize = 24.pt // arbitrary val defaultTextSize = fontSize.pt
return TextPaint().apply { return TextPaint().apply {
color = Color.BLACK color = Color.BLACK
textSize = defaultTextSize textSize = defaultTextSize

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@ -27,10 +28,12 @@ class Snowmtl(
private val translator: TranslatorEngine = BingTranslator(clientUtils, headers) private val translator: TranslatorEngine = BingTranslator(clientUtils, headers)
override val client = network.cloudflareClient.newBuilder() override val client: OkHttpClient by lazy {
.rateLimit(2) network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES) .readTimeout(2, TimeUnit.MINUTES)
.addInterceptor(TranslationInterceptor(language, translator)) .addInterceptor(TranslationInterceptor(language, translator))
.addInterceptor(ComposedImageInterceptor(baseUrl, language)) .addInterceptor(ComposedImageInterceptor(baseUrl, language, fontSize))
.build() .build()
}
} }

View File

@ -62,7 +62,20 @@ class TranslationInterceptor(
tokens: List<String>, tokens: List<String>,
mapping: Map<String, Pair<String, AssociatedDialog>>, mapping: Map<String, Pair<String, AssociatedDialog>>,
) = tokens.mapNotNull { token -> ) = tokens.mapNotNull { token ->
val list = token.decode().parseAs<List<String>>() val list = try {
token.decode().parseAs<List<String>>()
} catch (_: Exception) {
// The translator may return an invalid JSON, but it keeps the pattern sent.
TRANSLATOR_EXTRACT_REGEX.findAll(token).map {
listOf(
it.groups[1]?.value!!.encode(),
it.groups[3]?.value!!.let { dialog ->
dialog.takeIf { it.startsWith("\"") } ?: dialog.encode()
},
)
}.toList().flatten()
}
val key = list.first() val key = list.first()
val text = list.last() val text = list.last()
@ -102,7 +115,7 @@ class TranslationInterceptor(
private fun String.encode() = "\"${this}\"" private fun String.encode() = "\"${this}\""
private fun String.decode() = this.substringAfter("\"").substringBeforeLast("\"") private fun String.decode() = this.substringAfter("\"").substringBeforeLast("\"")
private val delimiter: String = "|" private val delimiter: String = "¦"
/** /**
* Tokenizes a list of texts based on the translator's character capacity per request * Tokenizes a list of texts based on the translator's character capacity per request
@ -137,6 +150,10 @@ class TranslationInterceptor(
private inline fun <reified T> String.parseAs(): T { private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this) return json.decodeFromString(this)
} }
companion object {
val TRANSLATOR_EXTRACT_REGEX = """"?(-?\d+)(\\?")?,((\\?")?([^(\])]+))""".toRegex()
}
} }
private class AssociatedDialog( private class AssociatedDialog(

View File

@ -97,8 +97,8 @@ class BingTranslator(private val client: OkHttpClient, private val headers: Head
val matchTwo = IG_PARAM_REGEX.find(scriptTwo)?.groups val matchTwo = IG_PARAM_REGEX.find(scriptTwo)?.groups
return TokenGroup( return TokenGroup(
token = matchOne?.get(2)?.value ?: "", token = matchOne?.get(4)?.value ?: "",
key = matchOne?.get(1)?.value ?: "", key = matchOne?.get(3)?.value ?: "",
ig = matchTwo?.get(1)?.value ?: "", ig = matchTwo?.get(1)?.value ?: "",
iid = document.selectFirst("div[data-iid]:not([class])")?.attr("data-iid") ?: "", iid = document.selectFirst("div[data-iid]:not([class])")?.attr("data-iid") ?: "",
) )

View File

@ -5,6 +5,7 @@ import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class Solarmtl( class Solarmtl(
@ -15,6 +16,7 @@ class Solarmtl(
language, language,
) { ) {
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.rateLimit(2) .rateLimit(2)
.build() .build()
} }