Move TMO and LectorManga to multisrc (#1002)

* Theme-lib when

* Lint

* Order generator

* Apply no-nsfw pref on request instead of init
This commit is contained in:
bapeey 2024-02-05 01:43:21 -05:00 committed by GitHub
parent 1ad394b740
commit a0a5282685
22 changed files with 330 additions and 1000 deletions

View File

@ -3,7 +3,7 @@
<application> <application>
<activity <activity
android:name=".es.tumangaonline.TuMangaOnlineUrlActivity" android:name="eu.kanade.tachiyomi.multisrc.lectortmo.LectorTmoUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -12,11 +12,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="visortmo.com" android:host="${SOURCEHOST}"
android:pathPattern="/library/..*/..*/..*" android:pathPattern="/library/..*/..*/..*"
android:scheme="https" /> android:scheme="${SOURCESCHEME}" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.extension.es.lectormanga
import eu.kanade.tachiyomi.multisrc.lectortmo.LectorTmo
import eu.kanade.tachiyomi.network.GET
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.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class LectorManga : LectorTmo("LectorManga", "https://lectormanga.com", "es") {
override val id = 7925520943983324764
override fun popularMangaSelector() = ".col-6 .card"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select("a").text()
thumbnail_url = element.select("img").attr("src")
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst("h1:has(small)")?.let { title = it.ownText() }
genre = document.select("a.py-2").joinToString(", ") {
it.text()
}
description = document.select(".col-12.mt-2").text()
status = parseStatus(document.select(".status-publishing").text())
thumbnail_url = document.select(".text-center img.img-fluid").attr("src")
}
override fun chapterListParse(response: Response): List<SChapter> = mutableListOf<SChapter>().apply {
val document = response.asJsoup()
// One-shot
if (document.select("#chapters").isEmpty()) {
return document.select(oneShotChapterListSelector).map { chapterFromElement(it, oneShotChapterName) }
}
// Regular list of chapters
val chapterNames = document.select("#chapters h4.text-truncate")
val chapterInfos = document.select("#chapters .chapter-list")
chapterNames.forEachIndexed { index, _ ->
val scanlator = chapterInfos[index].select("li")
if (getScanlatorPref()) {
scanlator.forEach { add(chapterFromElement(it, chapterNames[index].text())) }
} else {
scanlator.last { add(chapterFromElement(it, chapterNames[index].text())) }
}
}
}
override fun chapterFromElement(element: Element, chName: String) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href")
name = chName
scanlator = element.select("div.col-12.text-truncate span").text()
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let {
parseChapterDate(it)
} ?: 0
}
override fun imageRequest(page: Page) = GET(
url = page.imageUrl!!,
headers = headers.newBuilder()
.set("Referer", page.url.substringBefore("news/"))
.build(),
)
}

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.extension.es.tumangaonline
import eu.kanade.tachiyomi.multisrc.lectortmo.LectorTmo
class TuMangaOnline : LectorTmo("TuMangaOnline", "https://visortmo.com", "es") {
override val id = 4146344224513899730
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.es.tumangaonline package eu.kanade.tachiyomi.multisrc.lectortmo
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -36,30 +35,31 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class TuMangaOnline : ConfigurableSource, ParsedHttpSource() { abstract class LectorTmo(
override val name: String,
override val name = "TuMangaOnline" override val baseUrl: String,
override val lang: String,
override val baseUrl = "https://visortmo.com" ) : ConfigurableSource, ParsedHttpSource() {
override val lang = "es"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("Referer", "$baseUrl/")
}
private val imageCDNUrls = arrayOf(
"https://japanreader.com",
"https://img1.japanreader.com",
)
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
protected open val imageCDNUrls = arrayOf(
"https://img1.followmanga.com",
"https://img1.biggestchef.com",
"https://img1.indalchef.com",
"https://img1.recipesandcook.com",
"https://img1.cyclingte.com",
"https://img1.japanreader.com",
"https://japanreader.com",
)
private fun OkHttpClient.Builder.rateLimitImageCDNs(hosts: Array<String>, permits: Int, period: Long): OkHttpClient.Builder { private fun OkHttpClient.Builder.rateLimitImageCDNs(hosts: Array<String>, permits: Int, period: Long): OkHttpClient.Builder {
hosts.forEach { host -> hosts.forEach { host ->
rateLimitHost(host.toHttpUrl(), permits, period) rateLimitHost(host.toHttpUrl(), permits, period)
@ -115,10 +115,10 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
) )
.build() .build()
// Marks erotic content as false and excludes: Ecchi(6), GirlsLove(17), BoysLove(18) and Harem(19) genders // Marks erotic content as false and excludes: Ecchi(6), GirlsLove(17), BoysLove(18), Harem(19), Trap(94) 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 "" private fun getSFWUrlPart(): String = if (getSFWModePref()) "&exclude_genders%5B%5D=6&exclude_genders%5B%5D=17&exclude_genders%5B%5D=18&exclude_genders%5B%5D=19&exclude_genders%5B%5D=94&erotic=false" else ""
override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title$getSFWUrlPart&_pg=1&page=$page", headers) override fun popularMangaRequest(page: Int) = GET("$baseUrl/library?order_item=likes_count&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", headers)
override fun popularMangaNextPageSelector() = "a[rel='next']" override fun popularMangaNextPageSelector() = "a[rel='next']"
@ -132,7 +132,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
} }
} }
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title$getSFWUrlPart&_pg=1&page=$page", headers) override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&filter_by=title${getSFWUrlPart()}&_pg=1&page=$page", headers)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
@ -140,6 +140,28 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH)
client.newCall(searchMangaBySlugRequest(realQuery))
.asObservableSuccess()
.map { response ->
val details = mangaDetailsParse(response)
details.url = "/$PREFIX_LIBRARY/$realQuery"
MangasPage(listOf(details), false)
}
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
}
private fun searchMangaBySlugRequest(slug: String) = GET("$baseUrl/$PREFIX_LIBRARY/$slug", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/library".toHttpUrl().newBuilder() val url = "$baseUrl/library".toHttpUrl().newBuilder()
url.addQueryParameter("title", query) url.addQueryParameter("title", query)
@ -159,15 +181,6 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
is Demography -> { is Demography -> {
url.addQueryParameter("demography", filter.toUriPart()) 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())
}
is SortBy -> { is SortBy -> {
if (filter.state != null) { if (filter.state != null) {
url.addQueryParameter("order_item", SORTABLES[filter.state!!.index].second) url.addQueryParameter("order_item", SORTABLES[filter.state!!.index].second)
@ -179,8 +192,6 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
} }
is ContentTypeList -> { is ContentTypeList -> {
filter.state.forEach { content -> 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")) { if (!getSFWModePref() || (getSFWModePref() && content.id != "erotic")) {
when (content.state) { when (content.state) {
Filter.TriState.STATE_IGNORE -> url.addQueryParameter(content.id, "") Filter.TriState.STATE_IGNORE -> url.addQueryParameter(content.id, "")
@ -204,8 +215,11 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
return GET(url.build(), headers) return GET(url.build(), headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select("h2.element-subtitle").text() title = document.select("h2.element-subtitle").text()
document.select("h5.card-title").let { document.select("h5.card-title").let {
@ -219,54 +233,63 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
status = parseStatus(document.select("span.book-status").text()) status = parseStatus(document.select("span.book-status").text())
thumbnail_url = document.select(".book-thumbnail").attr("src") thumbnail_url = document.select(".book-thumbnail").attr("src")
} }
private fun parseStatus(status: String) = when {
protected fun parseStatus(status: String) = when {
status.contains("Publicándose") -> SManga.ONGOING status.contains("Publicándose") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
protected open val oneShotChapterName = "One Shot"
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
// One-shot // One-shot
if (document.select("div.chapters").isEmpty()) { if (document.select("div.chapters").isEmpty()) {
return document.select(oneShotChapterListSelector()).map { oneShotChapterFromElement(it) } return document.select(oneShotChapterListSelector).map { chapterFromElement(it, oneShotChapterName) }
} }
// Regular list of chapters // Regular list of chapters
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
document.select(regularChapterListSelector()).forEach { chapelement -> document.select(regularChapterListSelector).forEach { chapelement ->
val chaptername = chapelement.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim() val chapterName = chapelement.select("div.col-10.text-truncate").text().replace("&nbsp;", " ").trim()
val scanelement = chapelement.select("ul.chapter-list > li") val chapterScanlator = chapelement.select("ul.chapter-list > li")
if (getScanlatorPref()) { if (getScanlatorPref()) {
scanelement.forEach { chapters.add(regularChapterFromElement(it, chaptername)) } chapterScanlator.forEach { chapters.add(chapterFromElement(it, chapterName)) }
} else { } else {
scanelement.first { chapters.add(regularChapterFromElement(it, chaptername)) } chapterScanlator.first { chapters.add(chapterFromElement(it, chapterName)) }
} }
} }
return chapters return chapters
} }
protected open val oneShotChapterListSelector = "div.chapter-list-element > ul.list-group li.list-group-item"
protected open val regularChapterListSelector = "div.chapters > ul.list-group li.p-0.list-group-item"
override fun chapterListSelector() = throw UnsupportedOperationException() override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
private fun oneShotChapterListSelector() = "div.chapter-list-element > ul.list-group li.list-group-item"
private fun oneShotChapterFromElement(element: Element) = SChapter.create().apply { protected open fun chapterFromElement(element: Element, chName: String) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href")
name = "One Shot"
scanlator = element.select("div.col-md-6.text-truncate").text()
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) = SChapter.create().apply {
url = element.select("div.row > .text-right > a").attr("href") url = element.select("div.row > .text-right > a").attr("href")
name = chName name = chName
scanlator = element.select("div.col-md-6.text-truncate").text() scanlator = element.select("div.col-md-6.text-truncate").text()
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let { parseChapterDate(it) } date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let {
?: 0 parseChapterDate(it)
} ?: 0
} }
private fun parseChapterDate(date: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)?.time
?: 0 protected open fun parseChapterDate(date: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.parse(date)?.time ?: 0
}
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return GET(chapter.url, headers) return GET(chapter.url, headers)
} }
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply { override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
var doc = redirectToReadPage(document) var doc = redirectToReadPage(document)
@ -302,7 +325,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
} }
} }
private fun redirectToReadPage(document: Document): Document { private tailrec fun redirectToReadPage(document: Document): Document {
val script1 = document.selectFirst("script:containsData(uniqid)") val script1 = document.selectFirst("script:containsData(uniqid)")
val script2 = document.selectFirst("script:containsData(window.location.replace)") val script2 = document.selectFirst("script:containsData(window.location.replace)")
val script3 = document.selectFirst("script:containsData(redirectUrl)") val script3 = document.selectFirst("script:containsData(redirectUrl)")
@ -376,195 +399,8 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
} }
} }
// Note: At this moment (05/04/2023) it's necessary to make the image request with headers to prevent 403.
override fun imageRequest(page: Page): Request {
val imageHeaders = Headers.Builder()
.add("Referer", baseUrl)
.build()
return GET(page.imageUrl!!, imageHeaders)
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/$PREFIX_LIBRARY/$id", headers)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(realQuery))
.asObservableSuccess()
.map { response ->
val details = mangaDetailsParse(response)
details.url = "/$PREFIX_LIBRARY/$realQuery"
MangasPage(listOf(details), false)
}
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
}
private class Types : UriPartFilter(
"Filtrar por tipo",
arrayOf(
Pair("Ver todo", ""),
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
Pair("Manhwa", "manhwa"),
Pair("Novela", "novel"),
Pair("One shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Oel", "oel"),
),
)
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(
Pair("Ver todo", ""),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Josei", "josei"),
Pair("Kodomo", "kodomo"),
),
)
private class FilterBy : UriPartFilter(
"Filtrar por",
arrayOf(
Pair("Título", "title"),
Pair("Autor", "author"),
Pair("Compañia", "company"),
),
)
class SortBy : Filter.Sort(
"Ordenar por",
SORTABLES.map { it.first }.toTypedArray(),
Selection(0, false),
)
private class ContentType(name: String, val id: String) : Filter.TriState(name)
private class ContentTypeList(content: List<ContentType>) : Filter.Group<ContentType>("Filtrar por tipo de contenido", content)
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(),
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://lectortmo.com/library
// Last revision 15/02/2021
private fun getGenreList() = listOf(
Genre("Acción", "1"),
Genre("Aventura", "2"),
Genre("Comedia", "3"),
Genre("Drama", "4"),
Genre("Recuentos de la vida", "5"),
Genre("Ecchi", "6"),
Genre("Fantasia", "7"),
Genre("Magia", "8"),
Genre("Sobrenatural", "9"),
Genre("Horror", "10"),
Genre("Misterio", "11"),
Genre("Psicológico", "12"),
Genre("Romance", "13"),
Genre("Ciencia Ficción", "14"),
Genre("Thriller", "15"),
Genre("Deporte", "16"),
Genre("Girls Love", "17"),
Genre("Boys Love", "18"),
Genre("Harem", "19"),
Genre("Mecha", "20"),
Genre("Supervivencia", "21"),
Genre("Reencarnación", "22"),
Genre("Gore", "23"),
Genre("Apocalíptico", "24"),
Genre("Tragedia", "25"),
Genre("Vida Escolar", "26"),
Genre("Historia", "27"),
Genre("Militar", "28"),
Genre("Policiaco", "29"),
Genre("Crimen", "30"),
Genre("Superpoderes", "31"),
Genre("Vampiros", "32"),
Genre("Artes Marciales", "33"),
Genre("Samurái", "34"),
Genre("Género Bender", "35"),
Genre("Realidad Virtual", "36"),
Genre("Ciberpunk", "37"),
Genre("Musica", "38"),
Genre("Parodia", "39"),
Genre("Animación", "40"),
Genre("Demonios", "41"),
Genre("Familia", "42"),
Genre("Extranjero", "43"),
Genre("Niños", "44"),
Genre("Realidad", "45"),
Genre("Telenovela", "46"),
Genre("Guerra", "47"),
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
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val sfwModePref = androidx.preference.CheckBoxPreference(screen.context).apply { val sfwModePref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = SFW_MODE_PREF key = SFW_MODE_PREF
@ -635,9 +471,125 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
screen.addPreference(imgCDNRateLimitPreference) screen.addPreference(imgCDNRateLimitPreference)
} }
private fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE) override fun getFilterList() = FilterList(
FilterBy(),
Filter.Separator(),
SortBy(),
Filter.Separator(),
Types(),
Demography(),
ContentTypeList(getContentTypeList()),
Filter.Separator(),
GenreList(getGenreList()),
)
private fun getSFWModePref(): Boolean = preferences.getBoolean(SFW_MODE_PREF, SFW_MODE_PREF_DEFAULT_VALUE) private class FilterBy : UriPartFilter(
"Buscar por",
arrayOf(
Pair("Título", "title"),
Pair("Autor", "author"),
Pair("Compañia", "company"),
),
)
class SortBy : Filter.Sort(
"Ordenar por",
SORTABLES.map { it.first }.toTypedArray(),
Selection(0, false),
)
private class Types : UriPartFilter(
"Filtrar por tipo",
arrayOf(
Pair("Ver todo", ""),
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
Pair("Manhwa", "manhwa"),
Pair("Novela", "novel"),
Pair("One shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Oel", "oel"),
),
)
private class Demography : UriPartFilter(
"Filtrar por demografía",
arrayOf(
Pair("Ver todo", ""),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Josei", "josei"),
Pair("Kodomo", "kodomo"),
),
)
private fun getContentTypeList() = listOf(
ContentType("Webcomic", "webcomic"),
ContentType("Yonkoma", "yonkoma"),
ContentType("Amateur", "amateur"),
ContentType("Erótico", "erotic"),
)
// Array.from(document.querySelectorAll('#books-genders .col-auto .custom-control'))
// .map(a => `Genre("${a.querySelector('label').innerText}", "${a.querySelector('input').value}")`).join(',\n')
// on ${baseUrl}/library
// Last revision 02/04/2024 (mm/dd/yyyy)
private fun getGenreList() = listOf(
Genre("Acción", "1"),
Genre("Aventura", "2"),
Genre("Comedia", "3"),
Genre("Drama", "4"),
Genre("Recuentos de la vida", "5"),
Genre("Ecchi", "6"),
Genre("Fantasia", "7"),
Genre("Magia", "8"),
Genre("Sobrenatural", "9"),
Genre("Horror", "10"),
Genre("Misterio", "11"),
Genre("Psicológico", "12"),
Genre("Romance", "13"),
Genre("Ciencia Ficción", "14"),
Genre("Thriller", "15"),
Genre("Deporte", "16"),
Genre("Girls Love", "17"),
Genre("Boys Love", "18"),
Genre("Harem", "19"),
Genre("Mecha", "20"),
Genre("Supervivencia", "21"),
Genre("Reencarnación", "22"),
Genre("Gore", "23"),
Genre("Apocalíptico", "24"),
Genre("Tragedia", "25"),
Genre("Vida Escolar", "26"),
Genre("Historia", "27"),
Genre("Militar", "28"),
Genre("Policiaco", "29"),
Genre("Crimen", "30"),
Genre("Superpoderes", "31"),
Genre("Vampiros", "32"),
Genre("Artes Marciales", "33"),
Genre("Samurái", "34"),
Genre("Género Bender", "35"),
Genre("Realidad Virtual", "36"),
Genre("Ciberpunk", "37"),
Genre("Musica", "38"),
Genre("Parodia", "39"),
Genre("Animación", "40"),
Genre("Demonios", "41"),
Genre("Familia", "42"),
Genre("Extranjero", "43"),
Genre("Niños", "44"),
Genre("Realidad", "45"),
Genre("Telenovela", "46"),
Genre("Guerra", "47"),
Genre("Oeste", "48"),
Genre("Trap", "94"),
)
protected fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE)
protected fun getSFWModePref(): Boolean = preferences.getBoolean(SFW_MODE_PREF, SFW_MODE_PREF_DEFAULT_VALUE)
companion object { companion object {
private const val SCANLATOR_PREF = "scanlatorPref" private const val SCANLATOR_PREF = "scanlatorPref"
@ -647,7 +599,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
private const val SFW_MODE_PREF = "SFWModePref" private const val SFW_MODE_PREF = "SFWModePref"
private const val SFW_MODE_PREF_TITLE = "Ocultar contenido NSFW" 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_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, Harem y Trap."
private const val SFW_MODE_PREF_DEFAULT_VALUE = false private const val SFW_MODE_PREF_DEFAULT_VALUE = false
private val SFW_MODE_PREF_EXCLUDE_GENDERS = listOf("6", "17", "18", "19") private val SFW_MODE_PREF_EXCLUDE_GENDERS = listOf("6", "17", "18", "19")
@ -672,7 +624,7 @@ class TuMangaOnline : ConfigurableSource, ParsedHttpSource() {
private val ENTRIES_ARRAY = listOf(1, 2, 3, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 100).map { i -> i.toString() }.toTypedArray() private val ENTRIES_ARRAY = listOf(1, 2, 3, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 100).map { i -> i.toString() }.toTypedArray()
const val PREFIX_LIBRARY = "library" const val PREFIX_LIBRARY = "library"
const val PREFIX_ID_SEARCH = "id:" const val PREFIX_SLUG_SEARCH = "slug:"
private val SORTABLES = listOf( private val SORTABLES = listOf(
Pair("Me gusta", "likes_count"), Pair("Me gusta", "likes_count"),

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.multisrc.lectortmo
import eu.kanade.tachiyomi.source.model.Filter
class ContentType(name: String, val id: String) : Filter.TriState(name)
class ContentTypeList(content: List<ContentType>) : Filter.Group<ContentType>("Filtrar por tipo de contenido", content)
class Genre(name: String, val id: String) : Filter.TriState(name)
class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Filtrar por géneros", genres)
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
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.multisrc.lectortmo
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class LectorTmoGenerator : ThemeSourceGenerator {
override val themePkg = "lectortmo"
override val themeClass = "LectorTmo"
override val baseVersionCode: Int = 1
override val sources = listOf(
SingleLang("LectorManga", "https://lectormanga.com", "es", isNsfw = true, overrideVersionCode = 34),
SingleLang("TuMangaOnline", "https://visortmo.com", "es", isNsfw = true, overrideVersionCode = 49),
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
LectorTmoGenerator().createAll()
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.es.tumangaonline package eu.kanade.tachiyomi.multisrc.lectortmo
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -7,11 +7,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** class LectorTmoUrlActivity : Activity() {
* Springboard that accepts https://visortmo.com/library/:type/:id/:slug intents and redirects them to
* the main Tachiyomi process.
*/
class TuMangaOnlineUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
@ -23,17 +19,17 @@ class TuMangaOnlineUrlActivity : Activity() {
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${TuMangaOnline.PREFIX_ID_SEARCH}$type/$id/$slug") putExtra("query", "${LectorTmo.PREFIX_SLUG_SEARCH}$type/$id/$slug")
putExtra("filter", packageName) putExtra("filter", packageName)
} }
try { try {
startActivity(mainIntent) startActivity(mainIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e("TMOUrlActivity", e.toString()) Log.e("LectorTmoUrlActivity", e.toString())
} }
} else { } else {
Log.e("TMOUrlActivity", "could not parse uri from intent $intent") Log.e("LectorTmoUrlActivity", "could not parse uri from intent $intent")
} }
finish() finish()

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".es.lectormanga.LectorMangaUrlActivity"
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="lectormanga.com"
android:pathPattern="/gotobook/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,8 +0,0 @@
ext {
extName = 'LectorManga'
extClass = '.LectorManga'
extVersionCode = 34
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -1,660 +0,0 @@
package eu.kanade.tachiyomi.extension.es.lectormanga
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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
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 eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.SimpleDateFormat
import java.util.Locale
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class LectorManga : ConfigurableSource, ParsedHttpSource() {
override val name = "LectorManga"
override val baseUrl = "https://lectormanga.com"
override val lang = "es"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("Referer", "$baseUrl/")
}
private val imageCDNUrls = arrayOf(
"https://img1.followmanga.com",
"https://img1.biggestchef.com",
"https://img1.indalchef.com",
"https://img1.recipesandcook.com",
"https://img1.cyclingte.com",
"https://img1.japanreader.com",
"https://japanreader.com",
)
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun OkHttpClient.Builder.rateLimitImageCDNs(hosts: Array<String>, permits: Int, period: Long): OkHttpClient.Builder {
hosts.forEach { host ->
rateLimitHost(host.toHttpUrl(), permits, period)
}
return this
}
private fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder {
val naiveTrustManager = @SuppressLint("CustomX509TrustManager")
object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) = Unit
}
val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply {
val trustAllCerts = arrayOf<TrustManager>(naiveTrustManager)
init(null, trustAllCerts, SecureRandom())
}.socketFactory
sslSocketFactory(insecureSocketFactory, naiveTrustManager)
hostnameVerifier { _, _ -> true }
return this
}
private val ignoreSslClient = network.client.newBuilder()
.ignoreAllSSLErrors()
.followRedirects(false)
.rateLimit(
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.build()
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val url = request.url
if (url.host.contains("japanreader.com")) {
return@addInterceptor ignoreSslClient.newCall(request).execute()
}
chain.proceed(request)
}
.rateLimitHost(
baseUrl.toHttpUrl(),
preferences.getString(WEB_RATELIMIT_PREF, WEB_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.rateLimitImageCDNs(
imageCDNUrls,
preferences.getString(IMAGE_CDN_RATELIMIT_PREF, IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE)!!.toInt(),
60,
)
.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() = "a[rel='next']"
override fun popularMangaSelector() = ".col-6 .card"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select("a").text()
thumbnail_url = element.select("img").attr("src")
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/library?order_item=creation&order_dir=desc&page=$page", headers)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/library".toHttpUrl().newBuilder()
url.addQueryParameter("title", query)
url.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is Types -> {
url.addQueryParameter("type", filter.toUriPart())
}
is Demography -> {
url.addQueryParameter("demography", filter.toUriPart())
}
is FilterBy -> {
url.addQueryParameter("filter_by", filter.toUriPart())
}
is SortBy -> {
if (filter.state != null) {
url.addQueryParameter("order_item", SORTABLES[filter.state!!.index].second)
url.addQueryParameter(
"order_dir",
if (filter.state!!.ascending) { "asc" } else { "desc" },
)
}
}
is WebcomicFilter -> {
url.addQueryParameter(
"webcomic",
when (filter.state) {
Filter.TriState.STATE_INCLUDE -> "true"
Filter.TriState.STATE_EXCLUDE -> "false"
else -> ""
},
)
}
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) }
}
else -> {}
}
}
return GET(url.build(), 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("h1:has(small)").text()
genre = document.select("a.py-2").joinToString(", ") {
it.text()
}
description = document.select(".col-12.mt-2").text()
status = parseStatus(document.select(".status-publishing").text())
thumbnail_url = document.select(".text-center img.img-fluid").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> = mutableListOf<SChapter>().apply {
val document = response.asJsoup()
// One-shot
if (document.select("#chapters").isEmpty()) {
return document.select(oneShotChapterListSelector()).map { oneShotChapterFromElement(it) }
}
// Regular list of chapters
val chapterNames = document.select("#chapters h4.text-truncate")
val chapterInfos = document.select("#chapters .chapter-list")
chapterNames.forEachIndexed { index, _ ->
val scanlator = chapterInfos[index].select("li")
if (getScanlatorPref()) {
scanlator.forEach { add(regularChapterFromElement(chapterNames[index].text(), it)) }
} else {
scanlator.last { add(regularChapterFromElement(chapterNames[index].text(), it)) }
}
}
}
override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
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"
scanlator = element.select("div.col-12.col-sm-12.col-md-4.text-truncate span").text()
date_upload = element.select("span.badge.badge-primary.p-2").first()?.text()?.let { parseChapterDate(it) }
?: 0
}
private fun regularChapterFromElement(chapterName: String, info: Element) = SChapter.create().apply {
url = info.select("div.row > .text-right > a").attr("href")
name = chapterName
scanlator = info.select("div.col-12.col-sm-12.col-md-4.text-truncate span").text()
date_upload = info.select("span.badge.badge-primary.p-2").first()?.text()?.let {
parseChapterDate(it)
} ?: 0
}
private fun parseChapterDate(date: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.parse(date)?.time ?: 0
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(chapter.url, headers)
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
var doc = redirectToReadPage(document)
val currentUrl = doc.location()
val newUrl = if (!currentUrl.contains("cascade")) {
currentUrl.substringBefore("paginated") + "cascade"
} else {
currentUrl
}
if (currentUrl != newUrl) {
val redirectHeaders = super.headersBuilder()
.set("Referer", doc.location())
.build()
doc = client.newCall(GET(newUrl, redirectHeaders)).execute().asJsoup()
}
doc.select("div.viewer-container img:not(noscript img)").forEach {
add(
Page(
size,
doc.location(),
it.let {
if (it.hasAttr("data-src")) {
it.attr("abs:data-src")
} else {
it.attr("abs:src")
}
},
),
)
}
}
private fun redirectToReadPage(document: Document): Document {
val script1 = document.selectFirst("script:containsData(uniqid)")
val script2 = document.selectFirst("script:containsData(window.location.replace)")
val script3 = document.selectFirst("script:containsData(redirectUrl)")
val script4 = document.selectFirst("input#redir")
val script5 = document.selectFirst("script:containsData(window.opener):containsData(location.replace)")
val redirectHeaders = super.headersBuilder()
.set("Referer", document.location())
.build()
if (script1 != null) {
val data = script1.data()
val regexParams = """\{uniqid:'(.+)',cascade:(.+)\}""".toRegex()
val regexAction = """form\.action\s?=\s?'(.+)'""".toRegex()
val params = regexParams.find(data)
val action = regexAction.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (params != null && action != null) {
val formBody = FormBody.Builder()
.add("uniqid", params.groupValues[1])
.add("cascade", params.groupValues[2])
.build()
return redirectToReadPage(client.newCall(POST(action, redirectHeaders, formBody)).execute().asJsoup())
}
}
if (script2 != null) {
val data = script2.data()
val regexRedirect = """window\.location\.replace\(['"](.+)['"]\)""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadPage(client.newCall(GET(url, redirectHeaders)).execute().asJsoup())
}
}
if (script3 != null) {
val data = script3.data()
val regexRedirect = """redirectUrl\s?=\s?'(.+)'""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadPage(client.newCall(GET(url, redirectHeaders)).execute().asJsoup())
}
}
if (script4 != null) {
val url = script4.attr("value").unescapeUrl()
return redirectToReadPage(client.newCall(GET(url, redirectHeaders)).execute().asJsoup())
}
if (script5 != null) {
val data = script5.data()
val regexRedirect = """;[^.]location\.replace\(['"](.+)['"]\)""".toRegex()
val url = regexRedirect.find(data)?.groupValues?.get(1)?.unescapeUrl()
if (url != null) {
return redirectToReadPage(client.newCall(GET(url, redirectHeaders)).execute().asJsoup())
}
}
return document
}
private fun String.unescapeUrl(): String {
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
this.replace("\\/", "/")
} else {
this
}
}
override fun imageRequest(page: Page) = GET(
url = page.imageUrl!!,
headers = headers.newBuilder()
.removeAll("Referer")
.add("Referer", page.url.substringBefore("news/"))
.build(),
)
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/$MANGA_URL_CHUNK/$id", headers)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(realQuery))
.asObservableSuccess()
.map { response ->
val details = mangaDetailsParse(response)
details.url = "/$MANGA_URL_CHUNK/$realQuery"
MangasPage(listOf(details), false)
}
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
}
private class Types : UriPartFilter(
"Filtrar por tipo",
arrayOf(
Pair("Ver todos", ""),
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
Pair("Manhwa", "manhwa"),
Pair("Novela", "novel"),
Pair("One shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Oel", "oel"),
),
)
private class Demography : UriPartFilter(
"Filtrar por demografía",
arrayOf(
Pair("Ver todas", ""),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Josei", "josei"),
Pair("Kodomo", "kodomo"),
),
)
private class FilterBy : UriPartFilter(
"Campo de orden",
arrayOf(
Pair("Título", "title"),
Pair("Autor", "author"),
Pair("Compañia", "company"),
),
)
class SortBy : Filter.Sort(
"Ordenar por",
SORTABLES.map { it.first }.toTypedArray(),
Selection(0, false),
)
private class WebcomicFilter : Filter.TriState("Webcomic")
private class FourKomaFilter : Filter.TriState("Yonkoma")
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 GenreList(genres: List<Genre>) : Filter.Group<Genre>("Filtrar por géneros", genres)
override fun getFilterList() = FilterList(
Types(),
Demography(),
Filter.Separator(),
FilterBy(),
SortBy(),
Filter.Separator(),
WebcomicFilter(),
FourKomaFilter(),
AmateurFilter(),
EroticFilter(),
GenreList(getGenreList()),
)
// Array.from(document.querySelectorAll('#advancedSearch .custom-checkbox'))
// .map(a => `Genre("${a.querySelector('label').innerText}", "${a.querySelector('input').value}")`).join(',\n')
// on https://lectormanga.com/library
// Last revision 30/08/2021
private fun getGenreList() = listOf(
Genre("Acción", "1"),
Genre("Aventura", "2"),
Genre("Comedia", "3"),
Genre("Drama", "4"),
Genre("Recuentos de la vida", "5"),
Genre("Ecchi", "6"),
Genre("Fantasia", "7"),
Genre("Magia", "8"),
Genre("Sobrenatural", "9"),
Genre("Horror", "10"),
Genre("Misterio", "11"),
Genre("Psicológico", "12"),
Genre("Romance", "13"),
Genre("Ciencia Ficción", "14"),
Genre("Thriller", "15"),
Genre("Deporte", "16"),
Genre("Girls Love", "17"),
Genre("Boys Love", "18"),
Genre("Harem", "19"),
Genre("Mecha", "20"),
Genre("Supervivencia", "21"),
Genre("Reencarnación", "22"),
Genre("Gore", "23"),
Genre("Apocalíptico", "24"),
Genre("Tragedia", "25"),
Genre("Vida Escolar", "26"),
Genre("Historia", "27"),
Genre("Militar", "28"),
Genre("Policiaco", "29"),
Genre("Crimen", "30"),
Genre("Superpoderes", "31"),
Genre("Vampiros", "32"),
Genre("Artes Marciales", "33"),
Genre("Samurái", "34"),
Genre("Género Bender", "35"),
Genre("Realidad Virtual", "36"),
Genre("Ciberpunk", "37"),
Genre("Musica", "38"),
Genre("Parodia", "39"),
Genre("Animación", "40"),
Genre("Demonios", "41"),
Genre("Familia", "42"),
Genre("Extranjero", "43"),
Genre("Niños", "44"),
Genre("Realidad", "45"),
Genre("Telenovela", "46"),
Genre("Guerra", "47"),
Genre("Oeste", "48"),
)
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
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
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()
}
}
// 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(apiRateLimitPreference)
screen.addPreference(imgCDNRateLimitPreference)
}
private fun getScanlatorPref(): Boolean = preferences.getBoolean(SCANLATOR_PREF, SCANLATOR_PREF_DEFAULT_VALUE)
companion object {
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 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. App 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 la app. \nValor actual: %s"
private const val WEB_RATELIMIT_PREF_DEFAULT_VALUE = "8"
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. App 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 la app. \nValor actual: %s"
private const val IMAGE_CDN_RATELIMIT_PREF_DEFAULT_VALUE = "50"
private val ENTRIES_ARRAY = arrayOf("1", "2", "3", "5", "6", "7", "8", "9", "10", "15", "20", "30", "40", "50", "100")
const val PREFIX_ID_SEARCH = "id:"
const val MANGA_URL_CHUNK = "gotobook"
private val SORTABLES = listOf(
Pair("Me gusta", "likes_count"),
Pair("Alfabético", "alphabetically"),
Pair("Puntuación", "score"),
Pair("Creación", "creation"),
Pair("Fecha estreno", "release_date"),
)
}
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.extension.es.lectormanga
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://lectormanga.com/gotobook/:id intents and redirects them to
* the main Tachiyomi process.
*/
class LectorMangaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${LectorManga.PREFIX_ID_SEARCH}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("LectorMangaUrlActivity", e.toString())
}
} else {
Log.e("LectorMangaUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,8 +0,0 @@
ext {
extName = 'TuMangaOnline'
extClass = '.TuMangaOnline'
extVersionCode = 49
isNsfw = true
}
apply from: "$rootDir/common.gradle"