mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 10:22:47 +01:00
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:
parent
3d217e7931
commit
562fcabfe2
7
.github/workflows/issue_moderator.yml
vendored
7
.github/workflows/issue_moderator.yml
vendored
@ -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
|
||||
|
@ -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 |
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
Loading…
Reference in New Issue
Block a user