YushukeMangas: Fix content loading and bump versionID (#6986)

* Fix content loading and bump versionID

* Remove mutableList instance

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Remove use function

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Remove unused code

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Chopper 2025-01-07 15:24:02 -03:00 committed by Draff
parent 73013e9d46
commit d7f724243c
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 162 additions and 60 deletions

View File

@ -13,8 +13,8 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="yushukemangas.com" android:host="new.yushukemangas.com"
android:pathPattern="/manga" android:pathPattern="/manga/..*"
android:scheme="https" /> android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@ -1,8 +1,7 @@
ext { ext {
extName = 'Yushuke Mangas' extName = 'Yushuke Mangas'
extClass = '.YushukeMangas' extClass = '.YushukeMangas'
extVersionCode = 1 extVersionCode = 2
isNsfw = true
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -11,81 +11,93 @@ 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 kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
class YushukeMangas : ParsedHttpSource() { class YushukeMangas : ParsedHttpSource() {
override val name = "Yushuke Mangas" override val name = "Yushuke Mangas"
override val baseUrl = "https://yushukemangas.com" override val baseUrl = "https://new.yushukemangas.com"
override val lang = "pt-BR" override val lang = "pt-BR"
override val supportsLatest = true override val supportsLatest = true
override val versionId = 2
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.rateLimit(3) .rateLimit(1, 2)
.build() .build()
private val json: Json by injectLazy()
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers) override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = ".popular-manga-widget .popular-manga-item" override fun popularMangaSelector() = "#semanal a.top-item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply { override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text() title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src") thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) setUrlWithoutDomain(element.absUrl("href"))
} }
override fun popularMangaNextPageSelector() = null override fun popularMangaNextPageSelector() = null
// ============================== Latest =============================== // ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers) override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/obras", headers)
override fun latestUpdatesSelector() = ".manga-list .manga-item" override fun latestUpdatesSelector() = ".obras-grid .manga-card a"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
title = element.selectFirst("h2")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun latestUpdatesNextPageSelector() = ".pagination-next" override fun latestUpdatesNextPageSelector() = null
// ============================== Search =============================== // ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var url = "$baseUrl/search".toHttpUrl().newBuilder() val urlFilterBuilder = filters.fold("$baseUrl/obras".toHttpUrl().newBuilder()) { urlBuilder, filter ->
.addQueryParameter("q", query)
.addQueryParameter("page", "1")
.build()
filters.forEach { filter ->
when (filter) { when (filter) {
is GenreFilter -> { is RadioFilter -> {
val selected = filter.selected() val selected = filter.selected()
if (selected == all) return@forEach if (selected == all) return@fold urlBuilder
url = "$baseUrl/generos.php".toHttpUrl().newBuilder() urlBuilder.addQueryParameter(filter.query, selected)
.addQueryParameter("genre", selected)
.addQueryParameter("search", query)
.build()
} }
else -> {} is GenreFilter -> {
filter.state
.filter(GenreCheckBox::state)
.fold(urlBuilder) { builder, genre ->
builder.addQueryParameter(filter.query, genre.id)
}
}
else -> urlBuilder
} }
} }
return GET(url, headers)
val url = when {
query.isBlank() -> urlFilterBuilder
else -> baseUrl.toHttpUrl().newBuilder().addQueryParameter("search", query)
}
return GET(url.build(), headers)
} }
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) { if (query.startsWith(PREFIX_SEARCH)) {
val id = query.substringAfter(PREFIX_SEARCH) val slug = query.substringAfter(PREFIX_SEARCH)
return client.newCall(GET("$baseUrl/manga?id=$id", headers)) return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
val manga = mangaDetailsParse(it.asJsoup()) val manga = mangaDetailsParse(it.asJsoup())
@ -95,12 +107,24 @@ class YushukeMangas : ParsedHttpSource() {
return super.fetchSearchManga(page, query, filters) return super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaSelector() = "${latestUpdatesSelector()}, a.search-item" override fun searchMangaSelector() = ".search-result-item"
override fun searchMangaParse(response: Response): MangasPage {
return if (response.request.url.queryParameter("search").isNullOrBlank()) {
latestUpdatesParse(response)
} else {
super.searchMangaParse(response)
}
}
override fun searchMangaFromElement(element: Element) = SManga.create().apply { override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3, .search-title")!!.text() title = element.selectFirst(".search-result-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src") thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain((element.selectFirst("a") ?: element).absUrl("href")) setUrlWithoutDomain(
element.attr("onclick").let {
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
},
)
} }
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
@ -108,71 +132,150 @@ class YushukeMangas : ParsedHttpSource() {
// ============================== Manga Details ========================= // ============================== Manga Details =========================
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val details = document.selectFirst(".manga-header")!! val details = document.selectFirst(".manga-banner .container")!!
title = details.selectFirst("h1")!!.text() title = details.selectFirst("h1")!!.text()
thumbnail_url = details.selectFirst(".manga-image img")?.absUrl("src") thumbnail_url = details.selectFirst("img")?.absUrl("src")
genre = details.select(".manga-generos .genre-button").joinToString { it.text() } genre = details.select(".genre-tag").joinToString { it.text() }
description = details.selectFirst("p.manga-sinopse")?.text() description = details.selectFirst(".sinopse p")?.text()
details.selectFirst(".manga-meta > div")?.ownText()?.let {
status = when (it.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location()) setUrlWithoutDomain(document.location())
} }
private fun SManga.fetchMangaId(): String {
val document = client.newCall(mangaDetailsRequest(this)).execute().asJsoup()
return document.select("script")
.map(Element::data)
.firstOrNull(MANGA_ID_REGEX::containsMatchIn)
?.let { MANGA_ID_REGEX.find(it)?.groups?.get(1)?.value }
?: throw Exception("Manga ID não encontrado")
}
// ============================== Chapters =============================== // ============================== Chapters ===============================
override fun chapterListSelector() = ".chapter-list .chapter-item a" override fun chapterListSelector() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply { override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".chapter-number")!!.text() name = element.selectFirst(".chapter-number")!!.text()
setUrlWithoutDomain(element.absUrl("href")) setUrlWithoutDomain(element.absUrl("href"))
} }
private fun chapterListNextPageSelector() = latestUpdatesNextPageSelector()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
var page = 1 var page = 1
do { do {
val document = fetchChapterListPage(manga, page++) val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement) chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (document.selectFirst(chapterListNextPageSelector()) != null) } while (dto.hasNext())
return Observable.just(chapters) return Observable.just(chapters)
} }
private fun fetchChapterListPage(manga: SManga, page: Int): Document { private fun fetchChapterListPage(mangaId: String, page: Int): Response {
val url = "$baseUrl/ajax/load_more_chapters.php?order=DESC".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("page", page.toString())
.build()
return client return client
.newCall(GET("$baseUrl${manga.url}&page=$page", headers)) .newCall(GET(url, headers))
.execute().asJsoup() .execute()
} }
// ============================== Pages =============================== // ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images img").mapIndexed { index, imageUrl -> return document.select(".manga-container .manga-image").mapIndexed { index, imageUrl ->
Page(index, imageUrl = imageUrl.absUrl("src")) Page(index, imageUrl = imageUrl.absUrl("src"))
} }
} }
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
// ============================== Filters =============================== // ============================== Filters =============================
open class GenreFilter(displayName: String, private val vals: Array<String>, state: Int = 0) : override fun getFilterList(): FilterList {
Filter.Select<String>(displayName, vals, state) { return FilterList(
RadioFilter("Status", "status", statusList),
RadioFilter("Tipo", "tipo", typeList),
GenreFilter("Gêneros", "tags[]", genresList),
)
}
class RadioFilter(
displayName: String,
val query: String,
private val vals: Array<String>,
state: Int = 0,
) : Filter.Select<String>(displayName, vals, state) {
fun selected() = vals[state] fun selected() = vals[state]
} }
override fun getFilterList() = FilterList(GenreFilter("Gêneros", genresList)) protected class GenreFilter(
title: String,
val query: String,
genres: List<String>,
) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it) })
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
private val all = "Todos" private val all = "Todos"
private val genresList = arrayOf( private val statusList = arrayOf(
all, "+18", "Abuso", "Adulto", "Amor Puro", "Artes Marciais", all,
"Aventura", "Ação", "Comédia", "Crime", "Cultivação", "Drama", "Em andamento",
"Fantasia", "Gap Girls", "Gore", "Harém", "Histórico", "Horror", "Completo",
"Isekai", "Mistério", "Overpowered", "Psicológico", "Reencarnação", "Cancelado",
"Romance", "Sistema", "Tragédia", "Viagem no Tempo", "Violência", "Hiato",
) )
private val typeList = arrayOf(
all,
"Mangá",
"Manhwa",
"Manhua",
"Comics",
)
private var genresList: List<String> = listOf(
"Ação", "Artes Marciais", "Aventura",
"Comédia",
"Drama",
"Escolar",
"Esporte",
"Fantasia",
"Harém", "Histórico",
"Isekai",
"Josei",
"Mistério",
"Reencarnação", "Regressão", "Romance",
"Sci-fi", "Seinen", "Shoujo", "Shounen", "Slice of Life", "Sobrenatural", "Super Poderes",
"Terror",
"Vingança",
)
// ============================== Utilities ===========================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromStream(body.byteStream())
}
@Serializable
class ChaptersDto(val chapters: String, private val remaining: Int) {
fun hasNext() = remaining > 0
}
companion object { companion object {
const val PREFIX_SEARCH = "id:" const val PREFIX_SEARCH = "id:"
val SEARCH_URL_REGEX = "'([^']+)".toRegex()
val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
} }
} }

View File

@ -13,11 +13,11 @@ class YushukeMangasUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val id = intent?.data?.getQueryParameter("id") val pathSegment = intent?.data?.pathSegments
if (id != null) { if (pathSegment != null && pathSegment.size > 1) {
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}$id") putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
putExtra("filter", packageName) putExtra("filter", packageName)
} }