Add Kumanga inorichi/tachiyomi-extensions#921 (#3793)
* Add Kumanga inorichi/tachiyomi-extensions#921 * Fix pagination bug, added chapter date and correct name. * Prevent unnecessary pagination request * Fix chapter list loop, fix empty chapter
This commit is contained in:
parent
a84848d790
commit
b93641e90e
|
@ -0,0 +1,17 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Kumanga'
|
||||||
|
pkgNameSuffix = 'es.kumanga'
|
||||||
|
extClass = '.Kumanga'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '1.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'com.google.code.gson:gson:2.8.5'
|
||||||
|
compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,273 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.es.kumanga
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
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 java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class Kumanga : HttpSource() {
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
.newBuilder()
|
||||||
|
.followRedirects(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val name = "Kumanga"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.kumanga.com"
|
||||||
|
|
||||||
|
override val lang = "es"
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
private val chapterImagesHeaders = Headers.Builder()
|
||||||
|
.add("Referer", baseUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun getMangaCover(mangaId: String) = "https://static.kumanga.com/manga_covers/$mangaId.jpg?w=201"
|
||||||
|
|
||||||
|
private fun getMangaUrl(mangaId: String, mangaSlug: String, page: Int) = "/manga/$mangaId/p/$page/$mangaSlug#cl"
|
||||||
|
|
||||||
|
private fun parseMangaFromJson(json: JsonElement) = SManga.create().apply {
|
||||||
|
title = json["name"].string
|
||||||
|
description = json["description"].string.replace("\\", "")
|
||||||
|
url = getMangaUrl(json["id"].string, json["slug"].string, 1)
|
||||||
|
thumbnail_url = getMangaCover(json["id"].string)
|
||||||
|
|
||||||
|
val genresArray = json["categories"].array
|
||||||
|
genre = genresArray.joinToString { jsonObject ->
|
||||||
|
parseGenresFromJson(jsonObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseJson(json: String): JsonElement {
|
||||||
|
return JsonParser().parse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseGenresFromJson(json: JsonElement) = json["name"].string
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return POST("$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=&retrieveCategories=true&retrieveAuthors=false&contentType=manga", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val res = response.body()!!.string()
|
||||||
|
val json = parseJson(res)
|
||||||
|
val data = json["contents"].array
|
||||||
|
val retrievedCount = json["retrievedCount"].int
|
||||||
|
val hasNextPage = retrievedCount == 10
|
||||||
|
|
||||||
|
val mangas = data.map { jsonObject ->
|
||||||
|
parseMangaFromJson(jsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, 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()
|
||||||
|
|
||||||
|
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 > div.panel.panel-default.c_panel:has(table)"
|
||||||
|
|
||||||
|
private fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
element.select("table:first-child td h4").let { it ->
|
||||||
|
it.select("a:has(i)").let {
|
||||||
|
url = '/' + it.attr("href").replace("/c/", "/leer/")
|
||||||
|
name = it.text()
|
||||||
|
date_upload = parseChapterDate(it.attr("title"))
|
||||||
|
}
|
||||||
|
scanlator = it.select("span.pull-right.greenSpan")?.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> = mutableListOf<SChapter>().apply {
|
||||||
|
var document = response.asJsoup()
|
||||||
|
val params = document.select("body").toString().substringAfter("php_pagination(").substringBefore(")")
|
||||||
|
val numberChapters = params.split(",")[4].toIntOrNull()
|
||||||
|
val mangaId = params.split(",")[0]
|
||||||
|
val mangaSlug = params.split(",")[1].replace("'", "")
|
||||||
|
|
||||||
|
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> = mutableListOf<Page>().apply {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val imagesJsonListStr = document.select("head").toString()
|
||||||
|
.substringAfter("var pUrl=")
|
||||||
|
.substringBefore(";")
|
||||||
|
val imagesJsonList = parseJson(imagesJsonListStr).array
|
||||||
|
|
||||||
|
imagesJsonList.forEach {
|
||||||
|
val fakeImageUrl = it["imgURL"].string.replace("\\", "")
|
||||||
|
val imageUrl = baseUrl + fakeImageUrl
|
||||||
|
|
||||||
|
add(Page(size, "", imageUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page) = GET(page.imageUrl!!, chapterImagesHeaders)
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw Exception("Not Used")
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=$query&retrieveCategories=true&retrieveAuthors=false&contentType=manga")!!.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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue