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 = languageList.map(::Snowmtl) + override fun createSources() = languageList.map(::Snowmtl) } -data class Source(val lang: String, val target: String = lang, val origin: String = "en") - private val languageList = listOf( - Source("en"), - Source("es"), - Source("id"), - Source("it"), - Source("pt-BR", "pt"), + Language("en"), + Language("es"), + Language("id"), + Language("it"), + Language("pt-BR", "pt"), ) diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt index 09150a328..656ff99d8 100644 --- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt +++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt @@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.extension.all.snowmtl.interceptors import android.os.Build 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.extension.all.snowmtl.Source import eu.kanade.tachiyomi.extension.all.snowmtl.translator.TranslatorEngine +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 kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -15,7 +15,7 @@ import uy.kohesive.injekt.injectLazy @RequiresApi(Build.VERSION_CODES.O) class TranslationInterceptor( - private val source: Source, + private val source: Language, private val translator: TranslatorEngine, ) : Interceptor { @@ -66,7 +66,11 @@ class TranslationInterceptor( val key = list.first() val text = list.last() - mapping[key]?.second?.dialog?.copy(text = text) + mapping[key]?.second?.dialog?.copy( + textByLanguage = mapOf( + "text" to text, + ), + ) } /** diff --git a/src/all/solarmtl/build.gradle b/src/all/solarmtl/build.gradle new file mode 100644 index 000000000..5a8b5945f --- /dev/null +++ b/src/all/solarmtl/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'Solar Machine Translations' + extClass = '.SolarmtlFactory' + themePkg = 'machinetranslations' + baseUrl = 'https://solarmtl.com' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt new file mode 100644 index 000000000..f4a264431 --- /dev/null +++ b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.extension.all.solarmtl + +import android.os.Build +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.multisrc.machinetranslations.Language +import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations +import eu.kanade.tachiyomi.network.interceptor.rateLimit + +@RequiresApi(Build.VERSION_CODES.O) +class Solarmtl( + language: Language, +) : MachineTranslations( + name = "Solar Machine Translations", + baseUrl = "https://solarmtl.com", + language, +) { + override val client = super.client.newBuilder() + .rateLimit(2) + .build() +} diff --git a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt new file mode 100644 index 000000000..07cd2ff65 --- /dev/null +++ b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.extension.all.solarmtl + +import android.os.Build +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.multisrc.machinetranslations.Language +import eu.kanade.tachiyomi.source.SourceFactory + +@RequiresApi(Build.VERSION_CODES.O) +class SolarmtlFactory : SourceFactory { + override fun createSources() = languageList.map(::Solarmtl) +} + +private val languageList = listOf( + Language("en"), + Language("fr"), + Language("pt-BR", "pt"), +)