YugenMangas: Fix content loading (#11742)

* Fix content loading

* Remove escape quotes and capture group

* Update domain
This commit is contained in:
Chopper 2025-11-21 19:25:36 -03:00 committed by Draff
parent a085a3cb0d
commit 8bd7ed9a90
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 111 additions and 93 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Yugen Mangás' extName = 'Yugen Mangás'
extClass = '.YugenMangas' extClass = '.YugenMangas'
extVersionCode = 48 extVersionCode = 49
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -23,16 +23,13 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs import keiyoushi.utils.parseAs
import kotlinx.serialization.encodeToString import okhttp3.HttpUrl.Companion.toHttpUrl
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import java.util.Calendar
class YugenMangas : HttpSource(), ConfigurableSource { class YugenMangas : HttpSource(), ConfigurableSource {
@ -45,7 +42,11 @@ class YugenMangas : HttpSource(), ConfigurableSource {
else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!! else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
} }
private val defaultBaseUrl: String = "https://yugenmangasbr.yocat.xyz" private val defaultBaseUrl: String = "https://yugenmangasbr.dxtg.online"
private val apiUrl: String = "https://api.yugenweb.com"
private val imageBaseUrl: String get() = "$baseUrl/_next/image?url=$apiUrl/media"
override val lang = "pt-BR" override val lang = "pt-BR"
@ -60,6 +61,7 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(2) .rateLimit(2)
.rateLimitHost(apiUrl.toHttpUrl(), 2)
.setRandomUserAgent( .setRandomUserAgent(
preferences.getPrefUAType(), preferences.getPrefUAType(),
preferences.getPrefCustomUA(), preferences.getPrefCustomUA(),
@ -68,8 +70,6 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override val versionId = 2 override val versionId = 2
private val json: Json by injectLazy()
init { init {
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain -> preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) { if (domain != defaultBaseUrl) {
@ -84,47 +84,35 @@ class YugenMangas : HttpSource(), ConfigurableSource {
// ================================ Popular ======================================= // ================================ Popular =======================================
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/series?page=$page&order=desc&sort=views", headers) GET("$baseUrl/biblioteca?page=$page&sort_order=desc&sort_by=total_views", headers)
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val dto = response.getJsonBody().parseAs<LibraryWrapper<MangaDto>>()
val script = document.selectFirst("script:containsData(initialSeries)")?.data() return MangasPage(dto.mangas.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
?: throw Exception(warning)
val json = POPULAR_MANGA_REGEX.find(script)?.groups?.get(1)?.value
?.replace(ESCAPE_QUOTATION_MARK_REGEX, "\"")
?: throw Exception("Erro ao analisar lista de mangás/manhwas")
val dto = json.parseAs<LibraryWrapper>()
return MangasPage(dto.mangas.map(MangaDetailsDto::toSManga), dto.hasNextPage())
} }
// ================================ Latest ======================================= // ================================ Latest =======================================
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/capitulos?page=$page", headers)
GET("$baseUrl/chapters?page=$page", headers)
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup() val dto = response.getJsonBody().parseAs<LibraryWrapper<LatestUpdateDto>>()
val mangas = document.select("div.bg-card a[href*=series]").map { element -> return MangasPage(dto.mangas.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.attrImageSet()
setUrlWithoutDomain(element.absUrl("href").substringBeforeLast("/"))
}
}.takeIf(List<SManga>::isNotEmpty) ?: throw Exception(warning)
return MangasPage(mangas, document.selectFirst("a[aria-label='Próxima página']:not([aria-disabled='true'])") != null)
} }
// ================================ Search ======================================= // ================================ Search =======================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payload = json.encodeToString(SearchDto(query)).toRequestBody(JSON_MEDIA_TYPE) val url = "$apiUrl/api/v2/library/series".toHttpUrl().newBuilder()
return POST("$baseUrl/api/search", headers, payload) .addQueryParameter("name", query)
.addQueryParameter("per_page", "10")
.build()
return GET(url, headers)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val mangas = response.parseAs<SearchMangaDto>().series.map(MangaDto::toSManga) val dto = response.parseAs<Library<MangaDto>>()
return MangasPage(mangas, hasNextPage = false) return MangasPage(dto.series.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
} }
// ================================ Details ======================================= // ================================ Details =======================================
@ -134,9 +122,17 @@ class YugenMangas : HttpSource(), ConfigurableSource {
title = document.selectFirst("h1")!!.text() title = document.selectFirst("h1")!!.text()
description = document.selectFirst("[property='og:description']")?.attr("content") description = document.selectFirst("[property='og:description']")?.attr("content")
thumbnail_url = document.selectFirst("img")?.attrImageSet() thumbnail_url = document.selectFirst("img")?.attrImageSet()
author = document.selectFirst("p:contains(Autor) ~ div")?.text() author = document.selectFirst("p:contains(Autor) + p")?.text()
artist = document.selectFirst("p:contains(Artista) ~ div")?.text() artist = document.selectFirst("p:contains(Artista) + p")?.text()
genre = document.select("p:contains(Gêneros) ~ div div.inline-flex").joinToString { it.text() } genre = document.select("div:has(h1) + div + div [data-slot='badge']").joinToString { it.text() }
document.selectFirst("div:has(h1) + div [data-slot='badge']:last-child")?.let {
status = when (it.text().lowercase()) {
"em lançamento" -> SManga.ONGOING
"finalizada" -> SManga.COMPLETED
"em hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
} }
// ================================ Chapters ======================================= // ================================ Chapters =======================================
@ -156,7 +152,7 @@ class YugenMangas : HttpSource(), ConfigurableSource {
private fun chapterListRequest(manga: SManga, page: Int): Request { private fun chapterListRequest(manga: SManga, page: Int): Request {
val url = super.chapterListRequest(manga).url.newBuilder() val url = super.chapterListRequest(manga).url.newBuilder()
.addQueryParameter("reverse", "true") .addQueryParameter("order", "desc")
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.build() .build()
return GET(url, headers) return GET(url, headers)
@ -164,20 +160,25 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
return document.select("a.flex.bg-card[href*=series]").map { element -> return document.select("""div.md\:hidden a[href*=reader]""")
SChapter.create().apply { .distinctBy { it.absUrl("href") }
name = element.selectFirst("p")!!.text() .map { element ->
setUrlWithoutDomain(element.absUrl("href")) SChapter.create().apply {
name = element.selectFirst("p")!!.text()
element.selectFirst("span:has( > .lucide-calendar)")?.let {
date_upload = parseRelativeDate(it.text())
}
setUrlWithoutDomain(element.absUrl("href"))
}
} }
}
} }
// ================================ Pages =======================================} // ================================ Pages =======================================}
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val pages = response.getJsonBody().parseAs<List<PageDto>>()
return document.select("img[alt^=página]").mapIndexed { index, element -> return pages.sortedBy(PageDto::number).mapIndexed { index, src ->
Page(index, imageUrl = element.absUrl("src")) Page(index, imageUrl = "$apiUrl/media/${src.path}")
} }
} }
@ -218,6 +219,29 @@ class YugenMangas : HttpSource(), ConfigurableSource {
?.let { "$baseUrl$it" } ?.let { "$baseUrl$it" }
} }
private fun Response.getJsonBody(): String {
val document = asJsoup()
val script = document.select("script").map(Element::data)
.firstOrNull { script -> GET_JSON_BODY_REGEX.containsMatchIn(script) }
?: throw Exception(warning)
val values = GET_JSON_BODY_REGEX.find(script)?.groupValues?.filter(String::isNotBlank)
return values?.last()
?.let { "\"$it\"".parseAs<String>() }
?: throw Exception("Erro ao analisar os dados")
}
private fun parseRelativeDate(date: String): Long {
val number = DATE_REGEX.find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
date.contains("mês") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
date.contains("dia") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
date.contains("hora") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
else -> 0
}
}
private val warning = """ private val warning = """
Não foi possível localizar a lista de mangás/manhwas. Não foi possível localizar a lista de mangás/manhwas.
Tente atualizar a URL acessando: Extensões > $name > Configurações. Tente atualizar a URL acessando: Extensões > $name > Configurações.
@ -230,9 +254,10 @@ class YugenMangas : HttpSource(), ConfigurableSource {
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl" private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações" private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida." private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."
private val JSON_MEDIA_TYPE = "application/json".toMediaType() private val MANGA_REGEX = """(\{\\"initialData.+\"\}.+)(?:\]\}){2}\]""".toRegex()
private val POPULAR_MANGA_REGEX = """(\{\\"initialSeries.+\"\})\]""".toRegex() private val PAGES_REGEX = """pages\\":(\[.+\])\}\}""".toRegex()
private val ESCAPE_QUOTATION_MARK_REGEX = """\\"""".toRegex() private val GET_JSON_BODY_REGEX = """$MANGA_REGEX|$PAGES_REGEX""".toRegex()
private val DATE_REGEX = """\d+""".toRegex()
private val SRCSET_DELIMITER_REGEX = """\d+w,?""".toRegex() private val SRCSET_DELIMITER_REGEX = """\d+w,?""".toRegex()
} }
} }

View File

@ -3,66 +3,59 @@ package eu.kanade.tachiyomi.extension.pt.yugenmangas
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable @Serializable
class LibraryWrapper( class LibraryWrapper<T>(
@SerialName("initialSeries") @SerialName("initialData")
val mangas: List<MangaDetailsDto>, private val library: Library<T>,
) {
val mangas: List<T> get() = library.series
fun hasNextPage() = library.hasNextPage()
}
@Serializable
class Library<T>(
@JsonNames("updates")
val series: List<T> = emptyList(),
val pagination: Pagination,
) {
fun hasNextPage() = pagination.hasNextPage()
}
@Serializable
class Pagination(
@SerialName("current_page")
val currentPage: Int = 0, val currentPage: Int = 0,
@SerialName("total_pages")
val totalPages: Int = 0, val totalPages: Int = 0,
) { ) {
fun hasNextPage() = currentPage < totalPages fun hasNextPage() = currentPage < totalPages
} }
@Serializable @Serializable
class MangaDetailsDto( class LatestUpdateDto(
val title: String, val series: MangaDto,
@SerialName("path_cover")
val cover: String,
val code: String,
val author: String? = null,
val artist: String? = null,
val genres: List<String> = emptyList(),
val synopsis: String? = null,
val status: String? = null,
) { ) {
fun toSManga(): SManga = SManga.create().apply { fun toSManga(baseUrl: String) = series.toSManga(baseUrl)
title = this@MangaDetailsDto.title
author = this@MangaDetailsDto.author
artist = this@MangaDetailsDto.artist
description = synopsis
status = when (this@MangaDetailsDto.status) {
"Em Lançamento" -> SManga.ONGOING
"Hiato" -> SManga.ON_HIATUS
"Cancelado" -> SManga.CANCELLED
"Finalizado" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = genres.joinToString()
thumbnail_url = cover
url = "/series/$code"
}
} }
@Serializable
class SearchDto(
val query: String,
)
@Serializable
class SearchMangaDto(
val series: List<MangaDto>,
)
@Serializable @Serializable
class MangaDto( class MangaDto(
val code: String, val code: String,
val cover: String, val cover: String,
val name: String, val name: String = "",
val title: String = "",
) { ) {
fun toSManga() = SManga.create().apply { fun toSManga(baseUrl: String) = SManga.create().apply {
title = name title = this@MangaDto.title.takeIf(String::isNotBlank) ?: name
thumbnail_url = cover thumbnail_url = "$baseUrl/$cover&w=640&q=75"
url = "/series/$code" url = "/series/$code"
} }
} }
@Serializable
class PageDto(
val path: String,
val number: Long,
)