Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Brakeout'
pkgNameSuffix = 'es.brakeout'
extClass = '.Brakeout'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,182 @@
package eu.kanade.tachiyomi.extension.es.brakeout
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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 kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
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 uy.kohesive.injekt.injectLazy
import java.lang.IllegalArgumentException
import java.util.Calendar
class Brakeout : ParsedHttpSource() {
override val name = "Brakeout"
override val baseUrl = "https://brakeout.xyz"
override val lang = "es"
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularMangaSelector(): String = "div#div-diario figure, div#div-semanal figure, div#div-mensual figure"
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaParse(response: Response): MangasPage {
val mangasPage = super.popularMangaParse(response)
val distinctList = mangasPage.mangas.distinctBy { it.url }
return MangasPage(distinctList, mangasPage.hasNextPage)
}
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst("figcaption")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
}
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
override fun latestUpdatesSelector(): String = "section.flex > div.grid > figure"
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst("figcaption")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
if (query.length > 1) return GET("$baseUrl/comics#$query", headers)
throw Exception("La búsqueda debe tener al menos 2 caracteres")
}
return GET("$baseUrl/comics?page=$page", headers)
}
override fun searchMangaSelector(): String = "section.flex > div.grid > figure"
override fun searchMangaNextPageSelector(): String = "main.container section.flex > div > a:containsOwn(Siguiente)"
override fun searchMangaParse(response: Response): MangasPage {
val query = response.request.url.fragment ?: return super.searchMangaParse(response)
val document = response.asJsoup()
val mangas = parseMangaList(document, query)
return MangasPage(mangas, false)
}
private fun parseMangaList(document: Document, query: String): List<SManga> {
val docString = document.toString()
val mangaListJson = JSON_PROJECT_LIST.find(docString)?.destructured?.toList()?.get(0).orEmpty()
return try {
json.decodeFromString<List<SerieDto>>(mangaListJson)
.filter { it.title.contains(query, ignoreCase = true) }
.map {
SManga.create().apply {
title = it.title
thumbnail_url = it.thumbnail
url = "/ver/${it.id}/${it.slug}"
}
}
} catch (_: IllegalArgumentException) {
emptyList()
}
}
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst("figcaption")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
}
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
with(document.select("section#section-sinopsis")) {
description = select("p").text()
genre = select("div.flex:has(div:containsOwn(Géneros)) > div > a > span").joinToString { it.text() }
}
}
override fun chapterListSelector(): String = "section#section-list-cap div.grid-capitulos > div > a.group"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("div#name")!!.text()
date_upload = parseRelativeDate(element.selectFirst("time")!!.text())
}
override fun pageListParse(document: Document): List<Page> {
return document.select("section > div > img.readImg").mapIndexed { i, element ->
Page(i, "", element.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used!")
override fun getFilterList(): FilterList {
return FilterList(
Filter.Header("Limpie la barra de búsqueda y haga click en 'Filtrar' para mostrar todas las series."),
)
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("segundo").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minuto").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hora").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("día", "dia").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("semana").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
WordSet("mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
class WordSet(private vararg val words: String) {
fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
}
@Serializable
data class SerieDto(
val id: Int,
@SerialName("nombre") val title: String,
val slug: String,
@SerialName("portada") val thumbnail: String,
)
companion object {
private val JSON_PROJECT_LIST = """proyectos\s*=\s*(\[[\s\S]+?\])\s*;""".toRegex()
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Cerberus Series'
pkgNameSuffix = 'es.cerberusseries'
extClass = '.CerberusSeries'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,107 @@
package eu.kanade.tachiyomi.extension.es.cerberusseries
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.FilterList
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 okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
import java.util.Calendar
class CerberusSeries : ParsedHttpSource() {
override val name = "Cerberus Series"
override val baseUrl = "https://cerberuseries.xyz"
override val lang = "es"
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/comics?page=$page", headers)
override fun popularMangaSelector(): String = "div.grid > div:has(> div.c-iZMlIN)"
override fun popularMangaNextPageSelector(): String = "nav[role=navigation] a:contains(»), nav[role=navigation] a:contains(Next)"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.select("div.c-hCLgme a").attr("href"))
title = element.select("div.c-hCLgme a").text()
thumbnail_url = element.selectFirst("div.c-iZMlIN img")?.attr("abs:src")
}
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
override fun latestUpdatesSelector(): String = popularMangaSelector()
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw IOException("Esta funcionalidad aún no esta implementada.")
override fun searchMangaSelector(): String = throw UnsupportedOperationException("Not used!")
override fun searchMangaNextPageSelector(): String = throw UnsupportedOperationException("Not used!")
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used!")
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
thumbnail_url = document.selectFirst("div.thumb-wrapper img")!!.attr("abs:src")
title = document.selectFirst("div.series-title")!!.text()
genre = document.select("div.tags-container span").joinToString { it.text() }
description = document.selectFirst("div.description-container")!!.text()
author = document.select("div.useful-container p:containsOwn(Autor) strong").text()
}
override fun chapterListSelector(): String = "div.chapters-list-wrapper ul a"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("li span")!!.text()
date_upload = parseRelativeDate(element.selectFirst("li p")!!.text())
}
override fun pageListParse(document: Document): List<Page> {
return document.select("div.main-content p > img").mapIndexed { i, element ->
Page(i, "", element.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used!")
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
WordSet("segundo").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minuto").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hora").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("día", "dia").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("semana").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
WordSet("mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
class WordSet(private vararg val words: String) {
fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'HeavenManga'
pkgNameSuffix = 'es.heavenmanga'
extClass = '.HeavenManga'
extVersionCode = 6
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -0,0 +1,320 @@
package eu.kanade.tachiyomi.extension.es.heavenmanga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class HeavenManga : ParsedHttpSource() {
override val name = "HeavenManga"
override val baseUrl = "https://heavenmanga.com"
override val lang = "es"
// latest is broken on the site, it's the same as popular so turning it off
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) Gecko/20100101 Firefox/75")
}
override fun popularMangaSelector() = "div.page-item-detail"
override fun latestUpdatesSelector() = "#container .ultimos_epis .not"
override fun searchMangaSelector() = "div.c-tabs-item__content, ${popularMangaSelector()}"
override fun chapterListSelector() = "div.listing-chapters_wrap tr"
override fun popularMangaNextPageSelector() = "ul.pagination a[rel=next]"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/top?orderby=views&page=$page", headers)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val searchUrl = "$baseUrl/buscar?query=$query"
// Filter
val pageParameter = if (page > 1) "?page=$page" else ""
if (query.isBlank()) {
val ext = ".html"
var name: String
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
if (filter.toUriPart().isNotBlank() && filter.state != 0) {
name = filter.toUriPart()
return GET("$baseUrl/genero/$name$ext$pageParameter", headers)
}
}
is AlphabeticoFilter -> {
if (filter.toUriPart().isNotBlank() && filter.state != 0) {
name = filter.toUriPart()
return GET("$baseUrl/letra/$name$ext$pageParameter", headers)
}
}
is ListaCompletasFilter -> {
if (filter.toUriPart().isNotBlank() && filter.state != 0) {
name = filter.toUriPart()
return GET("$baseUrl/$name$pageParameter", headers)
}
}
else -> {}
}
}
}
return GET(searchUrl, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
return if (response.request.url.toString().contains("query=")) {
super.searchMangaParse(response)
} else {
popularMangaParse(response)
}
}
// get contents of a url
private fun getUrlContents(url: String): Document = client.newCall(GET(url, headers)).execute().asJsoup()
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("div.manga-name").text()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = element.select("img").attr("abs:src")
}
}
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
element.select("a").let {
val latestChapter = getUrlContents(it.attr("href"))
val url = latestChapter.select(".rpwe-clearfix:last-child a")
setUrlWithoutDomain(url.attr("href"))
title = it.select("span span").text()
thumbnail_url = it.select("img").attr("src")
}
}
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.select("h4 a").let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
}
thumbnail_url = element.select("img").attr("abs:data-src")
}
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a").let {
name = it.text()
setUrlWithoutDomain(it.attr("href"))
}
scanlator = element.select("span.pull-right").text()
}
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.select("div.tab-summary").let { info ->
genre = info.select("div.genres-content a").joinToString { it.text() }
thumbnail_url = info.select("div.summary_image img").attr("abs:data-src")
}
description = document.select("div.description-summary p").text()
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
override fun pageListRequest(chapter: SChapter): Request {
return getUrlContents(baseUrl + chapter.url).select("a[id=leer]").attr("abs:href")
.let { GET(it, headers) }
}
override fun pageListParse(document: Document): List<Page> {
return document.select("script:containsData(pUrl)").first()!!.data()
.substringAfter("pUrl=[").substringBefore("\"},];").split("\"},")
.mapIndexed { i, string -> Page(i, "", string.substringAfterLast("\"")) }
}
/**
* Array.from(document.querySelectorAll('.categorias a')).map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n')
* on https://heavenmanga.com/top/
* */
private class GenreFilter : UriPartFilter(
"Géneros",
arrayOf(
Pair("Todo", ""),
Pair("Accion", "accion"),
Pair("Adulto", "adulto"),
Pair("Aventura", "aventura"),
Pair("Artes Marciales", "artes+marciales"),
Pair("Acontesimientos de la Vida", "acontesimientos+de+la+vida"),
Pair("Bakunyuu", "bakunyuu"),
Pair("Sci-fi", "sci-fi"),
Pair("Comic", "comic"),
Pair("Combate", "combate"),
Pair("Comedia", "comedia"),
Pair("Cooking", "cooking"),
Pair("Cotidiano", "cotidiano"),
Pair("Colegialas", "colegialas"),
Pair("Critica social", "critica+social"),
Pair("Ciencia ficcion", "ciencia+ficcion"),
Pair("Cambio de genero", "cambio+de+genero"),
Pair("Cosas de la Vida", "cosas+de+la+vida"),
Pair("Drama", "drama"),
Pair("Deporte", "deporte"),
Pair("Doujinshi", "doujinshi"),
Pair("Delincuentes", "delincuentes"),
Pair("Ecchi", "ecchi"),
Pair("Escolar", "escolar"),
Pair("Erotico", "erotico"),
Pair("Escuela", "escuela"),
Pair("Estilo de Vida", "estilo+de+vida"),
Pair("Fantasia", "fantasia"),
Pair("Fragmentos de la Vida", "fragmentos+de+la+vida"),
Pair("Gore", "gore"),
Pair("Gender Bender", "gender+bender"),
Pair("Humor", "humor"),
Pair("Harem", "harem"),
Pair("Haren", "haren"),
Pair("Hentai", "hentai"),
Pair("Horror", "horror"),
Pair("Historico", "historico"),
Pair("Josei", "josei"),
Pair("Loli", "loli"),
Pair("Light", "light"),
Pair("Lucha Libre", "lucha+libre"),
Pair("Manga", "manga"),
Pair("Mecha", "mecha"),
Pair("Magia", "magia"),
Pair("Maduro", "maduro"),
Pair("Manhwa", "manhwa"),
Pair("Manwha", "manwha"),
Pair("Mature", "mature"),
Pair("Misterio", "misterio"),
Pair("Mutantes", "mutantes"),
Pair("Novela", "novela"),
Pair("Orgia", "orgia"),
Pair("OneShot", "oneshot"),
Pair("OneShots", "oneshots"),
Pair("Psicologico", "psicologico"),
Pair("Romance", "romance"),
Pair("Recuentos de la vida", "recuentos+de+la+vida"),
Pair("Smut", "smut"),
Pair("Shojo", "shojo"),
Pair("Shonen", "shonen"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Suspenso", "suspenso"),
Pair("School Life", "school+life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("SuperHeroes", "superheroes"),
Pair("Supernatural", "supernatural"),
Pair("Slice of Life", "slice+of+life"),
Pair("Super Poderes", "ssuper+poderes"),
Pair("Terror", "terror"),
Pair("Torneo", "torneo"),
Pair("Tragedia", "tragedia"),
Pair("Transexual", "transexual"),
Pair("Vida", "vida"),
Pair("Vampiros", "vampiros"),
Pair("Violencia", "violencia"),
Pair("Vida Pasada", "vida+pasada"),
Pair("Vida Cotidiana", "vida+cotidiana"),
Pair("Vida de Escuela", "vida+de+escuela"),
Pair("Webtoon", "webtoon"),
Pair("Webtoons", "webtoons"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
/**
* Array.from(document.querySelectorAll('.letras a')).map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n')
* on https://heavenmanga.com/top/
* */
private class AlphabeticoFilter : UriPartFilter(
"Alfabético",
arrayOf(
Pair("Todo", ""),
Pair("A", "a"),
Pair("B", "b"),
Pair("C", "c"),
Pair("D", "d"),
Pair("E", "e"),
Pair("F", "f"),
Pair("G", "g"),
Pair("H", "h"),
Pair("I", "i"),
Pair("J", "j"),
Pair("K", "k"),
Pair("L", "l"),
Pair("M", "m"),
Pair("N", "n"),
Pair("O", "o"),
Pair("P", "p"),
Pair("Q", "q"),
Pair("R", "r"),
Pair("S", "s"),
Pair("T", "t"),
Pair("U", "u"),
Pair("V", "v"),
Pair("W", "w"),
Pair("X", "x"),
Pair("Y", "y"),
Pair("Z", "z"),
Pair("0-9", "0-9"),
),
)
/**
* Array.from(document.querySelectorAll('#t li a')).map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n')
* on https://heavenmanga.com/top/
* */
private class ListaCompletasFilter : UriPartFilter(
"Lista Completa",
arrayOf(
Pair("Todo", ""),
Pair("Lista Comis", "comic"),
Pair("Lista Novelas", "novela"),
Pair("Lista Adulto", "adulto"),
),
)
override fun getFilterList() = FilterList(
// Search and filter don't work at the same time
Filter.Header("NOTA: Los filtros se ignoran si se utiliza la búsqueda de texto."),
Filter.Header("Sólo se puede utilizar un filtro a la vez."),
Filter.Separator(),
GenreFilter(),
AlphabeticoFilter(),
ListaCompletasFilter(),
)
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
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Ikigai Mangas'
pkgNameSuffix = 'es.ikigaimangas'
extClass = '.IkigaiMangas'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,216 @@
package eu.kanade.tachiyomi.extension.es.ikigaimangas
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class IkigaiMangas : HttpSource() {
override val baseUrl: String = "https://ikigaimangas.com"
private val apiBaseUrl: String = "https://panel.ikigaimangas.com"
private val pageViewerUrl: String = "https://ikigaitoon.com"
override val lang: String = "es"
override val name: String = "Ikigai Mangas"
override val supportsLatest: Boolean = true
override val client = super.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 1, 2)
.rateLimitHost(apiBaseUrl.toHttpUrl(), 2, 1)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override fun popularMangaRequest(page: Int): Request {
val apiUrl = "$apiBaseUrl/api/swf/series?page=$page&column=view_count&direction=desc"
return GET(apiUrl, headers)
}
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request {
val apiUrl = "$apiBaseUrl/api/swf/series?page=$page&column=last_chapter_date&direction=desc"
return GET(apiUrl, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
val apiUrl = "$apiBaseUrl/api/swf/series".toHttpUrl().newBuilder()
if (query.isNotEmpty()) apiUrl.addQueryParameter("search", query)
apiUrl.addQueryParameter("page", page.toString())
val genres = filters.firstInstanceOrNull<GenreFilter>()?.state.orEmpty()
.filter(Genre::state)
.map(Genre::id)
.joinToString(",")
val statuses = filters.firstInstanceOrNull<StatusFilter>()?.state.orEmpty()
.filter(Status::state)
.map(Status::id)
.joinToString(",")
if (genres.isNotEmpty()) apiUrl.addQueryParameter("genres", genres)
if (statuses.isNotEmpty()) apiUrl.addQueryParameter("status", statuses)
apiUrl.addQueryParameter("column", sortByFilter?.selected ?: "name")
apiUrl.addQueryParameter("direction", if (sortByFilter?.state?.ascending == true) "asc" else "desc")
apiUrl.addQueryParameter("type", "comic")
return GET(apiUrl.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage {
runCatching { fetchFilters() }
val result = json.decodeFromString<PayloadSeriesDto>(response.body.string())
val mangaList = result.data.filter { it.type == "comic" }.map {
SManga.create().apply {
url = "/series/comic-${it.slug}#${it.id}"
title = it.name
thumbnail_url = it.cover
}
}
val hasNextPage = result.currentPage < result.lastPage
return MangasPage(mangaList, hasNextPage)
}
override fun mangaDetailsParse(response: Response): SManga {
val slug = response.request.url
.toString()
.substringAfter("/series/comic-")
.substringBefore("#")
val apiUrl = "$apiBaseUrl/api/swf/series/$slug".toHttpUrl()
val newResponse = client.newCall(GET(url = apiUrl, headers = headers)).execute()
val result = json.decodeFromString<PayloadSeriesDetailsDto>(newResponse.body.string())
return SManga.create().apply {
title = result.series.name
thumbnail_url = result.series.cover
description = result.series.summary
status = parseStatus(result.series.status?.id)
genre = result.series.genres?.joinToString { it.name.trim() }
}
}
override fun getChapterUrl(chapter: SChapter): String = pageViewerUrl + chapter.url
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url.substringAfterLast("#")
return GET("$apiBaseUrl/api/swf/series/$id/chapter-list")
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.decodeFromString<PayloadChaptersDto>(response.body.string())
return result.data.map {
SChapter.create().apply {
url = "/capitulo/${it.id}"
name = "Capítulo ${it.name}"
date_upload = runCatching { dateFormat.parse(it.date)?.time }
.getOrNull() ?: 0L
}
}.reversed()
}
override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url.substringAfter("/capitulo/")
return GET("$apiBaseUrl/api/swf/chapters/$id")
}
override fun pageListParse(response: Response): List<Page> {
return json.decodeFromString<PayloadPagesDto>(response.body.string()).chapter.pages.mapIndexed { i, img ->
Page(i, "", img)
}
}
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
private fun parseStatus(statusId: Long?) = when (statusId) {
906397890812182531, 911437469204086787 -> SManga.ONGOING
906409397258190851 -> SManga.ON_HIATUS
906409532796731395, 911793517664960513 -> SManga.COMPLETED
906426661911756802, 906428048651190273, 911793767845265410, 911793856861798402 -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
data class SortProperty(val name: String, val value: String) {
override fun toString(): String = name
}
private fun getSortProperties(): List<SortProperty> = listOf(
SortProperty("Nombre", "name"),
SortProperty("Creado en", "created_at"),
SortProperty("Actualización más reciente", "last_chapter_date"),
SortProperty("Número de favoritos", "bookmark_count"),
SortProperty("Número de valoración", "rating_count"),
SortProperty("Número de vistas", "view_count"),
)
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>(
SortByFilter("Ordenar por", getSortProperties()),
)
filters += if (genresList.isNotEmpty() || statusesList.isNotEmpty()) {
listOf(
StatusFilter("Estados", getStatusFilters()),
GenreFilter("Géneros", getGenreFilters()),
)
} else {
listOf(
Filter.Header("Presione 'Restablecer' para intentar cargar los filtros"),
)
}
return FilterList(filters)
}
private fun getGenreFilters(): List<Genre> = genresList.map { Genre(it.first, it.second) }
private fun getStatusFilters(): List<Status> = statusesList.map { Status(it.first, it.second) }
private var genresList: List<Pair<String, Long>> = emptyList()
private var statusesList: List<Pair<String, Long>> = emptyList()
private var fetchFiltersAttempts = 0
private var fetchFiltersFailed = false
private fun fetchFilters() {
if (fetchFiltersAttempts <= 3 && ((genresList.isEmpty() && statusesList.isEmpty()) || fetchFiltersFailed)) {
val filters = runCatching {
val response = client.newCall(GET("$apiBaseUrl/api/swf/filter-options", headers)).execute()
json.decodeFromString<PayloadFiltersDto>(response.body.string())
}
fetchFiltersFailed = filters.isFailure
genresList = filters.getOrNull()?.data?.genres?.map { it.name.trim() to it.id } ?: emptyList()
statusesList = filters.getOrNull()?.data?.statuses?.map { it.name.trim() to it.id } ?: emptyList()
}
}
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull()
}

View File

@@ -0,0 +1,73 @@
package eu.kanade.tachiyomi.extension.es.ikigaimangas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PayloadSeriesDto(
val data: List<SeriesDto>,
@SerialName("current_page")val currentPage: Int = 0,
@SerialName("last_page") val lastPage: Int = 0,
)
@Serializable
data class SeriesDto(
val id: Long,
val name: String,
val slug: String,
val cover: String? = null,
val type: String? = null,
val summary: String? = null,
val status: SeriesStatusDto? = null,
val genres: List<FilterDto>? = null,
)
@Serializable
data class PayloadSeriesDetailsDto(
val series: SeriesDto,
)
@Serializable
data class PayloadChaptersDto(
var data: List<ChapterDto>,
)
@Serializable
data class ChapterDto(
val id: Long,
val name: String,
@SerialName("published_at") val date: String,
)
@Serializable
data class PayloadPagesDto(
val chapter: PageDto,
)
@Serializable
data class PageDto(
val pages: List<String>,
)
@Serializable
data class SeriesStatusDto(
val id: Long,
val name: String,
)
@Serializable
data class PayloadFiltersDto(
val data: GenresStatusesDto,
)
@Serializable
data class GenresStatusesDto(
val genres: List<FilterDto>,
val statuses: List<FilterDto>,
)
@Serializable
data class FilterDto(
val id: Long,
val name: String,
)

View File

@@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.extension.es.ikigaimangas
import eu.kanade.tachiyomi.source.model.Filter
class Genre(title: String, val id: Long) : Filter.CheckBox(title)
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
class Status(title: String, val id: Long) : Filter.CheckBox(title)
class StatusFilter(title: String, statuses: List<Status>) : Filter.Group<Status>(title, statuses)
class SortByFilter(title: String, private val sortProperties: List<IkigaiMangas.SortProperty>) : Filter.Sort(
title,
sortProperties.map { it.name }.toTypedArray(),
Selection(0, ascending = true),
) {
val selected: String
get() = sortProperties[state!!.index].value
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Ikuhentai'
pkgNameSuffix = 'es.ikuhentai'
extClass = '.Ikuhentai'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,270 @@
package eu.kanade.tachiyomi.extension.es.ikuhentai
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class Ikuhentai : ParsedHttpSource() {
override val name = "Ikuhentai"
override val baseUrl = "https://ikuhentai.net/"
override val lang = "es"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/page/$page?s&post_type=wp-manga&m_orderby=views", headers)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/page/$page?s&post_type=wp-manga&m_orderby=latest", headers)
}
// LIST SELECTOR
override fun popularMangaSelector() = "div.c-tabs-item__content"
override fun latestUpdatesSelector() = popularMangaSelector()
override fun searchMangaSelector() = popularMangaSelector()
// ELEMENT
override fun popularMangaFromElement(element: Element): SManga = searchMangaFromElement(element)
override fun latestUpdatesFromElement(element: Element): SManga = searchMangaFromElement(element)
// NEXT SELECTOR
override fun popularMangaNextPageSelector() = "a.nextpostslink"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.select("img").attr("data-src")
element.select("div.tab-thumb > a").first()!!.let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title")
}
return manga
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/page/$page".toHttpUrlOrNull()!!.newBuilder()
url.addQueryParameter("post_type", "wp-manga")
val pattern = "\\s+".toRegex()
val q = query.replace(pattern, "+")
if (query.isNotEmpty()) {
url.addQueryParameter("s", q)
} else {
url.addQueryParameter("s", "")
}
var orderBy: String
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
// is Status -> url.addQueryParameter("manga_status", arrayOf("", "completed", "ongoing")[filter.state])
is GenreList -> {
val genreInclude = mutableListOf<String>()
filter.state.forEach {
if (it.state == 1) {
genreInclude.add(it.id)
}
}
if (genreInclude.isNotEmpty()) {
genreInclude.forEach { genre ->
url.addQueryParameter("genre[]", genre)
}
}
}
is StatusList -> {
val statuses = mutableListOf<String>()
filter.state.forEach {
if (it.state == 1) {
statuses.add(it.id)
}
}
if (statuses.isNotEmpty()) {
statuses.forEach { status ->
url.addQueryParameter("status[]", status)
}
}
}
is SortBy -> {
orderBy = filter.toUriPart()
url.addQueryParameter("m_orderby", orderBy)
}
is TextField -> url.addQueryParameter(filter.key, filter.state)
else -> {}
}
}
return GET(url.toString(), headers)
}
// max 200 results
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div.site-content").first()!!
val manga = SManga.create()
manga.author = infoElement.select("div.author-content").text()
manga.artist = infoElement.select("div.artist-content").text()
val genres = mutableListOf<String>()
infoElement.select("div.genres-content a").forEach { element ->
val genre = element.text()
genres.add(genre)
}
manga.genre = genres.joinToString(", ")
manga.status = parseStatus(infoElement.select("div.post-status > div:nth-child(2) > div.summary-content").text())
manga.description = document.select("div.description-summary").text()
manga.thumbnail_url = document.select("div.summary_image > a > img").attr("data-src")
return manga
}
private fun parseStatus(element: String): Int = when {
element.lowercase().contains("ongoing") -> SManga.ONGOING
element.lowercase().contains("completado") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "li.wp-manga-chapter"
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a").first()!!
var url = urlElement.attr("href")
url = url.replace("/p/1", "")
url += "?style=list"
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(url)
chapter.name = urlElement.text()
return chapter
}
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
val basic = Regex("""Chapter\s([0-9]+)""")
when {
basic.containsMatchIn(chapter.name) -> {
basic.find(chapter.name)?.let {
chapter.chapter_number = it.groups[1]?.value!!.toFloat()
}
}
}
}
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
var i = 0
document.select("div.reading-content * img").forEach { element ->
val url = element.attr("data-src")
i++
if (url.isNotEmpty()) {
pages.add(Page(i, "", url))
}
}
return pages
}
override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val imgHeader = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30")
add("Referer", baseUrl)
}.build()
return GET(page.imageUrl!!, imgHeader)
}
// private class Status : Filter.TriState("Completed")
private class TextField(name: String, val key: String) : Filter.Text(name)
private class SortBy : UriPartFilter(
"Ordenar por",
arrayOf(
Pair("Relevance", ""),
Pair("Latest", "latest"),
Pair("A-Z", "alphabet"),
Pair("Calificación", "rating"),
Pair("Tendencia", "trending"),
Pair("Más visto", "views"),
Pair("Nuevo", "new-manga"),
),
)
private class Genre(name: String, val id: String = name) : Filter.TriState(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
private class Status(name: String, val id: String = name) : Filter.TriState(name)
private class StatusList(statuses: List<Status>) : Filter.Group<Status>("Estado", statuses)
override fun getFilterList() = FilterList(
// TextField("Judul", "title"),
TextField("Autor", "author"),
TextField("Año de publicación", "release"),
SortBy(),
StatusList(getStatusList()),
GenreList(getGenreList()),
)
private fun getStatusList() = listOf(
Status("Completado", "end"),
Status("En emisión", "on-going"),
Status("Cancelado", "canceled"),
Status("Pausado", "on-hold"),
)
private fun getGenreList() = listOf(
Genre("Ahegao", "ahegao"),
Genre("Anal", "anal"),
Genre("Bestiality", "bestialidad"),
Genre("Bondage", "bondage"),
Genre("Bukkake", "bukkake"),
Genre("Chicas monstruo", "chicas-monstruo"),
Genre("Chikan", "chikan"),
Genre("Colegialas", "colegialas"),
Genre("Comics porno", "comics-porno"),
Genre("Dark Skin", "dark-skin"),
Genre("Demonios", "demonios"),
Genre("Ecchi", "ecchi"),
Genre("Embarazadas", "embarazadas"),
Genre("Enfermeras", "enfermeras"),
Genre("Eroges", "eroges"),
Genre("Fantasía", "fantasia"),
Genre("Futanari", "futanari"),
Genre("Gangbang", "gangbang"),
Genre("Gemelas", "gemelas"),
Genre("Gender Bender", "gender-bender"),
Genre("Gore", "gore"),
Genre("Handjob", "handjob"),
Genre("Harem", "harem"),
Genre("Hipnosis", "hipnosis"),
Genre("Incesto", "incesto"),
Genre("Loli", "loli"),
Genre("Maids", "maids"),
Genre("Masturbación", "masturbacion"),
Genre("Milf", "milf"),
Genre("Mind Break", "mind-break"),
Genre("My Hero Academia", "my-hero-academia"),
Genre("Naruto", "naruto"),
Genre("Netorare", "netorare"),
Genre("Paizuri", "paizuri"),
Genre("Pokemon", "pokemon"),
Genre("Profesora", "profesora"),
Genre("Prostitución", "prostitucion"),
Genre("Romance", "romance"),
Genre("Straight Shota", "straight-shota"),
Genre("Tentáculos", "tentaculos"),
Genre("Virgen", "virgen"),
Genre("Yaoi", "yaoi"),
Genre("Yuri", "yuri"),
)
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
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'InManga'
pkgNameSuffix = 'es.inmanga'
extClass = '.InManga'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.extension.es.inmanga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class InManga : ParsedHttpSource() {
override val name = "InManga"
override val baseUrl = "https://inmanga.com"
override val lang = "es"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val postHeaders = headers.newBuilder()
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.add("X-Requested-With", "XMLHttpRequest")
.build()
private val json: Json by injectLazy()
private val imageCDN = "https://pack-yak.intomanga.com/"
/**
* Returns RequestBody to retrieve latest or populars Manga.
*
* @param page Current page number.
* @param isPopular If is true filter sortby = 1 else sortby = 3
* sortby = 1: Populars
* sortby = 3: Latest
*/
private fun requestBodyBuilder(page: Int, isPopular: Boolean): RequestBody = "filter%5Bgeneres%5D%5B%5D=-1&filter%5BqueryString%5D=&filter%5Bskip%5D=${(page - 1) * 10}&filter%5Btake%5D=10&filter%5Bsortby%5D=${if (isPopular) "1" else "3"}&filter%5BbroadcastStatus%5D=0&filter%5BonlyFavorites%5D=false&d=".toRequestBody(null)
override fun popularMangaRequest(page: Int) = POST(
url = "$baseUrl/manga/getMangasConsultResult",
headers = postHeaders,
body = requestBodyBuilder(page, true),
)
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element): SManga = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = "body"
override fun latestUpdatesRequest(page: Int) = POST(
url = "$baseUrl/manga/getMangasConsultResult",
headers = postHeaders,
body = requestBodyBuilder(page, false),
)
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element = element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val skip = (page - 1) * 10
val body =
"filter%5Bgeneres%5D%5B%5D=-1&filter%5BqueryString%5D=$query&filter%5Bskip%5D=$skip&filter%5Btake%5D=10&filter%5Bsortby%5D=1&filter%5BbroadcastStatus%5D=0&filter%5BonlyFavorites%5D=false&d=".toRequestBody(
null,
)
return POST("$baseUrl/manga/getMangasConsultResult", postHeaders, body)
}
override fun searchMangaParse(response: Response): MangasPage {
val mangas = mutableListOf<SManga>()
val document = response.asJsoup()
document.select(searchMangaSelector()).map { mangas.add(searchMangaFromElement(it)) }
return MangasPage(mangas, document.select(searchMangaSelector()).count() == 10)
}
override fun searchMangaSelector() = "body > a"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.select("h4.m0").text()
thumbnail_url = element.select("img").attr("abs:data-src")
}
override fun searchMangaNextPageSelector(): String? = null
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.select("div.col-md-3 div.panel.widget").let { info ->
thumbnail_url = info.select("img").attr("abs:src")
status = parseStatus(info.select(" a.list-group-item:contains(estado) span").text())
}
document.select("div.col-md-9").let { info ->
title = info.select("h1").text()
description = info.select("div.panel-body").text()
}
}
private fun parseStatus(status: String?) = when {
status == null -> SManga.UNKNOWN
status.contains("En emisión") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListRequest(manga: SManga) = GET(
url = "$baseUrl/chapter/getall?mangaIdentification=${manga.url.substringAfterLast("/")}",
headers = headers,
)
override fun chapterListParse(response: Response): List<SChapter> {
// The server returns a JSON with data property that contains a string with the JSON,
// so is necessary to decode twice.
val data = json.decodeFromString<InMangaResultDto>(response.body.string())
if (data.data.isNullOrEmpty()) {
return emptyList()
}
val result = json.decodeFromString<InMangaResultObjectDto<InMangaChapterDto>>(data.data)
if (!result.success) {
return emptyList()
}
return result.result
.map { chap -> chapterFromObject(chap) }
.sortedBy { it.chapter_number.toInt() }.reversed()
}
override fun chapterListSelector() = "not using"
private fun chapterFromObject(chapter: InMangaChapterDto) = SChapter.create().apply {
url = "/chapter/chapterIndexControls?identification=${chapter.identification}"
name = "Chapter ${chapter.friendlyChapterNumber}"
chapter_number = chapter.number!!.toFloat()
date_upload = parseChapterDate(chapter.registrationDate)
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used")
private fun parseChapterDate(string: String): Long {
return DATE_FORMATTER.parse(string)?.time ?: 0L
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
val ch = document.select("[id=\"FriendlyChapterNumberUrl\"]").attr("value")
val title = document.select("[id=\"FriendlyMangaName\"]").attr("value")
document.select("img.ImageContainer").forEachIndexed { i, img ->
add(Page(i, "", "$imageCDN/images/manga/$title/chapter/$ch/page/${i + 1}/${img.attr("id")}"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
override fun getFilterList() = FilterList()
companion object {
val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) }
}
}

View File

@@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.extension.es.inmanga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class InMangaResultDto(
val data: String?,
)
@Serializable
data class InMangaResultObjectDto<T>(
val message: String = "",
val success: Boolean,
val result: List<T>,
)
@Serializable
data class InMangaChapterDto(
@SerialName("PagesCount") val pagesCount: Int = 0,
@SerialName("Watched") val watched: Boolean? = false,
@SerialName("MangaIdentification") val mangaIdentification: String? = "",
@SerialName("MangaName") val mangaName: String? = "",
@SerialName("FriendlyMangaName") val friendlyMangaName: String? = "",
@SerialName("Id") val id: Int? = 0,
@SerialName("MangaId") val mangaId: Int? = 0,
@SerialName("Number") val number: Double? = null,
@SerialName("RegistrationDate") val registrationDate: String = "",
@SerialName("Description") val description: String? = "",
@SerialName("Pages") val pages: List<Int> = emptyList(),
@SerialName("Identification") val identification: String? = "",
@SerialName("FeaturedChapter") val featuredChapter: Boolean = false,
@SerialName("FriendlyChapterNumber") val friendlyChapterNumber: String? = "",
@SerialName("FriendlyChapterNumberUrl") val friendlyChapterNumberUrl: String? = "",
)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Kings Of Darkness'
pkgNameSuffix = 'es.kingsofdarkness'
extClass = '.KingsOfDarkness'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@@ -0,0 +1,99 @@
package eu.kanade.tachiyomi.extension.es.kingsofdarkness
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
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 org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class KingsOfDarkness : ParsedHttpSource() {
override val name = "Kings Of Darkness"
override val baseUrl = "https://kings-of-darkness.wixsite.com/0000"
override val lang = "es"
override val supportsLatest = false
override fun popularMangaSelector() = "#SITE_PAGES div.wixui-image"
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/proyectos", headers)
override fun popularMangaFromElement(element: Element) =
SManga.create().apply {
url = element.child(0).attr("href")
title = element.nextElementSibling()!!.text()
thumbnail_url = element.selectFirst("img")!!.image
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
fetchPopularManga(page).map { mp ->
mp.copy(mp.mangas.filter { it.title.contains(query, true) })
}!!
override fun mangaDetailsRequest(manga: SManga) =
GET(manga.url, headers)
override fun mangaDetailsParse(document: Document) =
SManga.create().apply {
url = document.location()
title = document.selectFirst("#SITE_PAGES h2")!!.text()
thumbnail_url = document.selectFirst("#SITE_PAGES img")!!.image
document.select("#SITE_PAGES p:last-of-type").let { el ->
description = el[0].text().trim()
genre = el[1].select("a").joinToString { it.text() }
}
}
override fun chapterListSelector() = "#SITE_PAGES a[target=_self]"
override fun chapterListRequest(manga: SManga) =
GET(manga.url, headers)
override fun chapterFromElement(element: Element) =
SChapter.create().apply {
url = element.attr("href")
name = element.child(0).text()
chapter_number = name.substring(3).toFloat()
}
override fun pageListRequest(chapter: SChapter) =
GET(chapter.url, headers)
override fun pageListParse(document: Document) =
document.select("#SITE_PAGES img").mapIndexed { idx, el ->
Page(idx, "", el.image)
}
private inline val Element.image: String
get() = attr("src").substringBefore("/v1/fill")
override fun latestUpdatesSelector() = ""
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Not used!")
override fun latestUpdatesFromElement(element: Element) =
throw UnsupportedOperationException("Not used!")
override fun popularMangaNextPageSelector(): String? = null
override fun searchMangaSelector() = ""
override fun searchMangaNextPageSelector(): String? = null
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Not used!")
override fun searchMangaFromElement(element: Element) =
throw UnsupportedOperationException("Not used!")
override fun imageUrlParse(document: Document) =
throw UnsupportedOperationException("Not used!")
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Kumanga'
pkgNameSuffix = 'es.kumanga'
extClass = '.Kumanga'
extVersionCode = 8
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,322 @@
package eu.kanade.tachiyomi.extension.es.kumanga
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.roundToInt
class Kumanga : HttpSource() {
override val name = "Kumanga"
override val baseUrl = "https://www.kumanga.com"
override val lang = "es"
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient
.newBuilder()
.followRedirects(true)
.addInterceptor { chain ->
val originalRequest = chain.request()
if (originalRequest.url.toString().endsWith("token=")) {
getKumangaToken()
val url = originalRequest.url.toString() + kumangaToken
val newRequest = originalRequest.newBuilder().url(url).build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
.build()
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
private var kumangaToken = ""
private fun encodeAndReverse(dtValue: String): String {
return Base64.encodeToString(dtValue.toByteArray(), Base64.DEFAULT).reversed().trim()
}
private fun decodeBase64(encodedString: String): String {
return Base64.decode(encodedString, Base64.DEFAULT).toString(charset("UTF-8"))
}
private fun getKumangaToken(): String {
val body = client.newCall(GET("$baseUrl/mangalist?&page=1", headers)).execute().asJsoup()
val dt = body.select("#searchinput").attr("dt").toString()
val kumangaTokenKey = encodeAndReverse(encodeAndReverse(dt))
.replace("=", "k")
.lowercase(Locale.ROOT)
kumangaToken = body.select("div.input-group [type=hidden]").attr(kumangaTokenKey)
return kumangaToken
}
private fun getMangaCover(mangaId: String) = "$baseUrl/kumathumb.php?src=$mangaId"
private fun getMangaUrl(mangaId: String, mangaSlug: String, page: Int) = "/manga/$mangaId/p/$page/$mangaSlug#cl"
private fun parseMangaFromJson(jsonObj: JsonObject) = SManga.create().apply {
title = jsonObj["name"]!!.jsonPrimitive.content
description = jsonObj["description"]!!.jsonPrimitive.content.replace("\\", "")
url = getMangaUrl(jsonObj["id"]!!.jsonPrimitive.content, jsonObj["slug"]!!.jsonPrimitive.content, 1)
thumbnail_url = getMangaCover(jsonObj["id"]!!.jsonPrimitive.content)
genre = jsonObj["categories"]!!.jsonArray
.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content }
}
override fun popularMangaRequest(page: Int): Request {
getKumangaToken() // Get new token every request (prevent http 400)
return POST("$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=&retrieveCategories=true&retrieveAuthors=false&contentType=manga&token=$kumangaToken", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val jsonResult = json.parseToJsonElement(response.body.string()).jsonObject
val mangaList = jsonResult["contents"]!!.jsonArray
.map { jsonEl -> parseMangaFromJson(jsonEl.jsonObject) }
val hasNextPage = jsonResult["retrievedCount"]!!.jsonPrimitive.int == 10
return MangasPage(mangaList, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = throw Exception("Not Used")
override fun latestUpdatesParse(response: Response) = throw Exception("Not Used")
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = response.asJsoup()
thumbnail_url = body.selectFirst("div.km-img-gral-2 img")?.attr("abs:src")
body.select("div#tab2").let {
status = parseStatus(it.select("span").text().orEmpty())
author = it.select("p:nth-child(3) > a").text()
artist = it.select("p:nth-child(4) > a").text()
}
}
private fun parseStatus(status: String) = when {
status.contains("Activo") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
private fun parseChapterDate(date: String): Long = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault())
.parse(date)?.time ?: 0L
private fun chapterSelector() = "div#accordion .title"
private fun chapterFromElement(element: Element) = SChapter.create().apply {
element.select("a:has(i)").let {
setUrlWithoutDomain(it.attr("abs:href").replace("/c/", "/leer/"))
name = it.text()
date_upload = parseChapterDate(it.attr("title"))
}
scanlator = element.select("span.pull-right.greenSpan").text()
}
override fun chapterListParse(response: Response): List<SChapter> = mutableListOf<SChapter>().apply {
var document = response.asJsoup()
val params = document.select("script:containsData(totCntnts)").toString()
val numberChapters = params.substringAfter("totCntnts=").substringBefore(";").toIntOrNull()
val mangaId = params.substringAfter("mid=").substringBefore(";")
val mangaSlug = params.substringAfter("slg='").substringBefore("';")
if (numberChapters != null) {
// Calculating total of pages, Kumanga shows 10 chapters per page, total_pages = #chapters / 10
val numberOfPages = (numberChapters / 10.toDouble() + 0.4).roundToInt()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
var page = 2
while (page <= numberOfPages) {
document = client.newCall(GET(baseUrl + getMangaUrl(mangaId, mangaSlug, page))).execute().asJsoup()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
page++
}
} else {
throw Exception("No fue posible obtener los capítulos")
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val form = document.selectFirst("form#myForm[action]")
if (form != null) {
val url = form.attr("action")
val bodyBuilder = FormBody.Builder()
val inputs = form.select("input")
inputs.map { input ->
bodyBuilder.add(input.attr("name"), input.attr("value"))
}
return pageListParse(client.newCall(POST(url, headers, bodyBuilder.build())).execute())
} else {
val imagesJsonRaw = document.select("script:containsData(var pUrl=)").firstOrNull()
?.data()
?.substringAfter("var pUrl=")
?.substringBefore(";")
?.let { decodeBase64(decodeBase64(it).reversed().dropLast(10).drop(10)) }
?: throw Exception("imagesJsonListStr null")
val jsonResult = json.parseToJsonElement(imagesJsonRaw).jsonArray
return jsonResult.mapIndexed { i, jsonEl ->
val jsonObj = jsonEl.jsonObject
val imagePath = jsonObj["imgURL"]!!.jsonPrimitive.content.replace("\\", "")
val docUrl = document.location()
val baseUrl = URL(docUrl).protocol + "://" + URL(docUrl).host // For some reason baseUri returns the full url
Page(i, baseUrl, "$baseUrl/$imagePath")
}
}
}
override fun imageRequest(page: Page): Request {
val imageHeaders = Headers.Builder()
.add("Referer", page.url)
.build()
return GET(page.imageUrl!!, imageHeaders)
}
override fun imageUrlParse(response: Response) = throw Exception("Not Used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
getKumangaToken()
val url = "$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=$query&retrieveCategories=true&retrieveAuthors=false&contentType=manga&token=$kumangaToken".toHttpUrlOrNull()!!.newBuilder()
filters.forEach { filter ->
when (filter) {
is TypeList -> {
filter.state
.filter { type -> type.state }
.forEach { type -> url.addQueryParameter("type_filter[]", type.id) }
}
is StatusList -> {
filter.state
.filter { status -> status.state }
.forEach { status -> url.addQueryParameter("status_filter[]", status.id) }
}
is GenreList -> {
filter.state
.filter { genre -> genre.state }
.forEach { genre -> url.addQueryParameter("category_filter[]", genre.id) }
}
else -> {}
}
}
return POST(url.build().toString(), headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun getFilterList() = FilterList(
TypeList(getTypeList()),
Filter.Separator(),
StatusList(getStatusList()),
Filter.Separator(),
GenreList(getGenreList()),
)
private class Type(name: String, val id: String) : Filter.CheckBox(name)
private class TypeList(types: List<Type>) : Filter.Group<Type>("Filtrar por tipos", types)
private class Status(name: String, val id: String) : Filter.CheckBox(name)
private class StatusList(status: List<Status>) : Filter.Group<Status>("Filtrar por estado", status)
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)
private fun getTypeList() = listOf(
Type("Manga", "1"),
Type("Manhwa", "2"),
Type("Manhua", "3"),
Type("One shot", "4"),
Type("Doujinshi", "5"),
)
private fun getStatusList() = listOf(
Status("Activo", "1"),
Status("Finalizado", "2"),
Status("Inconcluso", "3"),
)
private fun getGenreList() = listOf(
Genre("Acción", "1"),
Genre("Artes marciales", "2"),
Genre("Automóviles", "3"),
Genre("Aventura", "4"),
Genre("Ciencia Ficción", "5"),
Genre("Comedia", "6"),
Genre("Demonios", "7"),
Genre("Deportes", "8"),
Genre("Doujinshi", "9"),
Genre("Drama", "10"),
Genre("Ecchi", "11"),
Genre("Espacio exterior", "12"),
Genre("Fantasía", "13"),
Genre("Gender bender", "14"),
Genre("Gore", "46"),
Genre("Harem", "15"),
Genre("Hentai", "16"),
Genre("Histórico", "17"),
Genre("Horror", "18"),
Genre("Josei", "19"),
Genre("Juegos", "20"),
Genre("Locura", "21"),
Genre("Magia", "22"),
Genre("Mecha", "23"),
Genre("Militar", "24"),
Genre("Misterio", "25"),
Genre("Música", "26"),
Genre("Niños", "27"),
Genre("Parodia", "28"),
Genre("Policía", "29"),
Genre("Psicológico", "30"),
Genre("Recuentos de la vida", "31"),
Genre("Romance", "32"),
Genre("Samurai", "33"),
Genre("Seinen", "34"),
Genre("Shoujo", "35"),
Genre("Shoujo Ai", "36"),
Genre("Shounen", "37"),
Genre("Shounen Ai", "38"),
Genre("Sobrenatural", "39"),
Genre("Súperpoderes", "41"),
Genre("Suspenso", "40"),
Genre("Terror", "47"),
Genre("Tragedia", "48"),
Genre("Vampiros", "42"),
Genre("Vida escolar", "43"),
Genre("Yaoi", "44"),
Genre("Yuri", "45"),
)
}

View File

@@ -0,0 +1,23 @@
<?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

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'LectorManga'
pkgNameSuffix = 'es.lectormanga'
extClass = '.LectorManga'
extVersionCode = 31
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,616 @@
package eu.kanade.tachiyomi.extension.es.lectormanga
import android.app.Application
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
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.toHttpUrlOrNull
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.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.CountDownLatch
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.toHttpUrlOrNull()!!, permits, period)
}
return this
}
private var loadWebView = true
override val client: OkHttpClient = network.client.newBuilder()
.rateLimitHost(
baseUrl.toHttpUrlOrNull()!!,
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,
)
.addInterceptor { chain ->
val request = chain.request()
val url = request.url
if (url.host.contains("japanreader.com") && loadWebView) {
val handler = Handler(Looper.getMainLooper())
val latch = CountDownLatch(1)
var webView: WebView? = null
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
webview.settings.domStorageEnabled = true
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
webview.settings.useWideViewPort = false
webview.settings.loadWithOverviewMode = false
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
latch.countDown()
}
}
val headers = mutableMapOf<String, String>()
headers["Referer"] = baseUrl
webview.loadUrl(url.toString(), headers)
}
latch.await()
loadWebView = false
handler.post { webView?.destroy() }
}
chain.proceed(request)
}
.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".toHttpUrlOrNull()!!.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().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("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().orEmpty())
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("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"
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) {
doc = client.newCall(GET(newUrl, headers)).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")
}
},
),
)
}
}
// Some chapters uses JavaScript to redirect to read page
private fun redirectToReadPage(document: Document): Document {
val script1 = document.selectFirst("script:containsData(uniqid)")
val script2 = document.selectFirst("script:containsData(window.location.replace)")
val redirectHeaders = Headers.Builder()
.add("Referer", document.baseUri())
.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[1]
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[1]
return redirectToReadPage(client.newCall(GET(url, redirectHeaders)).execute().asJsoup())
}
return document
}
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 Exception("Not Used")
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. 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 = "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. 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 = "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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'LeerMangasXYZ'
pkgNameSuffix = 'es.leermangasxyz'
extClass = '.LeerMangasXYZ'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.extension.es.leermangasxyz
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
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 okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.net.URLEncoder
open class LeerMangasXYZ : ParsedHttpSource() {
override val baseUrl: String = "https://r1.leermanga.xyz"
override val lang: String = "es"
override val name: String = "LeerManga.xyz"
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used")
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
override val supportsLatest: Boolean = false
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
val row = element.select("td")
with(row[0]) {
chapter_number = text().toFloat()
date_upload = 0
}
with(row[1]) {
name = text()
url = selectFirst("a")!!.attr("href")
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = super.fetchChapterList(manga).map {
it.reversed()
}
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
setUrlWithoutDomain(document.baseUri())
val rawStatus = document.selectFirst("td:contains(Status)")!!.text()
status = getStatus(rawStatus.substringAfter("Status: "))
author = document.select("li[itemprop=author]").joinToString(separator = ", ") { it.text() }
thumbnail_url = document.selectFirst("img.img-thumbnail")!!.attr("abs:src")
description = document.selectFirst("p[itemprop=description]")!!.text()
genre = document.select("span[itemprop=genre]").joinToString(", ") { it.text() }
}
override fun pageListParse(document: Document): List<Page> {
val pages = document.select(pageListSelector()).map {
Page(
imageUrl = it.attr("href"),
index = it.attr("data-ngdesc").substringAfter("Page ").toInt(),
)
}
if (pages.isEmpty()) {
throw RuntimeException("Cannot fetch images from source")
}
return pages
}
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst("img.card-img-top")!!.attr("abs:src")
element.selectFirst("div.card-body")!!.let {
val dc = it.selectFirst("h5.card-title a")!!
url = dc.attr("href")
title = dc.text()
}
}
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
with(element) {
thumbnail_url = selectFirst("img")!!.attr("abs:src")
title = selectFirst("span[itemprop=name]")!!.text()
url = selectFirst("div.col-4 a")!!.attr("href")
}
}
private fun encodeString(str: String): String = URLEncoder.encode(str, "utf-8")
private fun getStatus(str: String): Int = when (str) {
"Emitiéndose", "Ongoing", "En emisión" -> SManga.ONGOING
"Finalizado" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// ========------- [[< Request >]]] =========--------
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/search?query=${encodeString(query)}&page=$page")
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// ------ ======== [[[ SELECTORS ]]] ======== -------
private fun pageListSelector() = "div[data-nanogallery2] a"
override fun searchMangaSelector(): String = "div[itemtype*=ComicSeries]"
override fun searchMangaNextPageSelector(): String = "CHANGE THIS"
override fun popularMangaSelector(): String = "div.card-group div.card"
override fun popularMangaNextPageSelector(): String = "CHANGE THIS"
override fun chapterListSelector(): String = "table#chaptersTable tbody tr"
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Manga Latino'
pkgNameSuffix = 'es.mangalatino'
extClass = '.MangaLatino'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Some files were not shown because too many files have changed in this diff Show More