Initial commit
2
src/es/brakeout/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
13
src/es/brakeout/build.gradle
Normal 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"
|
||||
BIN
src/es/brakeout/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/es/brakeout/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/es/brakeout/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
src/es/brakeout/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/es/brakeout/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/es/brakeout/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
@@ -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()
|
||||
}
|
||||
}
|
||||
2
src/es/cerberusseries/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/cerberusseries/build.gradle
Normal 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"
|
||||
BIN
src/es/cerberusseries/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/es/cerberusseries/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/es/cerberusseries/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/es/cerberusseries/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/es/cerberusseries/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/es/cerberusseries/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
@@ -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) }
|
||||
}
|
||||
}
|
||||
2
src/es/heavenmanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/heavenmanga/build.gradle
Normal 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"
|
||||
BIN
src/es/heavenmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/es/heavenmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/es/heavenmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/es/heavenmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/es/heavenmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/es/heavenmanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
2
src/es/ikigaimangas/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
14
src/es/ikigaimangas/build.gradle
Normal 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"
|
||||
BIN
src/es/ikigaimangas/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/es/ikigaimangas/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/es/ikigaimangas/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/es/ikigaimangas/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src/es/ikigaimangas/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/es/ikigaimangas/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
2
src/es/ikuhentai/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/ikuhentai/build.gradle
Normal 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"
|
||||
BIN
src/es/ikuhentai/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/es/ikuhentai/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/es/ikuhentai/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/es/ikuhentai/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/es/ikuhentai/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/es/ikuhentai/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
2
src/es/inmanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/inmanga/build.gradle
Normal 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"
|
||||
BIN
src/es/inmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/es/inmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/es/inmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/es/inmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/es/inmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/es/inmanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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? = "",
|
||||
)
|
||||
2
src/es/kingsofdarkness/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/es/kingsofdarkness/build.gradle
Normal 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"
|
||||
BIN
src/es/kingsofdarkness/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/es/kingsofdarkness/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/es/kingsofdarkness/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src/es/kingsofdarkness/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/es/kingsofdarkness/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/es/kingsofdarkness/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
@@ -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!")
|
||||
}
|
||||
2
src/es/kumanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/kumanga/build.gradle
Normal 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"
|
||||
BIN
src/es/kumanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/es/kumanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/es/kumanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/es/kumanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src/es/kumanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/es/kumanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
@@ -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"),
|
||||
)
|
||||
}
|
||||
23
src/es/lectormanga/AndroidManifest.xml
Normal 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>
|
||||
12
src/es/lectormanga/build.gradle
Normal 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"
|
||||
BIN
src/es/lectormanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/es/lectormanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/es/lectormanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src/es/lectormanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/es/lectormanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/es/lectormanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
@@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
2
src/es/leermangasxyz/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/es/leermangasxyz/build.gradle
Normal 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"
|
||||
BIN
src/es/leermangasxyz/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/es/leermangasxyz/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/es/leermangasxyz/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/es/leermangasxyz/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src/es/leermangasxyz/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -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"
|
||||
}
|
||||
2
src/es/mangalatino/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/es/mangalatino/build.gradle
Normal 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"
|
||||
BIN
src/es/mangalatino/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/es/mangalatino/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/es/mangalatino/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/es/mangalatino/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/es/mangalatino/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |