diff --git a/src/es/kumanga/build.gradle b/src/es/kumanga/build.gradle deleted file mode 100644 index 8310a459d..000000000 --- a/src/es/kumanga/build.gradle +++ /dev/null @@ -1,11 +0,0 @@ -ext { - extName = 'Kumanga' - extClass = '.Kumanga' - extVersionCode = 11 -} - -apply from: "$rootDir/common.gradle" - -dependencies { - implementation(project(':lib:randomua')) -} diff --git a/src/es/kumanga/res/mipmap-hdpi/ic_launcher.png b/src/es/kumanga/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index c8a55e202..000000000 Binary files a/src/es/kumanga/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/es/kumanga/res/mipmap-mdpi/ic_launcher.png b/src/es/kumanga/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index e731fc2a1..000000000 Binary files a/src/es/kumanga/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/es/kumanga/res/mipmap-xhdpi/ic_launcher.png b/src/es/kumanga/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 597f7a5a3..000000000 Binary files a/src/es/kumanga/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/es/kumanga/res/mipmap-xxhdpi/ic_launcher.png b/src/es/kumanga/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 5987b29bf..000000000 Binary files a/src/es/kumanga/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/es/kumanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/kumanga/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index c4da69246..000000000 Binary files a/src/es/kumanga/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/Kumanga.kt b/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/Kumanga.kt deleted file mode 100644 index e9a1ac677..000000000 --- a/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/Kumanga.kt +++ /dev/null @@ -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().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(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 = mutableListOf().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 { - 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>(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) : Filter.Group("Filtrar por tipos", types) - - private class Status(name: String, val id: String) : Filter.CheckBox(name) - private class StatusList(status: List) : Filter.Group("Filtrar por estado", status) - - private class Genre(name: String, val id: String) : Filter.CheckBox(name) - private class GenreList(genres: List) : Filter.Group("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 - } -} diff --git a/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/KumangaDto.kt b/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/KumangaDto.kt deleted file mode 100644 index 07daf5ad2..000000000 --- a/src/es/kumanga/src/eu/kanade/tachiyomi/extension/es/kumanga/KumangaDto.kt +++ /dev/null @@ -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, - 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, -)