HeavenManga: Fix chapter list and add latest tab (#1206)
CI / Prepare job (push) Successful in 4s Details
CI / Build multisrc modules (push) Successful in 3m39s Details
CI / Build individual modules (push) Successful in 37s Details
CI / Publish repo (push) Successful in 39s Details

Fixes
This commit is contained in:
bapeey 2024-02-12 07:51:31 -05:00 committed by Draff
parent 2556e117be
commit c5c6d77479
3 changed files with 174 additions and 99 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'HeavenManga' extName = 'HeavenManga'
extClass = '.HeavenManga' extClass = '.HeavenManga'
extVersionCode = 6 extVersionCode = 7
isNsfw = true isNsfw = true
} }

View File

@ -9,12 +9,17 @@ 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.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup 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.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class HeavenManga : ParsedHttpSource() { class HeavenManga : ParsedHttpSource() {
@ -24,81 +29,20 @@ class HeavenManga : ParsedHttpSource() {
override val lang = "es" override val lang = "es"
// latest is broken on the site, it's the same as popular so turning it off override val supportsLatest = true
override val supportsLatest = false
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder { override fun headersBuilder() = super.headersBuilder()
return Headers.Builder() .add("Referer", "$baseUrl/")
.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 popularMangaRequest(page: Int) = GET("$baseUrl/top?orderby=views&page=$page", headers) 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 { override fun popularMangaNextPageSelector() = "ul.pagination a[rel=next]"
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 popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply { 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 { override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
element.select("a").let { with(element.selectFirst("a")!!) {
val latestChapter = getUrlContents(it.attr("href")) val mangaUrl = attr("href").substringBeforeLast("/")
val url = latestChapter.select(".rpwe-clearfix:last-child a") setUrlWithoutDomain(mangaUrl)
setUrlWithoutDomain(url.attr("href")) title = select(".captitle").text()
title = it.select("span span").text() thumbnail_url = mangaUrl.replace("/manga/", "/uploads/manga/") + "/cover/cover_250x350.jpg"
thumbnail_url = it.select("img").attr("src")
} }
} }
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 { override fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.select("h4 a").let { element.select("h4 a").let {
title = it.text() title = it.text()
@ -126,16 +147,6 @@ class HeavenManga : ParsedHttpSource() {
thumbnail_url = element.select("img").attr("abs:data-src") 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 { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.select("div.tab-summary").let { info -> document.select("div.tab-summary").let { info ->
genre = info.select("div.genres-content a").joinToString { it.text() } 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() 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<SChapter> {
val mangaUrl = response.request.url.toString().substringBefore("?").removeSuffix("/")
val result = json.decodeFromString<PayloadChaptersDto>(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 { override fun pageListRequest(chapter: SChapter): Request {
return getUrlContents(baseUrl + chapter.url).select("a[id=leer]").attr("abs:href") val chapterId = chapter.url.substringAfterLast("#")
.let { GET(it, headers) } 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<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("script:containsData(pUrl)").first()!!.data() val data = document.select("script:containsData(pUrl)").first()!!.data()
.substringAfter("pUrl=[").substringBefore("\"},];").split("\"},") val jsonString = PAGES_REGEX.find(data)!!.groupValues[1].removeTrailingComma()
.mapIndexed { i, string -> Page(i, "", string.substringAfterLast("\"")) } val pages = json.decodeFromString<List<PageDto>>(jsonString)
return pages.mapIndexed { i, dto -> Page(i, "", dto.imgURL) }
} }
/** override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
* Array.from(document.querySelectorAll('.categorias a')).map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n')
* on https://heavenmanga.com/top/
* */
private class GenreFilter : UriPartFilter( private class GenreFilter : UriPartFilter(
"Géneros", "Géneros",
arrayOf( arrayOf(
Pair("Todo", ""), Pair("<Seleccionar>", ""),
Pair("Accion", "accion"), Pair("Accion", "accion"),
Pair("Adulto", "adulto"), Pair("Adulto", "adulto"),
Pair("Aventura", "aventura"), Pair("Aventura", "aventura"),
@ -258,7 +302,8 @@ class HeavenManga : ParsedHttpSource() {
private class AlphabeticoFilter : UriPartFilter( private class AlphabeticoFilter : UriPartFilter(
"Alfabético", "Alfabético",
arrayOf( arrayOf(
Pair("Todo", ""), Pair("<Seleccionar>", ""),
Pair("Other", "Other"),
Pair("A", "a"), Pair("A", "a"),
Pair("B", "b"), Pair("B", "b"),
Pair("C", "c"), Pair("C", "c"),
@ -296,7 +341,7 @@ class HeavenManga : ParsedHttpSource() {
private class ListaCompletasFilter : UriPartFilter( private class ListaCompletasFilter : UriPartFilter(
"Lista Completa", "Lista Completa",
arrayOf( arrayOf(
Pair("Todo", ""), Pair("<Seleccionar>", ""),
Pair("Lista Comis", "comic"), Pair("Lista Comis", "comic"),
Pair("Lista Novelas", "novela"), Pair("Lista Novelas", "novela"),
Pair("Lista Adulto", "adulto"), Pair("Lista Adulto", "adulto"),
@ -317,4 +362,13 @@ class HeavenManga : ParsedHttpSource() {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second 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
}
} }

View File

@ -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<ChapterDto>,
)
@Serializable
data class ChapterDto(
val id: Int,
val slug: String,
@SerialName("created_at") val createdAt: String,
)
@Serializable
data class PageDto(
val imgURL: String,
)