Kumanga: Update apiUrl and minor changes (#1586)

Fix Kumanga
This commit is contained in:
bapeey 2024-02-28 10:33:58 -05:00 committed by Draff
parent 37c80ab2f6
commit 5a643095ad
3 changed files with 125 additions and 78 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Kumanga' extName = 'Kumanga'
extClass = '.Kumanga' extClass = '.Kumanga'
extVersionCode = 8 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.es.kumanga
import android.util.Base64 import android.util.Base64
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -11,12 +12,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json 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.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -36,78 +33,71 @@ class Kumanga : HttpSource() {
override val baseUrl = "https://www.kumanga.com" override val baseUrl = "https://www.kumanga.com"
private val apiUrl = "https://www.kumanga.com/backend/ajax/searchengine_master.php"
override val lang = "es" override val lang = "es"
override val supportsLatest = false override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient private var kumangaToken = ""
.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() private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl) .add("Referer", "$baseUrl/")
private var kumangaToken = "" override val client: OkHttpClient = network.cloudflareClient
.newBuilder()
.rateLimit(2)
.addInterceptor { chain ->
val request = chain.request()
if (!request.url.toString().startsWith(apiUrl)) return@addInterceptor chain.proceed(request)
if (kumangaToken.isBlank()) getKumangaToken()
var newRequest = addTokenToRequest(request)
val response = chain.proceed(newRequest)
if (response.code == 400) {
response.close()
getKumangaToken()
newRequest = addTokenToRequest(request)
chain.proceed(newRequest)
} else {
response
}
}
.build()
private fun encodeAndReverse(dtValue: String): String { private fun addTokenToRequest(request: Request): Request {
return Base64.encodeToString(dtValue.toByteArray(), Base64.DEFAULT).reversed().trim() return request.newBuilder()
.url(request.url.newBuilder().removeAllQueryParameters("token").addQueryParameter("token", kumangaToken).build())
.build()
} }
private fun decodeBase64(encodedString: String): String { private fun getKumangaToken() {
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 body = client.newCall(GET("$baseUrl/mangalist?&page=1", headers)).execute().asJsoup()
val dt = body.select("#searchinput").attr("dt").toString() val dt = body.select("#searchinput").attr("dt").toString()
val kumangaTokenKey = encodeAndReverse(encodeAndReverse(dt)) val kumangaTokenKey = encodeAndReverse(encodeAndReverse(dt))
.replace("=", "k") .replace("=", "k")
.lowercase(Locale.ROOT) .lowercase(Locale.ROOT)
kumangaToken = body.select("div.input-group [type=hidden]").attr(kumangaTokenKey) 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 { override fun popularMangaRequest(page: Int): Request {
getKumangaToken() // Get new token every request (prevent http 400) val url = apiUrl.toHttpUrl().newBuilder()
return POST("$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=&retrieveCategories=true&retrieveAuthors=false&contentType=manga&token=$kumangaToken", headers) .addQueryParameter("page", page.toString())
.addQueryParameter("perPage", CONTENT_PER_PAGE.toString())
.addQueryParameter("retrieveCategories", "true")
.addQueryParameter("retrieveAuthors", "true")
.addQueryParameter("contentType", "manga")
.build()
return POST(url.toString(), headers)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val jsonResult = json.parseToJsonElement(response.body.string()).jsonObject val jsonResult = json.decodeFromString<ComicsPayloadDto>(response.body.string())
val mangas = jsonResult.contents.map { it.toSManga(baseUrl) }
val mangaList = jsonResult["contents"]!!.jsonArray val hasNextPage = jsonResult.retrievedCount == CONTENT_PER_PAGE
.map { jsonEl -> parseMangaFromJson(jsonEl.jsonObject) } return MangasPage(mangas, hasNextPage)
val hasNextPage = jsonResult["retrievedCount"]!!.jsonPrimitive.int == 10
return MangasPage(mangaList, hasNextPage)
} }
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
@ -115,31 +105,25 @@ class Kumanga : HttpSource() {
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response) = SManga.create().apply { override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = response.asJsoup() val document = response.asJsoup()
thumbnail_url = body.selectFirst("div.km-img-gral-2 img")?.attr("abs:src") thumbnail_url = document.selectFirst("div.km-img-gral-2 img")?.attr("abs:src")
body.select("div#tab2").let { document.select("div#tab1").let {
description = it.select("p").text()
}
document.select("div#tab2").let {
status = parseStatus(it.select("span").text().orEmpty()) status = parseStatus(it.select("span").text().orEmpty())
author = it.select("p:nth-child(3) > a").text() author = it.select("p:contains(Autor) > a").text()
artist = it.select("p:nth-child(4) > a").text() artist = it.select("p:contains(Artista) > a").text()
} }
} }
private fun parseStatus(status: String) = when { private fun chapterSelector() = "div[id^=accordion] .title"
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 { private fun chapterFromElement(element: Element) = SChapter.create().apply {
element.select("a:has(i)").let { element.select("a:has(i)").let {
setUrlWithoutDomain(it.attr("abs:href").replace("/c/", "/leer/")) setUrlWithoutDomain(it.attr("abs:href").replace("/c/", "/leer/"))
name = it.text() name = it.text()
date_upload = parseChapterDate(it.attr("title")) date_upload = parseDate(it.attr("title"))
} }
scanlator = element.select("span.pull-right.greenSpan").text() scanlator = element.select("span.pull-right.greenSpan").text()
} }
@ -157,7 +141,6 @@ class Kumanga : HttpSource() {
val numberOfPages = (numberChapters / 10.toDouble() + 0.4).roundToInt() val numberOfPages = (numberChapters / 10.toDouble() + 0.4).roundToInt()
document.select(chapterSelector()).map { add(chapterFromElement(it)) } document.select(chapterSelector()).map { add(chapterFromElement(it)) }
var page = 2 var page = 2
while (page <= numberOfPages) { while (page <= numberOfPages) {
document = client.newCall(GET(baseUrl + getMangaUrl(mangaId, mangaSlug, page))).execute().asJsoup() document = client.newCall(GET(baseUrl + getMangaUrl(mangaId, mangaSlug, page))).execute().asJsoup()
document.select(chapterSelector()).map { add(chapterFromElement(it)) } document.select(chapterSelector()).map { add(chapterFromElement(it)) }
@ -185,13 +168,12 @@ class Kumanga : HttpSource() {
?.substringAfter("var pUrl=") ?.substringAfter("var pUrl=")
?.substringBefore(";") ?.substringBefore(";")
?.let { decodeBase64(decodeBase64(it).reversed().dropLast(10).drop(10)) } ?.let { decodeBase64(decodeBase64(it).reversed().dropLast(10).drop(10)) }
?: throw Exception("imagesJsonListStr null") ?: throw Exception("No se pudo obtener la lista de imágenes")
val jsonResult = json.parseToJsonElement(imagesJsonRaw).jsonArray val jsonResult = json.decodeFromString<List<ImageDto>>(imagesJsonRaw)
return jsonResult.mapIndexed { i, jsonEl -> return jsonResult.mapIndexed { i, item ->
val jsonObj = jsonEl.jsonObject val imagePath = item.imgURL.replace("\\", "")
val imagePath = jsonObj["imgURL"]!!.jsonPrimitive.content.replace("\\", "")
val docUrl = document.location() val docUrl = document.location()
val baseUrl = URL(docUrl).protocol + "://" + URL(docUrl).host // For some reason baseUri returns the full url val baseUrl = URL(docUrl).protocol + "://" + URL(docUrl).host // For some reason baseUri returns the full url
Page(i, baseUrl, "$baseUrl/$imagePath") Page(i, baseUrl, "$baseUrl/$imagePath")
@ -209,8 +191,13 @@ class Kumanga : HttpSource() {
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
getKumangaToken() val url = apiUrl.toHttpUrl().newBuilder()
val url = "$baseUrl/backend/ajax/searchengine.php?page=$page&perPage=10&keywords=$query&retrieveCategories=true&retrieveAuthors=false&contentType=manga&token=$kumangaToken".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString())
.addQueryParameter("perPage", CONTENT_PER_PAGE.toString())
.addQueryParameter("retrieveCategories", "true")
.addQueryParameter("retrieveAuthors", "true")
.addQueryParameter("contentType", "manga")
.addQueryParameter("keywords", query)
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
@ -319,4 +306,33 @@ class Kumanga : HttpSource() {
Genre("Yaoi", "44"), Genre("Yaoi", "44"),
Genre("Yuri", "45"), Genre("Yuri", "45"),
) )
private fun parseStatus(status: String) = when {
status.contains("Activo") -> SManga.ONGOING
status.contains("Finalizado") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
private fun parseDate(date: String): Long {
return try {
DATE_FORMAT.parse(date)?.time ?: 0
} catch (_: Exception) {
0
}
}
private fun getMangaUrl(mangaId: String, mangaSlug: String, page: Int) = "/manga/$mangaId/p/$page/$mangaSlug"
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"))
}
companion object {
private val DATE_FORMAT = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.ROOT)
private const val CONTENT_PER_PAGE = 24
}
} }

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.extension.es.kumanga
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
@Serializable
class ComicsPayloadDto(
val contents: List<ComicDto>,
val retrievedCount: Int,
)
@Serializable
class ComicDto(
private val id: Int,
private val name: String,
private val slug: String,
) {
fun toSManga(baseUrl: String) = SManga.create().apply {
title = name
url = createMangaUrl(id.toString(), slug)
thumbnail_url = guessMangaCover(id.toString(), baseUrl)
}
private fun guessMangaCover(mangaId: String, baseUrl: String) = "$baseUrl/kumathumb.php?src=$mangaId"
private fun createMangaUrl(mangaId: String, mangaSlug: String) = "/manga/$mangaId/p/1/$mangaSlug"
}
@Serializable
class ImageDto(
val imgURL: String,
)