From c5c6d77479d473b7acd5251d603d224de1d31132 Mon Sep 17 00:00:00 2001 From: bapeey <90949336+bapeey@users.noreply.github.com> Date: Mon, 12 Feb 2024 07:51:31 -0500 Subject: [PATCH] HeavenManga: Fix chapter list and add latest tab (#1206) Fixes --- src/es/heavenmanga/build.gradle | 2 +- .../extension/es/heavenmanga/HeavenManga.kt | 250 +++++++++++------- .../es/heavenmanga/HeavenMangaDto.kt | 21 ++ 3 files changed, 174 insertions(+), 99 deletions(-) create mode 100644 src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenMangaDto.kt diff --git a/src/es/heavenmanga/build.gradle b/src/es/heavenmanga/build.gradle index 3f39c160a..988c42109 100644 --- a/src/es/heavenmanga/build.gradle +++ b/src/es/heavenmanga/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'HeavenManga' extClass = '.HeavenManga' - extVersionCode = 6 + extVersionCode = 7 isNsfw = true } diff --git a/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenManga.kt b/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenManga.kt index 2d19aa1ed..a1c5793a6 100644 --- a/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenManga.kt +++ b/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenManga.kt @@ -9,12 +9,17 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.Headers +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale class HeavenManga : ParsedHttpSource() { @@ -24,81 +29,20 @@ class HeavenManga : ParsedHttpSource() { override val lang = "es" - // latest is broken on the site, it's the same as popular so turning it off - override val supportsLatest = false + override val supportsLatest = true + + private val json: Json by injectLazy() override val client: OkHttpClient = network.cloudflareClient - override fun headersBuilder(): Headers.Builder { - return Headers.Builder() - .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) Gecko/20100101 Firefox/75") - } - - override fun popularMangaSelector() = "div.page-item-detail" - - override fun latestUpdatesSelector() = "#container .ultimos_epis .not" - - override fun searchMangaSelector() = "div.c-tabs-item__content, ${popularMangaSelector()}" - - override fun chapterListSelector() = "div.listing-chapters_wrap tr" - - override fun popularMangaNextPageSelector() = "ul.pagination a[rel=next]" - - override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() - - override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") override fun popularMangaRequest(page: Int) = GET("$baseUrl/top?orderby=views&page=$page", headers) - override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/", headers) + override fun popularMangaSelector() = "div.page-item-detail" - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val searchUrl = "$baseUrl/buscar?query=$query" - - // Filter - val pageParameter = if (page > 1) "?page=$page" else "" - - if (query.isBlank()) { - val ext = ".html" - var name: String - filters.forEach { filter -> - when (filter) { - is GenreFilter -> { - if (filter.toUriPart().isNotBlank() && filter.state != 0) { - name = filter.toUriPart() - return GET("$baseUrl/genero/$name$ext$pageParameter", headers) - } - } - is AlphabeticoFilter -> { - if (filter.toUriPart().isNotBlank() && filter.state != 0) { - name = filter.toUriPart() - return GET("$baseUrl/letra/$name$ext$pageParameter", headers) - } - } - is ListaCompletasFilter -> { - if (filter.toUriPart().isNotBlank() && filter.state != 0) { - name = filter.toUriPart() - return GET("$baseUrl/$name$pageParameter", headers) - } - } - else -> {} - } - } - } - - return GET(searchUrl, headers) - } - - override fun searchMangaParse(response: Response): MangasPage { - return if (response.request.url.toString().contains("query=")) { - super.searchMangaParse(response) - } else { - popularMangaParse(response) - } - } - - // get contents of a url - private fun getUrlContents(url: String): Document = client.newCall(GET(url, headers)).execute().asJsoup() + override fun popularMangaNextPageSelector() = "ul.pagination a[rel=next]" override fun popularMangaFromElement(element: Element): SManga { return SManga.create().apply { @@ -108,16 +52,93 @@ class HeavenManga : ParsedHttpSource() { } } + override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers) + + override fun latestUpdatesParse(response: Response): MangasPage { + val document = response.asJsoup() + + val mangas = document.selectFirst(latestUpdatesWrapperSelector())!! + .select(latestUpdatesSelector()) + .map { element -> + latestUpdatesFromElement(element) + }.distinctBy { it.url } + + val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) + } + + private fun latestUpdatesWrapperSelector() = ".container #loop-content " + + override fun latestUpdatesSelector() = "span.list-group-item:not(:has(> div.row:containsOwn(Novela)))" + + override fun latestUpdatesNextPageSelector(): String? = null + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { - element.select("a").let { - val latestChapter = getUrlContents(it.attr("href")) - val url = latestChapter.select(".rpwe-clearfix:last-child a") - setUrlWithoutDomain(url.attr("href")) - title = it.select("span span").text() - thumbnail_url = it.select("img").attr("src") + with(element.selectFirst("a")!!) { + val mangaUrl = attr("href").substringBeforeLast("/") + setUrlWithoutDomain(mangaUrl) + title = select(".captitle").text() + thumbnail_url = mangaUrl.replace("/manga/", "/uploads/manga/") + "/cover/cover_250x350.jpg" } } + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder() + if (query.isNotBlank()) { + if (query.length < 3) throw Exception("La búsqueda debe tener al menos 3 caracteres") + url.addPathSegment("buscar") + .addQueryParameter("query", query) + } else { + val ext = ".html" + var name: String + filters.forEach { filter -> + when (filter) { + is GenreFilter -> { + if (filter.toUriPart().isNotBlank() && filter.state != 0) { + name = filter.toUriPart() + url.addPathSegment("genero") + .addPathSegment(name + ext) + } + } + is AlphabeticoFilter -> { + if (filter.toUriPart().isNotBlank() && filter.state != 0) { + name = filter.toUriPart() + url.addPathSegment("letra") + .addPathSegment("manga$ext") + .addQueryParameter("alpha", name) + } + } + is ListaCompletasFilter -> { + if (filter.toUriPart().isNotBlank() && filter.state != 0) { + name = filter.toUriPart() + url.addPathSegment(name) + } + } + else -> {} + } + } + } + + if (page > 1) url.addQueryParameter("page", page.toString()) + + return GET(url.build(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + return if (response.request.url.pathSegments.contains("buscar")) { + super.searchMangaParse(response) + } else { + popularMangaParse(response) + } + } + + override fun searchMangaSelector() = "div.c-tabs-item__content" + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaFromElement(element: Element) = SManga.create().apply { element.select("h4 a").let { title = it.text() @@ -126,16 +147,6 @@ class HeavenManga : ParsedHttpSource() { thumbnail_url = element.select("img").attr("abs:data-src") } - override fun chapterFromElement(element: Element): SChapter { - return SChapter.create().apply { - element.select("a").let { - name = it.text() - setUrlWithoutDomain(it.attr("href")) - } - scanlator = element.select("span.pull-right").text() - } - } - override fun mangaDetailsParse(document: Document) = SManga.create().apply { document.select("div.tab-summary").let { info -> genre = info.select("div.genres-content a").joinToString { it.text() } @@ -144,27 +155,60 @@ class HeavenManga : ParsedHttpSource() { description = document.select("div.description-summary p").text() } - override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + override fun chapterListRequest(manga: SManga): Request { + val mangaUrl = (baseUrl + manga.url).toHttpUrl().newBuilder() + .addQueryParameter("columns[0][data]", "number") + .addQueryParameter("columns[0][orderable]", "true") + .addQueryParameter("columns[1][data]", "created_at") + .addQueryParameter("columns[1][searchable]", "true") + .addQueryParameter("order[0][column]", "1") + .addQueryParameter("order[0][dir]", "desc") + .addQueryParameter("start", "0") + .addQueryParameter("length", CHAPTER_LIST_LIMIT.toString()) + + val headers = headersBuilder() + .add("X-Requested-With", "XMLHttpRequest") + .build() + + return GET(mangaUrl.build(), headers) + } + + override fun chapterListParse(response: Response): List { + val mangaUrl = response.request.url.toString().substringBefore("?").removeSuffix("/") + val result = json.decodeFromString(response.body.string()) + return result.data.map { + SChapter.create().apply { + name = "Capítulo: ${it.slug}" + setUrlWithoutDomain("$mangaUrl/${it.slug}#${it.id}") + date_upload = runCatching { dateFormat.parse(it.createdAt)?.time ?: 0 }.getOrDefault(0) + } + } + } + + override fun chapterListSelector() = throw UnsupportedOperationException() + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() override fun pageListRequest(chapter: SChapter): Request { - return getUrlContents(baseUrl + chapter.url).select("a[id=leer]").attr("abs:href") - .let { GET(it, headers) } + val chapterId = chapter.url.substringAfterLast("#") + if (chapterId.isBlank()) throw Exception("Error al obtener el id del capítulo. Actualice la lista") + val url = "$baseUrl/manga/leer/$chapterId" + return GET(url, headers) } override fun pageListParse(document: Document): List { - return document.select("script:containsData(pUrl)").first()!!.data() - .substringAfter("pUrl=[").substringBefore("\"},];").split("\"},") - .mapIndexed { i, string -> Page(i, "", string.substringAfterLast("\"")) } + val data = document.select("script:containsData(pUrl)").first()!!.data() + val jsonString = PAGES_REGEX.find(data)!!.groupValues[1].removeTrailingComma() + val pages = json.decodeFromString>(jsonString) + return pages.mapIndexed { i, dto -> Page(i, "", dto.imgURL) } } - /** - * Array.from(document.querySelectorAll('.categorias a')).map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n') - * on https://heavenmanga.com/top/ - * */ + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + private class GenreFilter : UriPartFilter( "Géneros", arrayOf( - Pair("Todo", ""), + Pair("", ""), Pair("Accion", "accion"), Pair("Adulto", "adulto"), Pair("Aventura", "aventura"), @@ -258,7 +302,8 @@ class HeavenManga : ParsedHttpSource() { private class AlphabeticoFilter : UriPartFilter( "Alfabético", arrayOf( - Pair("Todo", ""), + Pair("", ""), + Pair("Other", "Other"), Pair("A", "a"), Pair("B", "b"), Pair("C", "c"), @@ -296,7 +341,7 @@ class HeavenManga : ParsedHttpSource() { private class ListaCompletasFilter : UriPartFilter( "Lista Completa", arrayOf( - Pair("Todo", ""), + Pair("", ""), Pair("Lista Comis", "comic"), Pair("Lista Novelas", "novela"), Pair("Lista Adulto", "adulto"), @@ -317,4 +362,13 @@ class HeavenManga : ParsedHttpSource() { Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { fun toUriPart() = vals[state].second } + + private fun String.removeTrailingComma() = replace(TRAILING_COMMA_REGEX, "$1") + + companion object { + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + val PAGES_REGEX = """pUrl\s*=\s*(\[.*\])\s*;""".toRegex() + val TRAILING_COMMA_REGEX = """,\s*(\}|\])""".toRegex() + private const val CHAPTER_LIST_LIMIT = 10000 + } } diff --git a/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenMangaDto.kt b/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenMangaDto.kt new file mode 100644 index 000000000..9c5cdef59 --- /dev/null +++ b/src/es/heavenmanga/src/eu/kanade/tachiyomi/extension/es/heavenmanga/HeavenMangaDto.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.extension.es.heavenmanga + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PayloadChaptersDto( + val data: List, +) + +@Serializable +data class ChapterDto( + val id: Int, + val slug: String, + @SerialName("created_at") val createdAt: String, +) + +@Serializable +data class PageDto( + val imgURL: String, +)