Update TMO, LectorTMO & TMOHentai (#6307)

* Add configurable rate limit, image CDN rate limit and SFW mode.

* Add configurable image download mode (cascade/paginated).

* Configurable ratelimit and image CDN ratelimit.

* Fix redundant title
This commit is contained in:
Edgar Mejía 2021-03-28 05:13:45 -06:00 committed by GitHub
parent 59b380205f
commit 135e08b754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 593 additions and 239 deletions

View File

@ -5,8 +5,12 @@ ext {
extName = 'LectorManga'
pkgNameSuffix = 'es.lectormanga'
extClass = '.LectorManga'
extVersionCode = 14
extVersionCode = 15
libVersion = '1.2'
}
dependencies {
implementation project(':lib-ratelimit')
}
apply from: "$rootDir/common.gradle"

View File

@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.extension.es.lectormanga
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.CheckBoxPreference
import android.support.v7.preference.ListPreference
import android.support.v7.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
@ -15,8 +17,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -27,9 +29,6 @@ import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Note: this source is similar to TuMangaOnline.
*/
class LectorManga : ConfigurableSource, ParsedHttpSource() {
override val name = "LectorManga"
@ -40,14 +39,29 @@ class LectorManga : ConfigurableSource, ParsedHttpSource() {
override val supportsLatest = true
private val userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
private val imageCDNUrl = "https://img1.followmanga.com"
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("User-Agent", userAgent)
.add("Referer", "$baseUrl/")
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val webRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(baseUrl)!!,
preferences.getString(WEB_RATELIMIT_PREF, WEB_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60
)
private val imageCDNRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(imageCDNUrl)!!,
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60
)
override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(webRateLimitInterceptor)
.addNetworkInterceptor(imageCDNRateLimitInterceptor)
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&type=&filter_by=title&page=$page", headers)
override fun popularMangaNextPageSelector() = ".pagination .page-item:not(.disabled) a[rel='next']"
@ -176,17 +190,16 @@ class LectorManga : ConfigurableSource, ParsedHttpSource() {
}
// Regular list of chapters
val dupselect = getduppref()!!
val chapterNames = document.select("#chapters h4.text-truncate")
val chapterNumbers = chapterNames.map { it.text().substringAfter("Capítulo").substringBefore("|").trim().toFloat() }
val chapterInfos = document.select("#chapters .chapter-list")
chapterNames.forEachIndexed { index, _ ->
val scanlator = chapterInfos[index].select("li")
if (dupselect == "one") {
scanlator.last { add(regularChapterFromElement(chapterNames[index].text(), it, chapterNumbers[index])) }
} else {
if (getScanlatorPref()) {
scanlator.forEach { add(regularChapterFromElement(chapterNames[index].text(), it, chapterNumbers[index])) }
} else {
scanlator.last { add(regularChapterFromElement(chapterNames[index].text(), it, chapterNumbers[index])) }
}
}
}
@ -224,9 +237,9 @@ class LectorManga : ConfigurableSource, ParsedHttpSource() {
val currentUrl = client.newCall(GET(chapter.url, headers)).execute().asJsoup().body().baseUri()
// Get /cascade instead of /paginate to get all pages at once
val newUrl = if (getPageMethod() == "cascade" && currentUrl.contains("paginated")) {
val newUrl = if (getPageMethodPref() == "cascade" && currentUrl.contains("paginated")) {
currentUrl.substringBefore("paginated") + "cascade"
} else if (getPageMethod() == "paginated" && currentUrl.contains("cascade")) {
} else if (getPageMethodPref() == "paginated" && currentUrl.contains("cascade")) {
currentUrl.substringBefore("cascade") + "paginated"
} else currentUrl
@ -234,7 +247,7 @@ class LectorManga : ConfigurableSource, ParsedHttpSource() {
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
if (getPageMethod() == "cascade") {
if (getPageMethodPref() == "cascade") {
document.select("div.viewer-container img").forEach {
add(
Page(
@ -410,91 +423,192 @@ class LectorManga : ConfigurableSource, ParsedHttpSource() {
fun toUriPart() = vals[state].second
}
// Preferences
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val deduppref = androidx.preference.ListPreference(screen.context).apply {
key = DEDUP_PREF_Title
title = DEDUP_PREF_Title
entries = arrayOf("Mostrar todos los scanlators", "Mostrar solo un scanlator")
entryValues = arrayOf("all", "one")
summary = "%s"
val scanlatorPref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = SCANLATOR_PREF
title = SCANLATOR_PREF_TITLE
summary = SCANLATOR_PREF_SUMMARY
setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(DEDUP_PREF, entry).commit()
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SCANLATOR_PREF, checkValue).commit()
}
}
val pageMethod = androidx.preference.ListPreference(screen.context).apply {
key = PAGEGET_PREF_Title
title = PAGEGET_PREF_Title
entries = arrayOf("Cascada (recomendado)", "Paginado")
val pageMethodPref = androidx.preference.ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "ginado")
entryValues = arrayOf("cascade", "paginated")
summary = "%s"
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(PAGEGET_PREF, entry).commit()
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(deduppref)
screen.addPreference(pageMethod)
// Rate limit
val apiRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
key = WEB_RATELIMIT_PREF
title = WEB_RATELIMIT_PREF_TITLE
summary = WEB_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(WEB_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(WEB_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val imgCDNRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
key = IMAGE_CDN_RATELIMIT_PREF
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(IMAGE_CDN_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(scanlatorPref)
screen.addPreference(pageMethodPref)
screen.addPreference(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val deduppref = ListPreference(screen.context).apply {
key = DEDUP_PREF_Title
title = DEDUP_PREF_Title
entries = arrayOf("Mostrar todos los scanlators", "Mostrar solo un scanlator")
entryValues = arrayOf("all", "one")
summary = "%s"
val scanlatorPref = CheckBoxPreference(screen.context).apply {
key = SCANLATOR_PREF
title = SCANLATOR_PREF_TITLE
summary = SCANLATOR_PREF_SUMMARY
setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(DEDUP_PREF, entry).commit()
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SCANLATOR_PREF, checkValue).commit()
}
}
val pageMethod = ListPreference(screen.context).apply {
key = PAGEGET_PREF_Title
title = PAGEGET_PREF_Title
entries = arrayOf("Cascada (recomendado)", "Paginado")
val pageMethodPref = ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "ginado")
entryValues = arrayOf("cascade", "paginated")
summary = "%s"
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(PAGEGET_PREF, entry).commit()
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(deduppref)
screen.addPreference(pageMethod)
// Rate limit
val apiRateLimitPreference = ListPreference(screen.context).apply {
key = WEB_RATELIMIT_PREF
title = WEB_RATELIMIT_PREF_TITLE
summary = WEB_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(WEB_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(WEB_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val imgCDNRateLimitPreference = ListPreference(screen.context).apply {
key = IMAGE_CDN_RATELIMIT_PREF
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(IMAGE_CDN_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(scanlatorPref)
screen.addPreference(pageMethodPref)
screen.addPreference(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
}
private fun getduppref() = preferences.getString(DEDUP_PREF, "all")
private fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE)
private fun getPageMethod() = preferences.getString(PAGEGET_PREF, "cascade")
private fun getPageMethodPref() = preferences.getString(PAGE_METHOD_PREF, PAGE_METHOD_PREF_DEFAULT_VALUE)
companion object {
private const val DEDUP_PREF_Title = "Preferencias de scanlator"
private const val DEDUP_PREF = "deduppref"
private const val PAGEGET_PREF_Title = "Método para la descarga de imágenes"
private const val PAGEGET_PREF = "pagemethodpref"
private const val SCANLATOR_PREF = "scanlatorPref"
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"
private const val SCANLATOR_PREF_DEFAULT_VALUE = true
private const val PAGE_METHOD_PREF = "pageMethodPref"
private const val PAGE_METHOD_PREF_TITLE = "Método para descargar imágenes"
private const val PAGE_METHOD_PREF_SUMMARY = "Previene ser banneado por el servidor cuando se usa la configuración \"Cascada\" ya que esta reduce la cantidad de solicitudes.\nPuedes usar \"Páginado\" cuando las imágenes no carguen usando \"Cascada\".\nConfiguración actual: %s"
private const val PAGE_METHOD_PREF_DEFAULT_VALUE = "cascade"
private const val WEB_RATELIMIT_PREF = "webRatelimitPreference"
// Ratelimit permits per second for main website
private const val WEB_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para el sitio web"
// This value affects network request amount to TMO url. Lower this value may reduce the chance to get HTTP 429 error, but loading speed will be slower too. Tachiyomi restart required. \nCurrent value: %s
private const val WEB_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red a la URL de TMO. Reducir este valor puede disminuir la posibilidad de obtener un error HTTP 429, pero la velocidad de descarga será más lenta. Se requiere reiniciar Tachiyomi. \nValor actual: %s"
private const val WEB_RATELIMIT_PREF_DEFAULT_VALUE = "10"
private const val IMAGE_CDN_RATELIMIT_PREF = "imgCDNRatelimitPreference"
// Ratelimit permits per second for image CDN
private const val IMAGE_CDN_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para descarga de imágenes"
// This value affects network request amount for loading image. Lower this value may reduce the chance to get error when loading image, but loading speed will be slower too. Tachiyomi restart required. \nCurrent value: %s
private const val IMAGE_CDN_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red para descargar imágenes. Reducir este valor puede disminuir errores al cargar imagenes, pero la velocidad de descarga será más lenta. Se requiere reiniciar Tachiyomi. \nValor actual: %s"
private const val IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE = "10"
private val ENTRIES_ARRAY = (1..10).map { i -> i.toString() }.toTypedArray()
const val PREFIX_ID_SEARCH = "id:"
const val MANGA_URL_CHUNK = "gotobook"

View File

@ -5,7 +5,7 @@ ext {
extName = 'TMOHentai'
pkgNameSuffix = 'es.tmohentai'
extClass = '.TMOHentai'
extVersionCode = 4
extVersionCode = 5
libVersion = '1.2'
containsNsfw = true
}

View File

@ -1,8 +1,13 @@
package eu.kanade.tachiyomi.extension.es.tmohentai
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.ListPreference
import android.support.v7.preference.PreferenceScreen
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -15,9 +20,11 @@ import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Nsfw
class TMOHentai : ParsedHttpSource() {
class TMOHentai : ConfigurableSource, ParsedHttpSource() {
override val name = "TMOHentai"
@ -27,6 +34,10 @@ class TMOHentai : ParsedHttpSource() {
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/section/all?view=list&page=$page&order=popularity&order-dir=desc&search[searchText]=&search[searchBy]=name&type=all", headers)
override fun popularMangaSelector() = "table > tbody > tr[data-toggle=popover]"
@ -71,19 +82,47 @@ class TMOHentai : ParsedHttpSource() {
name = element.select("h3.truncate").text()
scanlator = parsedInformation.substringAfter("By").substringBefore("Language").trim()
setUrlWithoutDomain(element.select("a.pull-right.btn.btn-primary").attr("href"))
var currentUrl = element.select("a.pull-right.btn.btn-primary").attr("href")
if (currentUrl.contains("/1")) {
currentUrl = currentUrl.substringBeforeLast("/")
}
setUrlWithoutDomain(currentUrl)
// date_upload = no date in the web
}
override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url.substringBefore("/paginated") + "/cascade", headers) // "/cascade" to get all images
// "/cascade" to get all images
override fun pageListRequest(chapter: SChapter): Request {
val currentUrl = chapter.url
val newUrl = if (getPageMethodPref() == "cascade" && currentUrl.contains("paginated")) {
currentUrl.substringBefore("paginated") + "cascade"
} else if (getPageMethodPref() == "paginated" && currentUrl.contains("cascade")) {
currentUrl.substringBefore("cascade") + "paginated"
} else currentUrl
return GET("$baseUrl$newUrl", headers)
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
document.select("div#content-images > div.row > div.col-xs-12.text-center > img.content-image")?.forEach {
add(Page(size, "", baseUrl + it.attr("data-original")))
if (getPageMethodPref() == "cascade") {
document.select("div#content-images img.content-image")?.forEach {
add(Page(size, "", it.attr("data-original")))
}
} else {
val pageList = document.select("select#select-page").first().select("option").map { it.attr("value").toInt() }
val url = document.baseUri()
pageList.forEach {
add(Page(it, "$url/$it"))
}
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
override fun imageUrlParse(document: Document) = document.select("div#content-images img.content-image").attr("data-original")
override fun imageRequest(page: Page) = GET("$baseUrl${page.imageUrl!!}", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/section/all?view=list")!!.newBuilder()
@ -193,9 +232,13 @@ class TMOHentai : ParsedHttpSource() {
Selection(2, false)
)
// Array.from(document.querySelectorAll('#advancedSearch .list-group .list-group-item'))
// .map(a => `Genre("${a.querySelector('span').innerText.replace(' ', '')}", "${a.querySelector('input').value}")`).join(',\n')
// https://tmohentai.com/section/hentai
/**
* Last check: 17/02/2021
* https://tmohentai.com/section/hentai
*
* Array.from(document.querySelectorAll('#advancedSearch .list-group .list-group-item'))
* .map(a => `Genre("${a.querySelector('span').innerText.replace(' ', '')}", "${a.querySelector('input').value}")`).join(',\n')
*/
private fun getGenreList() = listOf(
Genre("Romance", "1"),
Genre("Fantasy", "2"),
@ -245,7 +288,62 @@ class TMOHentai : ParsedHttpSource() {
Genre("Yandere", "46")
)
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val pageMethodPref = androidx.preference.ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "Páginado")
entryValues = arrayOf("cascade", "paginated")
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(pageMethodPref)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val pageMethodPref = ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "Páginado")
entryValues = arrayOf(PAGE_METHOD_PREF_CASCADE, PAGE_METHOD_PREF_PAGINATED)
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(pageMethodPref)
}
private fun getPageMethodPref() = preferences.getString(PAGE_METHOD_PREF, PAGE_METHOD_PREF_DEFAULT_VALUE)
companion object {
private const val PAGE_METHOD_PREF = "pageMethodPref"
private const val PAGE_METHOD_PREF_TITLE = "Método de descarga de imágenes"
private const val PAGE_METHOD_PREF_SUMMARY = "Puede corregir errores al cargar las imágenes.\nConfiguración actual: %s"
private const val PAGE_METHOD_PREF_CASCADE = "cascade"
private const val PAGE_METHOD_PREF_PAGINATED = "paginated"
private const val PAGE_METHOD_PREF_DEFAULT_VALUE = PAGE_METHOD_PREF_CASCADE
const val PREFIX_CONTENTS = "contents"
const val PREFIX_ID_SEARCH = "id:"

View File

@ -5,7 +5,7 @@ ext {
extName = 'TuMangaOnline'
pkgNameSuffix = 'es.tumangaonline'
extClass = '.TuMangaOnline'
extVersionCode = 31
extVersionCode = 32
libVersion = '1.2'
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.es.tumangaonline
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.CheckBoxPreference
import android.support.v7.preference.ListPreference
import android.support.v7.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
@ -16,7 +17,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
@ -35,28 +35,37 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
override val baseUrl = "https://lectortmo.com"
val baseurl = HttpUrl.parse(baseUrl)!!
override val lang = "es"
override val supportsLatest = true
private val userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
private val imageCDNUrl = "https://img1.japanreader.com"
private val rateLimitInterceptor = SpecificHostRateLimitInterceptor(baseurl, 10, 60) // real ratelimit values
override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(rateLimitInterceptor)
.build()
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("User-Agent", userAgent)
.add("Referer", "$baseUrl/")
.add("Cache-mode", "no-cache")
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title&_page=1&page=$page", headers)
private val webRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(baseUrl)!!,
preferences.getString(WEB_RATELIMIT_PREF, WEB_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60
)
private val imageCDNRateLimitInterceptor = SpecificHostRateLimitInterceptor(
HttpUrl.parse(imageCDNUrl)!!,
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60
)
override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(webRateLimitInterceptor)
.addNetworkInterceptor(imageCDNRateLimitInterceptor)
.build()
// Marks erotic content as false and excludes: Ecchi(6), GirlsLove(17), BoysLove(18) and Harem(19) genders
private val getSFWUrlPart = if (getSFWModePref()) "&exclude_genders%5B%5D=6&exclude_genders%5B%5D=17&exclude_genders%5B%5D=18&exclude_genders%5B%5D=19&erotic=false" else ""
override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title$getSFWUrlPart&_page=1&page=$page", headers)
override fun popularMangaNextPageSelector() = "a.page-link"
@ -70,7 +79,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
}
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title&_page=1&page=$page", headers)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title$getSFWUrlPart&_page=1&page=$page", headers)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
@ -80,11 +89,15 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/library")!!.newBuilder()
url.addQueryParameter("title", query)
if (getSFWModePref()) {
SFW_MODE_PREF_EXCLUDE_GENDERS.forEach { gender ->
url.addQueryParameter("exclude_genders[]", gender)
}
url.addQueryParameter("erotic", "false")
}
url.addQueryParameter("page", page.toString())
url.addQueryParameter("_page", "1") // Extra Query to Prevent Scrapping aka without it = 403
filters.forEach { filter ->
when (filter) {
is Types -> {
@ -93,6 +106,12 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
is Demography -> {
url.addQueryParameter("demography", filter.toUriPart())
}
is Status -> {
url.addQueryParameter("status", filter.toUriPart())
}
is TranslationStatus -> {
url.addQueryParameter("translation_status", filter.toUriPart())
}
is FilterBy -> {
url.addQueryParameter("filter_by", filter.toUriPart())
}
@ -105,93 +124,58 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
)
}
}
is WebcomicFilter -> {
url.addQueryParameter(
"webcomic",
when (filter.state) {
Filter.TriState.STATE_INCLUDE -> "true"
Filter.TriState.STATE_EXCLUDE -> "false"
else -> ""
is ContentTypeList -> {
filter.state.forEach { content ->
// If (SFW mode is not enabled) OR (SFW mode is enabled AND filter != erotic) -> Apply filter
// else -> ignore filter
if (!getSFWModePref() || (getSFWModePref() && content.id != "erotic")) {
when (content.state) {
Filter.TriState.STATE_IGNORE -> url.addQueryParameter(content.id, "")
Filter.TriState.STATE_INCLUDE -> url.addQueryParameter(content.id, "true")
Filter.TriState.STATE_EXCLUDE -> url.addQueryParameter(content.id, "false")
}
}
)
}
is FourKomaFilter -> {
url.addQueryParameter(
"yonkoma",
when (filter.state) {
Filter.TriState.STATE_INCLUDE -> "true"
Filter.TriState.STATE_EXCLUDE -> "false"
else -> ""
}
)
}
is AmateurFilter -> {
url.addQueryParameter(
"amateur",
when (filter.state) {
Filter.TriState.STATE_INCLUDE -> "true"
Filter.TriState.STATE_EXCLUDE -> "false"
else -> ""
}
)
}
is EroticFilter -> {
url.addQueryParameter(
"erotic",
when (filter.state) {
Filter.TriState.STATE_INCLUDE -> "true"
Filter.TriState.STATE_EXCLUDE -> "false"
else -> ""
}
)
}
}
is GenreList -> {
filter.state
.filter { genre -> genre.state }
.forEach { genre -> url.addQueryParameter("genders[]", genre.id) }
filter.state.forEach { genre ->
when (genre.state) {
Filter.TriState.STATE_INCLUDE -> url.addQueryParameter("exclude_genders[]", genre.id)
Filter.TriState.STATE_EXCLUDE -> url.addQueryParameter("genders[]", genre.id)
}
}
}
}
}
return GET(url.build().toString(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select("h2.element-subtitle").text()
document.select("h5.card-title").let {
author = it?.first()?.attr("title")?.substringAfter(", ")
artist = it?.last()?.attr("title")?.substringAfter(", ")
}
genre = document.select("a.py-2").joinToString(", ") {
it.text()
}
description = document.select("p.element-description")?.text()
status = parseStatus(document.select("span.book-status")?.text().orEmpty())
thumbnail_url = document.select(".book-thumbnail").attr("src")
}
private fun parseStatus(status: String) = when {
status.contains("Publicándose") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
// One-shot
if (document.select("div.chapters").isEmpty()) {
return document.select(oneShotChapterListSelector()).map { oneShotChapterFromElement(it) }
}
// Regular list of chapters
val chapters = mutableListOf<SChapter>()
document.select(regularChapterListSelector()).forEach { chapelement ->
@ -202,22 +186,17 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
.toFloat()
val chaptername = chapelement.select("div.col-10.text-truncate").text()
val scanelement = chapelement.select("ul.chapter-list > li")
val dupselect = getduppref()!!
if (dupselect == "one") {
scanelement.first { chapters.add(regularChapterFromElement(it, chaptername, chapternumber)) }
} else {
if (getScanlatorPref()) {
scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername, chapternumber)) }
} else {
scanelement.first { chapters.add(regularChapterFromElement(it, chaptername, chapternumber)) }
}
}
return chapters
}
override fun chapterListSelector() = throw UnsupportedOperationException("Not used")
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used")
private fun oneShotChapterListSelector() = "div.chapter-list-element > ul.list-group li.list-group-item"
private fun oneShotChapterFromElement(element: Element) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href")
name = "One Shot"
@ -225,9 +204,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let { parseChapterDate(it) }
?: 0
}
private fun regularChapterListSelector() = "div.chapters > ul.list-group li.p-0.list-group-item"
private fun regularChapterFromElement(element: Element, chName: String, number: Float) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href")
name = chName
@ -236,25 +213,20 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let { parseChapterDate(it) }
?: 0
}
private fun parseChapterDate(date: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)?.time
?: 0
override fun pageListRequest(chapter: SChapter): Request {
val currentUrl = client.newCall(GET(chapter.url, headers)).execute().asJsoup().body().baseUri()
// Get /cascade instead of /paginate to get all pages at once
val newUrl = if (getPageMethod() == "cascade" && currentUrl.contains("paginated")) {
val newUrl = if (getPageMethodPref() == "cascade" && currentUrl.contains("paginated")) {
currentUrl.substringBefore("paginated") + "cascade"
} else if (getPageMethod() == "paginated" && currentUrl.contains("cascade")) {
} else if (getPageMethodPref() == "paginated" && currentUrl.contains("cascade")) {
currentUrl.substringBefore("cascade") + "paginated"
} else currentUrl
return GET(newUrl, headers)
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
if (getPageMethod() == "cascade") {
if (getPageMethodPref() == "cascade") {
document.select("div.viewer-container img").forEach {
add(
Page(
@ -275,8 +247,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
}
}
}
// Note: At this moment (13/07/2020) it's necessary to make the image request without headers to prevent 403.
// Note: At this moment (15/02/2021) it's necessary to make the image request without headers to prevent 403.
override fun imageRequest(page: Page) = GET(page.imageUrl!!)
override fun imageUrlParse(document: Document): String {
@ -319,6 +290,27 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
)
)
private class Status : UriPartFilter(
"Filtrar por estado de serie",
arrayOf(
Pair("Ver todo", ""),
Pair("Publicándose", "publishing"),
Pair("Finalizado", "ended"),
Pair("Cancelado", "cancelled"),
Pair("Pausado", "on_hold")
)
)
private class TranslationStatus : UriPartFilter(
"Filtrar por estado de traducción",
arrayOf(
Pair("Ver todo", ""),
Pair("Activo", "publishing"),
Pair("Finalizado", "ended"),
Pair("Abandonado", "cancelled")
)
)
private class Demography : UriPartFilter(
"Filtrar por demografía",
arrayOf(
@ -332,7 +324,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
)
private class FilterBy : UriPartFilter(
"Campo de orden",
"Filtrar por",
arrayOf(
Pair("Título", "title"),
Pair("Autor", "author"),
@ -346,36 +338,38 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
Selection(0, false)
)
private class WebcomicFilter : Filter.TriState("Webcomic")
private class ContentType(name: String, val id: String) : Filter.TriState(name)
private class FourKomaFilter : Filter.TriState("Yonkoma")
private class ContentTypeList(content: List<ContentType>) : Filter.Group<ContentType>("Filtrar por tipo de contenido", content)
private class AmateurFilter : Filter.TriState("Amateur")
private class EroticFilter : Filter.TriState("Erótico")
private class Genre(name: String, val id: String) : Filter.CheckBox(name)
private class Genre(name: String, val id: String) : Filter.TriState(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Filtrar por géneros", genres)
override fun getFilterList() = FilterList(
Types(),
Filter.Separator(),
Filter.Header("Ignorado sino se filtra por tipo"),
Status(),
Filter.Separator(),
Filter.Header("Ignorado sino se filtra por tipo"),
TranslationStatus(),
Filter.Separator(),
Demography(),
Filter.Separator(),
FilterBy(),
Filter.Separator(),
SortBy(),
Filter.Separator(),
WebcomicFilter(),
FourKomaFilter(),
AmateurFilter(),
EroticFilter(),
ContentTypeList(getContentTypeList()),
Filter.Separator(),
GenreList(getGenreList())
)
// Array.from(document.querySelectorAll('#books-genders .col-auto .custom-control'))
// .map(a => `Genre("${a.querySelector('label').innerText}", "${a.querySelector('input').value}")`).join(',\n')
// on https://tumangaonline.me/library
// Last revision 13/07/2020
// on https://lectortmo.com/library
// Last revision 15/02/2021
private fun getGenreList() = listOf(
Genre("Acción", "1"),
Genre("Aventura", "2"),
@ -427,94 +421,238 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
Genre("Oeste", "48")
)
private fun getContentTypeList() = listOf(
ContentType("Webcomic", "webcomic"),
ContentType("Yonkoma", "yonkoma"),
ContentType("Amateur", "amateur"),
ContentType("Erótico", "erotic")
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val deduppref = androidx.preference.ListPreference(screen.context).apply {
key = DEDUP_PREF_Title
title = DEDUP_PREF_Title
entries = arrayOf("Mostrar todos los scanlators", "Mostrar solo un scanlator")
entryValues = arrayOf("all", "one")
summary = "%s"
val SFWModePref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = SFW_MODE_PREF
title = SFW_MODE_PREF_TITLE
summary = SFW_MODE_PREF_SUMMARY
setDefaultValue(SFW_MODE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(DEDUP_PREF, entry).commit()
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SFW_MODE_PREF, checkValue).commit()
}
}
val pageMethod = androidx.preference.ListPreference(screen.context).apply {
key = PAGEGET_PREF_Title
title = PAGEGET_PREF_Title
entries = arrayOf("Cascada (recomendado)", "Paginado")
val scanlatorPref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = SCANLATOR_PREF
title = SCANLATOR_PREF_TITLE
summary = SCANLATOR_PREF_SUMMARY
setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SCANLATOR_PREF, checkValue).commit()
}
}
val pageMethodPref = androidx.preference.ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "Páginado")
entryValues = arrayOf("cascade", "paginated")
summary = "%s"
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(PAGEGET_PREF, entry).commit()
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(deduppref)
screen.addPreference(pageMethod)
// Rate limit
val apiRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
key = WEB_RATELIMIT_PREF
title = WEB_RATELIMIT_PREF_TITLE
summary = WEB_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(WEB_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(WEB_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val imgCDNRateLimitPreference = androidx.preference.ListPreference(screen.context).apply {
key = IMAGE_CDN_RATELIMIT_PREF
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(IMAGE_CDN_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(SFWModePref)
screen.addPreference(scanlatorPref)
screen.addPreference(pageMethodPref)
screen.addPreference(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val deduppref = ListPreference(screen.context).apply {
key = DEDUP_PREF_Title
title = DEDUP_PREF_Title
entries = arrayOf("Mostrar todos los scanlators", "Mostrar solo un scanlator")
entryValues = arrayOf("all", "one")
summary = "%s"
val SFWModePref = CheckBoxPreference(screen.context).apply {
key = SFW_MODE_PREF
title = SFW_MODE_PREF_TITLE
summary = SFW_MODE_PREF_SUMMARY
setDefaultValue(SFW_MODE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(DEDUP_PREF, entry).commit()
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SFW_MODE_PREF, checkValue).commit()
}
}
val pageMethod = ListPreference(screen.context).apply {
key = PAGEGET_PREF_Title
title = PAGEGET_PREF_Title
entries = arrayOf("Cascada (recomendado)", "Paginado")
val scanlatorPref = CheckBoxPreference(screen.context).apply {
key = SCANLATOR_PREF
title = SCANLATOR_PREF_TITLE
summary = SCANLATOR_PREF_SUMMARY
setDefaultValue(SCANLATOR_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SCANLATOR_PREF, checkValue).commit()
}
}
val pageMethodPref = ListPreference(screen.context).apply {
key = PAGE_METHOD_PREF
title = PAGE_METHOD_PREF_TITLE
entries = arrayOf("Cascada", "Páginado")
entryValues = arrayOf("cascade", "paginated")
summary = "%s"
summary = PAGE_METHOD_PREF_SUMMARY
setDefaultValue(PAGE_METHOD_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(PAGEGET_PREF, entry).commit()
try {
val setting = preferences.edit().putString(PAGE_METHOD_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(deduppref)
screen.addPreference(pageMethod)
// Rate limit
val apiRateLimitPreference = ListPreference(screen.context).apply {
key = WEB_RATELIMIT_PREF
title = WEB_RATELIMIT_PREF_TITLE
summary = WEB_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(WEB_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(WEB_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val imgCDNRateLimitPreference = ListPreference(screen.context).apply {
key = IMAGE_CDN_RATELIMIT_PREF
title = IMAGE_CDN_RATELIMIT_PREF_TITLE
summary = IMAGE_CDN_RATELIMIT_PREF_SUMMARY
entries = ENTRIES_ARRAY
entryValues = ENTRIES_ARRAY
setDefaultValue(IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
try {
val setting = preferences.edit().putString(IMAGE_CDN_RATELIMIT_PREF, newValue as String).commit()
setting
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(SFWModePref)
screen.addPreference(scanlatorPref)
screen.addPreference(pageMethodPref)
screen.addPreference(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
}
private fun getduppref() = preferences.getString(DEDUP_PREF, "all")
private fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE)
private fun getPageMethod() = preferences.getString(PAGEGET_PREF, "cascade")
private fun getSFWModePref(): Boolean = preferences.getBoolean(SFW_MODE_PREF, SFW_MODE_PREF_DEFAULT_VALUE)
private fun getPageMethodPref() = preferences.getString(PAGE_METHOD_PREF, PAGE_METHOD_PREF_DEFAULT_VALUE)
companion object {
private const val DEDUP_PREF_Title = "Preferencias de scanlator"
private const val DEDUP_PREF = "deduppref"
private const val PAGEGET_PREF_Title = "Método para la descarga de imágenes"
private const val PAGEGET_PREF = "pagemethodpref"
private const val SCANLATOR_PREF = "scanlatorPref"
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"
private const val SCANLATOR_PREF_DEFAULT_VALUE = true
private const val SFW_MODE_PREF = "SFWModePref"
private const val SFW_MODE_PREF_TITLE = "Ocultar contenido NSFW"
private const val SFW_MODE_PREF_SUMMARY = "Ocultar el contenido erótico (puede que aún activandolo se sigan mostrando portadas o series NSFW). Ten en cuenta que al activarlo se ignoran filtros al explorar y buscar.\nLos filtros ignorados son: Filtrar por tipo de contenido (Erotico) y el Filtrar por generos: Ecchi, Boys Love, Girls Love y Harem."
private const val SFW_MODE_PREF_DEFAULT_VALUE = false
private val SFW_MODE_PREF_EXCLUDE_GENDERS = listOf("6", "17", "18", "19")
private const val PAGE_METHOD_PREF = "pageMethodPref"
private const val PAGE_METHOD_PREF_TITLE = "Método para descargar imágenes"
private const val PAGE_METHOD_PREF_SUMMARY = "Previene ser banneado por el servidor cuando se usa la configuración \"Cascada\" ya que esta reduce la cantidad de solicitudes.\nPuedes usar \"Páginado\" cuando las imágenes no carguen usando \"Cascada\".\nConfiguración actual: %s"
private const val PAGE_METHOD_PREF_DEFAULT_VALUE = "cascade"
private const val WEB_RATELIMIT_PREF = "webRatelimitPreference"
// Ratelimit permits per second for main website
private const val WEB_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para el sitio web"
// This value affects network request amount to TMO url. Lower this value may reduce the chance to get HTTP 429 error, but loading speed will be slower too. Tachiyomi restart required. \nCurrent value: %s
private const val WEB_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red a la URL de TMO. Reducir este valor puede disminuir la posibilidad de obtener un error HTTP 429, pero la velocidad de descarga será más lenta. Se requiere reiniciar Tachiyomi. \nValor actual: %s"
private const val WEB_RATELIMIT_PREF_DEFAULT_VALUE = "10"
private const val IMAGE_CDN_RATELIMIT_PREF = "imgCDNRatelimitPreference"
// Ratelimit permits per second for image CDN
private const val IMAGE_CDN_RATELIMIT_PREF_TITLE = "Ratelimit por minuto para descarga de imágenes"
// This value affects network request amount for loading image. Lower this value may reduce the chance to get error when loading image, but loading speed will be slower too. Tachiyomi restart required. \nCurrent value: %s
private const val IMAGE_CDN_RATELIMIT_PREF_SUMMARY = "Este valor afecta la cantidad de solicitudes de red para descargar imágenes. Reducir este valor puede disminuir errores al cargar imagenes, pero la velocidad de descarga será más lenta. Se requiere reiniciar Tachiyomi. \nValor actual: %s"
private const val IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE = "10"
private val ENTRIES_ARRAY = (1..10).map { i -> i.toString() }.toTypedArray()
const val PREFIX_LIBRARY = "library"
const val PREFIX_ID_SEARCH = "id:"