Remove Kumanga (#1857)

* Remove Kumanga

* Add rule to issue_moderator

* Rebuild

* Update regex

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
bapeey 2024-03-14 12:48:25 -05:00 committed by GitHub
parent 3d217e7931
commit 562fcabfe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 7 additions and 415 deletions

View File

@ -58,6 +58,13 @@ jobs:
"ignoreCase": true,
"labels": ["invalid"],
"message": "ReManga (Russian) will not be added back as it has been removed [due to legal reasons](https://github.com/github/dmca)."
},
{
"type": "both",
"regex": "(kumanga\\.com)",
"ignoreCase": true,
"labels": ["invalid"],
"message": "{match} will not be added back as it is too difficult to maintain."
}
]
auto-close-ignore-label: do-not-autoclose

View File

@ -1,11 +0,0 @@
ext {
extName = 'Kumanga'
extClass = '.Kumanga'
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:randomua'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,373 +0,0 @@
package eu.kanade.tachiyomi.extension.es.kumanga
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.roundToInt
class Kumanga : HttpSource(), ConfigurableSource {
override val name = "Kumanga"
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 supportsLatest = false
private var kumangaToken = ""
private val json: Json by injectLazy()
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
.add("Accept-language", "es-419,es;q=0.6")
.add("Cache-Control", "max-age=0")
.add("Sec-Fetch-Dest", "document")
.add("Sec-Fetch-Mode", "navigate")
.add("Sec-Fetch-Site", "none")
.add("Sec-Fetch-User", "?1")
.add("Sec-GPC", "1")
.add("Upgrade-Insecure-Requests", "1")
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
)
.rateLimit(1)
.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 addTokenToRequest(request: Request): Request {
return request.newBuilder()
.url(request.url.newBuilder().removeAllQueryParameters("token").addQueryParameter("token", kumangaToken).build())
.build()
}
private fun getKumangaToken() {
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)
}
override fun popularMangaRequest(page: Int): Request {
val url = apiUrl.toHttpUrl().newBuilder()
.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 {
val jsonResult = json.decodeFromString<ComicsPayloadDto>(response.body.string())
val mangas = jsonResult.contents.map { it.toSManga(baseUrl) }
val hasNextPage = jsonResult.retrievedCount == CONTENT_PER_PAGE
return MangasPage(mangas, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
thumbnail_url = document.selectFirst("div.km-img-gral-2 img")?.attr("abs:src")
document.select("div#tab1").let {
description = it.select("p").text()
}
document.select("div#tab2").let {
status = parseStatus(it.select("span").text().orEmpty())
author = it.select("p:contains(Autor) > a").text()
artist = it.select("p:contains(Artista) > a").text()
}
}
private fun chapterSelector() = "div[id^=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 = parseDate(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()
var location = document.location()
val params = document.select("script:containsData(totCntnts)").toString()
val pagesVar = params.substringAfter("totCntnts").substringAfter("=").substringBefore(";").trim()
val chaptersNumber = params.substringAfter(pagesVar).substringAfter("=").substringBefore(";").toIntOrNull()
val mangaId = params.substringAfter("mid").substringAfter("=").substringBefore(";").trim()
val mangaSlug = params.substringAfter("slg").substringAfter("=").substringBefore(";").trim().removeSurrounding("'")
if (chaptersNumber != null) {
val numberOfPages = ((chaptersNumber - 10) / 10.toDouble() + 0.4).roundToInt()
document.select(chapterSelector()).map { add(chapterFromElement(it)) }
var page = 2
while (page <= numberOfPages) {
val pageHeaders = headersBuilder().set("Referer", location).build()
document = client.newCall(
GET(
baseUrl + getMangaUrl(mangaId, mangaSlug, page),
pageHeaders,
),
).execute().asJsoup()
location = document.location()
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("No se pudo obtener la lista de imágenes")
val jsonResult = json.decodeFromString<List<ImageDto>>(imagesJsonRaw)
return jsonResult.mapIndexed { i, item ->
val imagePath = item.imgURL.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 UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = apiUrl.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 ->
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()),
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
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"),
)
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

@ -1,31 +0,0 @@
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/$mangaSlug"
}
@Serializable
class ImageDto(
val imgURL: String,
)