diff --git a/lib-multisrc/machinetranslations/AndroidManifest.xml b/lib-multisrc/machinetranslations/AndroidManifest.xml
new file mode 100644
index 000000000..dac6d158a
--- /dev/null
+++ b/lib-multisrc/machinetranslations/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/all/snowmtl/assets/fonts/LICENSE.txt b/lib-multisrc/machinetranslations/assets/fonts/LICENSE.txt
similarity index 100%
rename from src/all/snowmtl/assets/fonts/LICENSE.txt
rename to lib-multisrc/machinetranslations/assets/fonts/LICENSE.txt
diff --git a/src/all/snowmtl/assets/fonts/coming_soon_regular.ttf b/lib-multisrc/machinetranslations/assets/fonts/coming_soon_regular.ttf
similarity index 100%
rename from src/all/snowmtl/assets/fonts/coming_soon_regular.ttf
rename to lib-multisrc/machinetranslations/assets/fonts/coming_soon_regular.ttf
diff --git a/lib-multisrc/machinetranslations/build.gradle.kts b/lib-multisrc/machinetranslations/build.gradle.kts
new file mode 100644
index 000000000..dc076cc37
--- /dev/null
+++ b/lib-multisrc/machinetranslations/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("lib-multisrc")
+}
+
+baseVersionCode = 1
diff --git a/lib-multisrc/machinetranslations/res/mipmap-hdpi/ic_launcher.png b/lib-multisrc/machinetranslations/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..c61c9328c
Binary files /dev/null and b/lib-multisrc/machinetranslations/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/lib-multisrc/machinetranslations/res/mipmap-mdpi/ic_launcher.png b/lib-multisrc/machinetranslations/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..68cb9f512
Binary files /dev/null and b/lib-multisrc/machinetranslations/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/lib-multisrc/machinetranslations/res/mipmap-xhdpi/ic_launcher.png b/lib-multisrc/machinetranslations/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..15d3ccdf9
Binary files /dev/null and b/lib-multisrc/machinetranslations/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/lib-multisrc/machinetranslations/res/mipmap-xxhdpi/ic_launcher.png b/lib-multisrc/machinetranslations/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c7f54aaf0
Binary files /dev/null and b/lib-multisrc/machinetranslations/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/lib-multisrc/machinetranslations/res/mipmap-xxxhdpi/ic_launcher.png b/lib-multisrc/machinetranslations/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..ba00c8a5e
Binary files /dev/null and b/lib-multisrc/machinetranslations/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt
new file mode 100644
index 000000000..d74aec66f
--- /dev/null
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt
@@ -0,0 +1,211 @@
+package eu.kanade.tachiyomi.multisrc.machinetranslations
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
+import eu.kanade.tachiyomi.network.GET
+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.ParsedHttpSource
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+@RequiresApi(Build.VERSION_CODES.O)
+abstract class MachineTranslations(
+ override val name: String,
+ override val baseUrl: String,
+ val language: Language,
+) : ParsedHttpSource() {
+
+ override val supportsLatest = true
+
+ private val json: Json by injectLazy()
+
+ override val lang = language.lang
+
+ override val client = network.cloudflareClient.newBuilder()
+ .addInterceptor(ComposedImageInterceptor(baseUrl, language))
+ .build()
+
+ // ============================== Popular ===============================
+
+ private val popularFilter = FilterList(SelectionList("", listOf(Option(value = "views", query = "sort_by"))))
+
+ override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
+
+ override fun popularMangaSelector() = searchMangaSelector()
+
+ override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
+
+ override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
+
+ // =============================== Latest ===============================
+
+ private val latestFilter = FilterList(SelectionList("", listOf(Option(value = "recent", query = "sort_by"))))
+
+ override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
+
+ override fun latestUpdatesSelector() = searchMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
+
+ // =========================== Search ============================
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/search".toHttpUrl().newBuilder()
+ .addQueryParameter("page", page.toString())
+
+ if (query.isNotBlank()) {
+ url.addQueryParameter("query", query)
+ }
+
+ filters.forEach { filter ->
+ when (filter) {
+ is SelectionList -> {
+ val selected = filter.selected()
+ if (selected.value.isBlank()) {
+ return@forEach
+ }
+ url.addQueryParameter(selected.query, selected.value)
+ }
+ is GenreList -> {
+ filter.state.filter(GenreCheckBox::state).forEach { genre ->
+ url.addQueryParameter("genres", genre.id)
+ }
+ }
+ else -> {}
+ }
+ }
+
+ return GET(url.build(), headers)
+ }
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ if (query.startsWith(PREFIX_SEARCH)) {
+ val slug = query.removePrefix(PREFIX_SEARCH)
+ return fetchMangaDetails(SManga.create().apply { url = "/comics/$slug" }).map { manga ->
+ MangasPage(listOf(manga), false)
+ }
+ }
+
+ return super.fetchSearchManga(page, query, filters)
+ }
+
+ override fun searchMangaSelector() = "section h2 + div > div"
+
+ override fun searchMangaFromElement(element: Element) = SManga.create().apply {
+ title = element.selectFirst("h3")!!.text()
+ thumbnail_url = element.selectFirst("img")?.absUrl("src")
+ setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
+ }
+
+ override fun searchMangaNextPageSelector() = "a[href*=search]:contains(Next)"
+
+ // =========================== Manga Details ============================
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ title = document.selectFirst("h1")!!.text()
+ description = document.selectFirst("p:has(span:contains(Synopsis))")?.ownText()
+ author = document.selectFirst("p:has(span:contains(Author))")?.ownText()
+ genre = document.select("h2:contains(Genres) + div span").joinToString { it.text() }
+ thumbnail_url = document.selectFirst("img.object-cover")?.absUrl("src")
+ document.selectFirst("p:has(span:contains(Status))")?.ownText()?.let {
+ status = when (it.lowercase()) {
+ "ongoing" -> SManga.ONGOING
+ "complete" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+ setUrlWithoutDomain(document.location())
+ }
+
+ // ============================== Chapters ==============================
+ override fun chapterListSelector() = "section li"
+
+ override fun chapterFromElement(element: Element) = SChapter.create().apply {
+ element.selectFirst("a")!!.let {
+ name = it.ownText()
+ setUrlWithoutDomain(it.absUrl("href"))
+ }
+ date_upload = parseChapterDate(element.selectFirst("span")?.text())
+ }
+
+ // =============================== Pages ================================
+
+ override fun pageListParse(document: Document): List {
+ val pages = document.selectFirst("div#json-data")
+ ?.ownText()?.parseAs>()
+ ?: throw Exception("Pages not found")
+
+ return pages.mapIndexed { index, dto ->
+ val imageUrl = when {
+ dto.imageUrl.startsWith("http") -> dto.imageUrl
+ else -> "https://${dto.imageUrl}"
+ }
+ val fragment = json.encodeToString>(
+ dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
+ )
+ Page(index, imageUrl = "$imageUrl#$fragment")
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = ""
+
+ // ============================= Utilities ==============================
+
+ private fun parseChapterDate(date: String?): Long {
+ date ?: return 0
+ return try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
+ }
+
+ private fun parseRelativeDate(date: String): Long {
+ val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
+ val cal = Calendar.getInstance()
+
+ return when {
+ date.contains("day", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
+ date.contains("hour", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
+ date.contains("minute", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
+ date.contains("second", true) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
+ date.contains("week", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
+ else -> 0
+ }
+ }
+
+ private inline fun String.parseAs(): T {
+ return json.decodeFromString(this)
+ }
+
+ // =============================== Filters ================================
+
+ override fun getFilterList(): FilterList {
+ val filters = mutableListOf>(
+ SelectionList("Sort", sortByList),
+ Filter.Separator(),
+ GenreList(title = "Genres", genres = genreList),
+ )
+
+ return FilterList(filters)
+ }
+
+ companion object {
+ val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
+ const val PREFIX_SEARCH = "id:"
+ private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
+ }
+}
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlDto.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsDto.kt
similarity index 69%
rename from src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlDto.kt
rename to lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsDto.kt
index 1b7c9b61d..4302cc8ea 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlDto.kt
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsDto.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.all.snowmtl
+package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.graphics.Color
import android.os.Build
@@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
@@ -31,14 +32,17 @@ data class Dialog(
val y1: Float,
val x2: Float,
val y2: Float,
- val text: String,
val angle: Float = 0f,
val isBold: Boolean = false,
val isNewApi: Boolean = false,
+ val textByLanguage: Map = emptyMap(),
val type: String = "normal",
private val fbColor: List = emptyList(),
private val bgColor: List = emptyList(),
) {
+ val text: String get() = textByLanguage["text"] ?: throw Exception("Dialog not found")
+ fun getTextBy(language: Language) = textByLanguage[language.target] ?: text
+
val width get() = x2 - x1
val height get() = y2 - y1
val centerY get() = (y2 + y1) / 2f
@@ -62,14 +66,15 @@ private object DialogListSerializer :
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray(
element.jsonArray.map { jsonElement ->
- val (coordinates, text) = getCoordinatesAndDialog(jsonElement)
+ val coordinates = getCoordinates(jsonElement)
+ val textByLanguage = getDialogs(jsonElement)
buildJsonObject {
put("x1", coordinates[0])
put("y1", coordinates[1])
put("x2", coordinates[2])
put("y2", coordinates[3])
- put("text", text)
+ put("textByLanguage", textByLanguage)
try {
val obj = jsonElement.jsonObject
@@ -85,13 +90,28 @@ private object DialogListSerializer :
)
}
- private fun getCoordinatesAndDialog(element: JsonElement): Pair {
+ private fun getCoordinates(element: JsonElement): JsonArray {
return try {
- val arr = element.jsonArray
- arr[0].jsonArray to arr[1]
+ element.jsonArray[0].jsonArray
} catch (_: Exception) {
- val obj = element.jsonObject
- obj["bbox"]!!.jsonArray to obj["text"]!!
+ element.jsonObject["bbox"]!!.jsonArray
+ }
+ }
+ private fun getDialogs(element: JsonElement): JsonObject {
+ return try {
+ buildJsonObject {
+ put("text", element.jsonArray[1])
+ }
+ } catch (_: Exception) {
+ buildJsonObject {
+ // There is a problem when the "angle" is processed
+ element.jsonObject.entries.forEach {
+ if (it.key in listOf("angle", "bbox")) {
+ return@forEach
+ }
+ put(it.key, it.value)
+ }
+ }
}
}
}
diff --git a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt
new file mode 100644
index 000000000..96c03fa74
--- /dev/null
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt
@@ -0,0 +1,5 @@
+package eu.kanade.tachiyomi.multisrc.machinetranslations
+
+class MachineTranslationsFactoryUtils
+
+data class Language(val lang: String, val target: String = lang, val origin: String = "en")
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFilters.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFilters.kt
similarity index 96%
rename from src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFilters.kt
rename to lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFilters.kt
index 104b0db3c..a9624d1f7 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFilters.kt
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFilters.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.all.snowmtl
+package eu.kanade.tachiyomi.multisrc.machinetranslations
import eu.kanade.tachiyomi.source.model.Filter
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlUrlActivity.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsUrlActivity.kt
similarity index 77%
rename from src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlUrlActivity.kt
rename to lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsUrlActivity.kt
index d793f15db..31c01a251 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlUrlActivity.kt
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsUrlActivity.kt
@@ -1,13 +1,15 @@
-package eu.kanade.tachiyomi.extension.all.snowmtl
+package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.util.Log
+import androidx.annotation.RequiresApi
import kotlin.system.exitProcess
-
-class SnowmtlUrlActivity : Activity() {
+@RequiresApi(Build.VERSION_CODES.O)
+class MachineTranslationsUrlActivity : Activity() {
private val tag = javaClass.simpleName
@@ -18,7 +20,7 @@ class SnowmtlUrlActivity : Activity() {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
- putExtra("query", "${Snowmtl.PREFIX_SEARCH}$item")
+ putExtra("query", "${MachineTranslations.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/ComposedImageInterceptor.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt
similarity index 91%
rename from src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/ComposedImageInterceptor.kt
rename to lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt
index cf581282d..6278f6037 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/ComposedImageInterceptor.kt
+++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.extension.all.snowmtl.interceptors
+package eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@@ -12,14 +12,14 @@ import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import androidx.annotation.RequiresApi
-import eu.kanade.tachiyomi.extension.all.snowmtl.Dialog
-import eu.kanade.tachiyomi.extension.all.snowmtl.Snowmtl.Companion.PAGE_REGEX
+import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog
+import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
+import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy
@@ -35,7 +35,7 @@ import kotlin.math.sqrt
@RequiresApi(Build.VERSION_CODES.O)
class ComposedImageInterceptor(
baseUrl: String,
- private val client: OkHttpClient,
+ val language: Language,
) : Interceptor {
private val json: Json by injectLazy()
@@ -61,14 +61,16 @@ class ComposedImageInterceptor(
.url(url)
.build()
+ // Load the fonts before opening the connection to load the image,
+ // so there aren't two open connections inside the interceptor.
+ loadAllFont(chain)
+
val response = chain.proceed(imageRequest)
if (response.isSuccessful.not()) {
return response
}
- loadAllFont(chain)
-
val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!!
.copy(Bitmap.Config.ARGB_8888, true)
@@ -165,11 +167,17 @@ class ComposedImageInterceptor(
private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? {
return try {
val request = GET(fontUrl, chain.request().headers)
- val response = client
- .newCall(request).execute()
- .takeIf(Response::isSuccessful) ?: return null
+ val response = chain.proceed(request)
+
+ if (response.isSuccessful.not()) {
+ response.close()
+ return null
+ }
+
val fontName = request.url.pathSegments.last()
- response.body.byteStream().toTypeface(fontName)
+ response.body.use {
+ it.byteStream().toTypeface(fontName)
+ }
} catch (e: Exception) {
null
}
@@ -225,14 +233,17 @@ class ComposedImageInterceptor(
return dialogBox
}
- private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint) =
- StaticLayout.Builder.obtain(dialog.text, 0, dialog.text.length, textPaint, dialog.width.toInt()).apply {
+ private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint): StaticLayout {
+ val text = dialog.getTextBy(language)
+
+ return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, dialog.width.toInt()).apply {
setAlignment(Layout.Alignment.ALIGN_CENTER)
setIncludePad(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
}
}.build()
+ }
// Invert color in black dialog box.
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {
diff --git a/src/all/snowmtl/build.gradle b/src/all/snowmtl/build.gradle
index edb782da6..0928c6457 100644
--- a/src/all/snowmtl/build.gradle
+++ b/src/all/snowmtl/build.gradle
@@ -1,7 +1,9 @@
ext {
extName = 'Snow Machine Translations'
extClass = '.SnowmtlFactory'
- extVersionCode = 6
+ themePkg = 'machinetranslations'
+ baseUrl = 'https://snowmtl.ru'
+ overrideVersionCode = 6
isNsfw = true
}
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt
index e6c460d64..ab026c08d 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt
+++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt
@@ -2,226 +2,35 @@ package eu.kanade.tachiyomi.extension.all.snowmtl
import android.os.Build
import androidx.annotation.RequiresApi
-import eu.kanade.tachiyomi.extension.all.snowmtl.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.extension.all.snowmtl.interceptors.TranslationInterceptor
import eu.kanade.tachiyomi.extension.all.snowmtl.translator.BingTranslator
import eu.kanade.tachiyomi.extension.all.snowmtl.translator.TranslatorEngine
-import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
+import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations
+import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.interceptor.rateLimit
-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.ParsedHttpSource
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import rx.Observable
-import uy.kohesive.injekt.injectLazy
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Locale
import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O)
class Snowmtl(
- source: Source,
-) : ParsedHttpSource() {
+ language: Language,
+) : MachineTranslations(
+ name = "Snow Machine Translations",
+ baseUrl = "https://snowmtl.ru",
+ language,
+) {
+ override val lang = language.lang
- override val name = "Snow Machine Translations"
-
- override val baseUrl = "https://snowmtl.ru"
-
- override val lang = source.lang
-
- override val supportsLatest = true
-
- private val json: Json by injectLazy()
-
- private val translatorClient = network.cloudflareClient.newBuilder()
- .rateLimit(1, 3, TimeUnit.SECONDS)
+ private val clientUtils = network.cloudflareClient.newBuilder()
+ .rateLimit(1, 2, TimeUnit.SECONDS)
.build()
- private val translator: TranslatorEngine = BingTranslator(translatorClient, headers)
+ private val translator: TranslatorEngine = BingTranslator(clientUtils, headers)
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.readTimeout(2, TimeUnit.MINUTES)
- .addInterceptor(TranslationInterceptor(source, translator))
- .addInterceptor(ComposedImageInterceptor(baseUrl, super.client))
+ .addInterceptor(TranslationInterceptor(language, translator))
+ .addInterceptor(ComposedImageInterceptor(baseUrl, language))
.build()
-
- // ============================== Popular ===============================
-
- private val popularFilter = FilterList(SelectionList("", listOf(Option(value = "views", query = "sort_by"))))
-
- override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
-
- override fun popularMangaSelector() = searchMangaSelector()
-
- override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
-
- override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
-
- // =============================== Latest ===============================
-
- private val latestFilter = FilterList(SelectionList("", listOf(Option(value = "recent", query = "sort_by"))))
-
- override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
-
- override fun latestUpdatesSelector() = searchMangaSelector()
-
- override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
-
- override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
-
- // =========================== Search ============================
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- val url = "$baseUrl/search".toHttpUrl().newBuilder()
- .addQueryParameter("page", page.toString())
-
- if (query.isNotBlank()) {
- url.addQueryParameter("query", query)
- }
-
- filters.forEach { filter ->
- when (filter) {
- is SelectionList -> {
- val selected = filter.selected()
- if (selected.value.isBlank()) {
- return@forEach
- }
- url.addQueryParameter(selected.query, selected.value)
- }
- is GenreList -> {
- filter.state.filter(GenreCheckBox::state).forEach { genre ->
- url.addQueryParameter("genres", genre.id)
- }
- }
- else -> {}
- }
- }
-
- return GET(url.build(), headers)
- }
-
- override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
- if (query.startsWith(PREFIX_SEARCH)) {
- val slug = query.removePrefix(PREFIX_SEARCH)
- return fetchMangaDetails(SManga.create().apply { url = "/comics/$slug" }).map { manga ->
- MangasPage(listOf(manga), false)
- }
- }
-
- return super.fetchSearchManga(page, query, filters)
- }
-
- override fun searchMangaSelector() = "section h2 + div > div"
-
- override fun searchMangaFromElement(element: Element) = SManga.create().apply {
- title = element.selectFirst("h3")!!.text()
- thumbnail_url = element.selectFirst("img")?.absUrl("src")
- setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
- }
-
- override fun searchMangaNextPageSelector() = "a[href*=search]:contains(Next)"
-
- // =========================== Manga Details ============================
- override fun mangaDetailsParse(document: Document) = SManga.create().apply {
- title = document.selectFirst("h1")!!.text()
- description = document.selectFirst("p:has(span:contains(Synopsis))")?.ownText()
- author = document.selectFirst("p:has(span:contains(Author))")?.ownText()
- genre = document.select("h2:contains(Genres) + div span").joinToString { it.text() }
- thumbnail_url = document.selectFirst("img.object-cover")?.absUrl("src")
- document.selectFirst("p:has(span:contains(Status))")?.ownText()?.let {
- status = when (it.lowercase()) {
- "ongoing" -> SManga.ONGOING
- "complete" -> SManga.COMPLETED
- else -> SManga.UNKNOWN
- }
- }
- setUrlWithoutDomain(document.location())
- }
-
- // ============================== Chapters ==============================
- override fun chapterListSelector() = "section li"
-
- override fun chapterFromElement(element: Element) = SChapter.create().apply {
- element.selectFirst("a")!!.let {
- name = it.ownText()
- setUrlWithoutDomain(it.absUrl("href"))
- }
- date_upload = parseChapterDate(element.selectFirst("span")?.text())
- }
-
- // =============================== Pages ================================
-
- override fun pageListParse(document: Document): List {
- val pages = document.selectFirst("div#json-data")
- ?.ownText()?.parseAs>()
- ?: throw Exception("Pages not found")
-
- return pages.mapIndexed { index, dto ->
- val imageUrl = when {
- dto.imageUrl.startsWith("http") -> dto.imageUrl
- else -> "https://${dto.imageUrl}"
- }
- val fragment = json.encodeToString>(
- dto.dialogues.filter { it.text.isNotBlank() },
- )
- Page(index, imageUrl = "$imageUrl#$fragment")
- }
- }
-
- override fun imageUrlParse(document: Document): String = ""
-
- // ============================= Utilities ==============================
-
- private fun parseChapterDate(date: String?): Long {
- date ?: return 0
- return try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
- }
-
- private fun parseRelativeDate(date: String): Long {
- val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
- val cal = Calendar.getInstance()
-
- return when {
- date.contains("day", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
- date.contains("hour", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
- date.contains("minute", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
- date.contains("second", true) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
- date.contains("week", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
- else -> 0
- }
- }
-
- private inline fun String.parseAs(): T {
- return json.decodeFromString(this)
- }
-
- // =============================== Filters ================================
-
- override fun getFilterList(): FilterList {
- val filters = mutableListOf>(
- SelectionList("Sort", sortByList),
- Filter.Separator(),
- GenreList(title = "Genres", genres = genreList),
- )
-
- return FilterList(filters)
- }
-
- companion object {
- val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
- const val PREFIX_SEARCH = "id:"
- private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
- }
}
diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt
index 3856b8a90..ed9796480 100644
--- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt
+++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt
@@ -2,20 +2,18 @@ package eu.kanade.tachiyomi.extension.all.snowmtl
import android.os.Build
import androidx.annotation.RequiresApi
-import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.source.SourceFactory
@RequiresApi(Build.VERSION_CODES.O)
class SnowmtlFactory : SourceFactory {
- override fun createSources(): List