Move snowmlt to lib-multisrc and add Solarmtl (#7054)

* Move snowmlt to lib-multisrc

* Fix snowmtl

* Remove assets from src
This commit is contained in:
Chopper 2025-01-09 11:40:56 -03:00 committed by Draff
parent 690c553b6c
commit 580b2b1b16
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
22 changed files with 384 additions and 248 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslationsUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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<MangasPage> {
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<Page> {
val pages = document.selectFirst("div#json-data")
?.ownText()?.parseAs<List<PageDto>>()
?: 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<List<Dialog>>(
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 <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
// =============================== Filters ================================
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
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)
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.all.snowmtl package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -31,14 +32,17 @@ data class Dialog(
val y1: Float, val y1: Float,
val x2: Float, val x2: Float,
val y2: Float, val y2: Float,
val text: String,
val angle: Float = 0f, val angle: Float = 0f,
val isBold: Boolean = false, val isBold: Boolean = false,
val isNewApi: Boolean = false, val isNewApi: Boolean = false,
val textByLanguage: Map<String, String> = emptyMap(),
val type: String = "normal", val type: String = "normal",
private val fbColor: List<Int> = emptyList(), private val fbColor: List<Int> = emptyList(),
private val bgColor: List<Int> = emptyList(), private val bgColor: List<Int> = 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 width get() = x2 - x1
val height get() = y2 - y1 val height get() = y2 - y1
val centerY get() = (y2 + y1) / 2f val centerY get() = (y2 + y1) / 2f
@ -62,14 +66,15 @@ private object DialogListSerializer :
override fun transformDeserialize(element: JsonElement): JsonElement { override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray( return JsonArray(
element.jsonArray.map { jsonElement -> element.jsonArray.map { jsonElement ->
val (coordinates, text) = getCoordinatesAndDialog(jsonElement) val coordinates = getCoordinates(jsonElement)
val textByLanguage = getDialogs(jsonElement)
buildJsonObject { buildJsonObject {
put("x1", coordinates[0]) put("x1", coordinates[0])
put("y1", coordinates[1]) put("y1", coordinates[1])
put("x2", coordinates[2]) put("x2", coordinates[2])
put("y2", coordinates[3]) put("y2", coordinates[3])
put("text", text) put("textByLanguage", textByLanguage)
try { try {
val obj = jsonElement.jsonObject val obj = jsonElement.jsonObject
@ -85,13 +90,28 @@ private object DialogListSerializer :
) )
} }
private fun getCoordinatesAndDialog(element: JsonElement): Pair<JsonArray, JsonElement> { private fun getCoordinates(element: JsonElement): JsonArray {
return try { return try {
val arr = element.jsonArray element.jsonArray[0].jsonArray
arr[0].jsonArray to arr[1]
} catch (_: Exception) { } catch (_: Exception) {
val obj = element.jsonObject element.jsonObject["bbox"]!!.jsonArray
obj["bbox"]!!.jsonArray to obj["text"]!! }
}
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)
}
}
} }
} }
} }

View File

@ -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")

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.all.snowmtl package eu.kanade.tachiyomi.multisrc.machinetranslations
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter

View File

@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.extension.all.snowmtl package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import kotlin.system.exitProcess import kotlin.system.exitProcess
@RequiresApi(Build.VERSION_CODES.O)
class SnowmtlUrlActivity : Activity() { class MachineTranslationsUrlActivity : Activity() {
private val tag = javaClass.simpleName private val tag = javaClass.simpleName
@ -18,7 +20,7 @@ class SnowmtlUrlActivity : Activity() {
val item = pathSegments[1] val item = pathSegments[1]
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Snowmtl.PREFIX_SEARCH}$item") putExtra("query", "${MachineTranslations.PREFIX_SEARCH}$item")
putExtra("filter", packageName) putExtra("filter", packageName)
} }

View File

@ -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.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -12,14 +12,14 @@ import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.extension.all.snowmtl.Dialog import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog
import eu.kanade.tachiyomi.extension.all.snowmtl.Snowmtl.Companion.PAGE_REGEX import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -35,7 +35,7 @@ import kotlin.math.sqrt
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class ComposedImageInterceptor( class ComposedImageInterceptor(
baseUrl: String, baseUrl: String,
private val client: OkHttpClient, val language: Language,
) : Interceptor { ) : Interceptor {
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -61,14 +61,16 @@ class ComposedImageInterceptor(
.url(url) .url(url)
.build() .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) val response = chain.proceed(imageRequest)
if (response.isSuccessful.not()) { if (response.isSuccessful.not()) {
return response return response
} }
loadAllFont(chain)
val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!! val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!!
.copy(Bitmap.Config.ARGB_8888, true) .copy(Bitmap.Config.ARGB_8888, true)
@ -165,11 +167,17 @@ class ComposedImageInterceptor(
private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? { private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? {
return try { return try {
val request = GET(fontUrl, chain.request().headers) val request = GET(fontUrl, chain.request().headers)
val response = client val response = chain.proceed(request)
.newCall(request).execute()
.takeIf(Response::isSuccessful) ?: return null if (response.isSuccessful.not()) {
response.close()
return null
}
val fontName = request.url.pathSegments.last() val fontName = request.url.pathSegments.last()
response.body.byteStream().toTypeface(fontName) response.body.use {
it.byteStream().toTypeface(fontName)
}
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -225,14 +233,17 @@ class ComposedImageInterceptor(
return dialogBox return dialogBox
} }
private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint) = private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint): StaticLayout {
StaticLayout.Builder.obtain(dialog.text, 0, dialog.text.length, textPaint, dialog.width.toInt()).apply { val text = dialog.getTextBy(language)
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, dialog.width.toInt()).apply {
setAlignment(Layout.Alignment.ALIGN_CENTER) setAlignment(Layout.Alignment.ALIGN_CENTER)
setIncludePad(false) setIncludePad(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED) setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
} }
}.build() }.build()
}
// Invert color in black dialog box. // Invert color in black dialog box.
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) { private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {

View File

@ -1,7 +1,9 @@
ext { ext {
extName = 'Snow Machine Translations' extName = 'Snow Machine Translations'
extClass = '.SnowmtlFactory' extClass = '.SnowmtlFactory'
extVersionCode = 6 themePkg = 'machinetranslations'
baseUrl = 'https://snowmtl.ru'
overrideVersionCode = 6
isNsfw = true isNsfw = true
} }

View File

@ -2,226 +2,35 @@ package eu.kanade.tachiyomi.extension.all.snowmtl
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi 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.interceptors.TranslationInterceptor
import eu.kanade.tachiyomi.extension.all.snowmtl.translator.BingTranslator import eu.kanade.tachiyomi.extension.all.snowmtl.translator.BingTranslator
import eu.kanade.tachiyomi.extension.all.snowmtl.translator.TranslatorEngine 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.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 import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class Snowmtl( class Snowmtl(
source: Source, language: Language,
) : ParsedHttpSource() { ) : MachineTranslations(
name = "Snow Machine Translations",
baseUrl = "https://snowmtl.ru",
language,
) {
override val lang = language.lang
override val name = "Snow Machine Translations" private val clientUtils = network.cloudflareClient.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS)
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)
.build() .build()
private val translator: TranslatorEngine = BingTranslator(translatorClient, headers) private val translator: TranslatorEngine = BingTranslator(clientUtils, headers)
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.rateLimit(2) .rateLimit(2)
.readTimeout(2, TimeUnit.MINUTES) .readTimeout(2, TimeUnit.MINUTES)
.addInterceptor(TranslationInterceptor(source, translator)) .addInterceptor(TranslationInterceptor(language, translator))
.addInterceptor(ComposedImageInterceptor(baseUrl, super.client)) .addInterceptor(ComposedImageInterceptor(baseUrl, language))
.build() .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<MangasPage> {
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<Page> {
val pages = document.selectFirst("div#json-data")
?.ownText()?.parseAs<List<PageDto>>()
?: 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<List<Dialog>>(
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 <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
// =============================== Filters ================================
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
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)
}
} }

View File

@ -2,20 +2,18 @@ package eu.kanade.tachiyomi.extension.all.snowmtl
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class SnowmtlFactory : SourceFactory { class SnowmtlFactory : SourceFactory {
override fun createSources(): List<Source> = 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( private val languageList = listOf(
Source("en"), Language("en"),
Source("es"), Language("es"),
Source("id"), Language("id"),
Source("it"), Language("it"),
Source("pt-BR", "pt"), Language("pt-BR", "pt"),
) )

View File

@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.extension.all.snowmtl.interceptors
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi 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.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.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -15,7 +15,7 @@ import uy.kohesive.injekt.injectLazy
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class TranslationInterceptor( class TranslationInterceptor(
private val source: Source, private val source: Language,
private val translator: TranslatorEngine, private val translator: TranslatorEngine,
) : Interceptor { ) : Interceptor {
@ -66,7 +66,11 @@ class TranslationInterceptor(
val key = list.first() val key = list.first()
val text = list.last() val text = list.last()
mapping[key]?.second?.dialog?.copy(text = text) mapping[key]?.second?.dialog?.copy(
textByLanguage = mapOf(
"text" to text,
),
)
} }
/** /**

View File

@ -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"

View File

@ -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()
}

View File

@ -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"),
)