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 {
extName = 'Yugen Mangás'
extClass = '.YugenMangas'
extVersionCode = 48
extVersionCode = 49
}
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.setRandomUserAgent
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.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
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 keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
class YugenMangas : HttpSource(), ConfigurableSource {
@ -45,7 +42,11 @@ class YugenMangas : HttpSource(), ConfigurableSource {
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"
@ -60,6 +61,7 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(2)
.rateLimitHost(apiUrl.toHttpUrl(), 2)
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
@ -68,8 +70,6 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override val versionId = 2
private val json: Json by injectLazy()
init {
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
@ -84,47 +84,35 @@ class YugenMangas : HttpSource(), ConfigurableSource {
// ================================ Popular =======================================
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 {
val document = response.asJsoup()
val script = document.selectFirst("script:containsData(initialSeries)")?.data()
?: 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())
val dto = response.getJsonBody().parseAs<LibraryWrapper<MangaDto>>()
return MangasPage(dto.mangas.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
}
// ================================ Latest =======================================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/chapters?page=$page", headers)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/capitulos?page=$page", headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("div.bg-card a[href*=series]").map { element ->
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)
val dto = response.getJsonBody().parseAs<LibraryWrapper<LatestUpdateDto>>()
return MangasPage(dto.mangas.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
}
// ================================ Search =======================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val payload = json.encodeToString(SearchDto(query)).toRequestBody(JSON_MEDIA_TYPE)
return POST("$baseUrl/api/search", headers, payload)
val url = "$apiUrl/api/v2/library/series".toHttpUrl().newBuilder()
.addQueryParameter("name", query)
.addQueryParameter("per_page", "10")
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val mangas = response.parseAs<SearchMangaDto>().series.map(MangaDto::toSManga)
return MangasPage(mangas, hasNextPage = false)
val dto = response.parseAs<Library<MangaDto>>()
return MangasPage(dto.series.map { it.toSManga(imageBaseUrl) }, dto.hasNextPage())
}
// ================================ Details =======================================
@ -134,9 +122,17 @@ class YugenMangas : HttpSource(), ConfigurableSource {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst("[property='og:description']")?.attr("content")
thumbnail_url = document.selectFirst("img")?.attrImageSet()
author = document.selectFirst("p:contains(Autor) ~ div")?.text()
artist = document.selectFirst("p:contains(Artista) ~ div")?.text()
genre = document.select("p:contains(Gêneros) ~ div div.inline-flex").joinToString { it.text() }
author = document.selectFirst("p:contains(Autor) + p")?.text()
artist = document.selectFirst("p:contains(Artista) + p")?.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 =======================================
@ -156,7 +152,7 @@ class YugenMangas : HttpSource(), ConfigurableSource {
private fun chapterListRequest(manga: SManga, page: Int): Request {
val url = super.chapterListRequest(manga).url.newBuilder()
.addQueryParameter("reverse", "true")
.addQueryParameter("order", "desc")
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
@ -164,20 +160,25 @@ class YugenMangas : HttpSource(), ConfigurableSource {
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select("a.flex.bg-card[href*=series]").map { element ->
SChapter.create().apply {
name = element.selectFirst("p")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
return document.select("""div.md\:hidden a[href*=reader]""")
.distinctBy { it.absUrl("href") }
.map { element ->
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 =======================================}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("img[alt^=página]").mapIndexed { index, element ->
Page(index, imageUrl = element.absUrl("src"))
val pages = response.getJsonBody().parseAs<List<PageDto>>()
return pages.sortedBy(PageDto::number).mapIndexed { index, src ->
Page(index, imageUrl = "$apiUrl/media/${src.path}")
}
}
@ -218,6 +219,29 @@ class YugenMangas : HttpSource(), ConfigurableSource {
?.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 = """
Não foi possível localizar a lista de mangás/manhwas.
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 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 val JSON_MEDIA_TYPE = "application/json".toMediaType()
private val POPULAR_MANGA_REGEX = """(\{\\"initialSeries.+\"\})\]""".toRegex()
private val ESCAPE_QUOTATION_MARK_REGEX = """\\"""".toRegex()
private val MANGA_REGEX = """(\{\\"initialData.+\"\}.+)(?:\]\}){2}\]""".toRegex()
private val PAGES_REGEX = """pages\\":(\[.+\])\}\}""".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()
}
}

View File

@ -3,66 +3,59 @@ package eu.kanade.tachiyomi.extension.pt.yugenmangas
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
class LibraryWrapper(
@SerialName("initialSeries")
val mangas: List<MangaDetailsDto>,
class LibraryWrapper<T>(
@SerialName("initialData")
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,
@SerialName("total_pages")
val totalPages: Int = 0,
) {
fun hasNextPage() = currentPage < totalPages
}
@Serializable
class MangaDetailsDto(
val title: String,
@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,
class LatestUpdateDto(
val series: MangaDto,
) {
fun toSManga(): SManga = SManga.create().apply {
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"
}
fun toSManga(baseUrl: String) = series.toSManga(baseUrl)
}
@Serializable
class SearchDto(
val query: String,
)
@Serializable
class SearchMangaDto(
val series: List<MangaDto>,
)
@Serializable
class MangaDto(
val code: String,
val cover: String,
val name: String,
val name: String = "",
val title: String = "",
) {
fun toSManga() = SManga.create().apply {
title = name
thumbnail_url = cover
fun toSManga(baseUrl: String) = SManga.create().apply {
title = this@MangaDto.title.takeIf(String::isNotBlank) ?: name
thumbnail_url = "$baseUrl/$cover&w=640&q=75"
url = "/series/$code"
}
}
@Serializable
class PageDto(
val path: String,
val number: Long,
)