diff --git a/src/pt/yushukemangas/AndroidManifest.xml b/src/pt/yushukemangas/AndroidManifest.xml
index eb9cc45dc..eee4b4423 100644
--- a/src/pt/yushukemangas/AndroidManifest.xml
+++ b/src/pt/yushukemangas/AndroidManifest.xml
@@ -13,8 +13,8 @@
diff --git a/src/pt/yushukemangas/build.gradle b/src/pt/yushukemangas/build.gradle
index 684e0aa57..beb8f625b 100644
--- a/src/pt/yushukemangas/build.gradle
+++ b/src/pt/yushukemangas/build.gradle
@@ -1,8 +1,7 @@
ext {
extName = 'Yushuke Mangas'
extClass = '.YushukeMangas'
- extVersionCode = 1
- isNsfw = true
+ extVersionCode = 2
}
apply from: "$rootDir/common.gradle"
diff --git a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangas.kt b/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangas.kt
index 1895fe4f1..26b1d6da0 100644
--- a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangas.kt
+++ b/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangas.kt
@@ -11,81 +11,93 @@ 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 kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
+import uy.kohesive.injekt.injectLazy
class YushukeMangas : ParsedHttpSource() {
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 supportsLatest = true
+ override val versionId = 2
+
override val client = network.cloudflareClient.newBuilder()
- .rateLimit(3)
+ .rateLimit(1, 2)
.build()
+ private val json: Json by injectLazy()
+
// ============================== Popular ===============================
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 {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
- setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
+ setUrlWithoutDomain(element.absUrl("href"))
}
override fun popularMangaNextPageSelector() = null
// ============================== 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 {
- title = element.selectFirst("h2")!!.text()
- thumbnail_url = element.selectFirst("img")?.absUrl("src")
- setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
- }
+ override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
- override fun latestUpdatesNextPageSelector() = ".pagination-next"
+ override fun latestUpdatesNextPageSelector() = null
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- var url = "$baseUrl/search".toHttpUrl().newBuilder()
- .addQueryParameter("q", query)
- .addQueryParameter("page", "1")
- .build()
- filters.forEach { filter ->
+ val urlFilterBuilder = filters.fold("$baseUrl/obras".toHttpUrl().newBuilder()) { urlBuilder, filter ->
when (filter) {
- is GenreFilter -> {
+ is RadioFilter -> {
val selected = filter.selected()
- if (selected == all) return@forEach
- url = "$baseUrl/generos.php".toHttpUrl().newBuilder()
- .addQueryParameter("genre", selected)
- .addQueryParameter("search", query)
- .build()
+ if (selected == all) return@fold urlBuilder
+ urlBuilder.addQueryParameter(filter.query, selected)
}
- 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 {
if (query.startsWith(PREFIX_SEARCH)) {
- val id = query.substringAfter(PREFIX_SEARCH)
- return client.newCall(GET("$baseUrl/manga?id=$id", headers))
+ val slug = query.substringAfter(PREFIX_SEARCH)
+ return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map {
val manga = mangaDetailsParse(it.asJsoup())
@@ -95,12 +107,24 @@ class YushukeMangas : ParsedHttpSource() {
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 {
- title = element.selectFirst("h3, .search-title")!!.text()
+ title = element.selectFirst(".search-result-title")!!.text()
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
@@ -108,71 +132,150 @@ class YushukeMangas : ParsedHttpSource() {
// ============================== Manga Details =========================
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()
- thumbnail_url = details.selectFirst(".manga-image img")?.absUrl("src")
- genre = details.select(".manga-generos .genre-button").joinToString { it.text() }
- description = details.selectFirst("p.manga-sinopse")?.text()
+ thumbnail_url = details.selectFirst("img")?.absUrl("src")
+ genre = details.select(".genre-tag").joinToString { it.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())
}
+ 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 ===============================
- override fun chapterListSelector() = ".chapter-list .chapter-item a"
+ override fun chapterListSelector() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".chapter-number")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
- private fun chapterListNextPageSelector() = latestUpdatesNextPageSelector()
-
override fun fetchChapterList(manga: SManga): Observable> {
+ val mangaId = manga.fetchMangaId()
val chapters = mutableListOf()
var page = 1
do {
- val document = fetchChapterListPage(manga, page++)
+ val dto = fetchChapterListPage(mangaId, page++).parseAs()
+ val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
- } while (document.selectFirst(chapterListNextPageSelector()) != null)
+ } while (dto.hasNext())
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
- .newCall(GET("$baseUrl${manga.url}&page=$page", headers))
- .execute().asJsoup()
+ .newCall(GET(url, headers))
+ .execute()
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List {
- 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"))
}
}
override fun imageUrlParse(document: Document) = ""
- // ============================== Filters ===============================
+ // ============================== Filters =============================
- open class GenreFilter(displayName: String, private val vals: Array, state: Int = 0) :
- Filter.Select(displayName, vals, state) {
+ override fun getFilterList(): FilterList {
+ 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,
+ state: Int = 0,
+ ) : Filter.Select(displayName, vals, state) {
fun selected() = vals[state]
}
- override fun getFilterList() = FilterList(GenreFilter("Gêneros", genresList))
+ protected class GenreFilter(
+ title: String,
+ val query: String,
+ genres: List,
+ ) : Filter.Group(title, genres.map { GenreCheckBox(it) })
+
+ class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
private val all = "Todos"
- private val genresList = arrayOf(
- all, "+18", "Abuso", "Adulto", "Amor Puro", "Artes Marciais",
- "Aventura", "Ação", "Comédia", "Crime", "Cultivação", "Drama",
- "Fantasia", "Gap Girls", "Gore", "Harém", "Histórico", "Horror",
- "Isekai", "Mistério", "Overpowered", "Psicológico", "Reencarnação",
- "Romance", "Sistema", "Tragédia", "Viagem no Tempo", "Violência",
+ private val statusList = arrayOf(
+ all,
+ "Em andamento",
+ "Completo",
+ "Cancelado",
+ "Hiato",
)
+ private val typeList = arrayOf(
+ all,
+ "Mangá",
+ "Manhwa",
+ "Manhua",
+ "Comics",
+ )
+
+ private var genresList: List = 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 Response.parseAs(): T {
+ return json.decodeFromStream(body.byteStream())
+ }
+
+ @Serializable
+ class ChaptersDto(val chapters: String, private val remaining: Int) {
+ fun hasNext() = remaining > 0
+ }
+
companion object {
const val PREFIX_SEARCH = "id:"
+ val SEARCH_URL_REGEX = "'([^']+)".toRegex()
+ val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
}
}
diff --git a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt b/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt
index 78e58dab1..20d6940cf 100644
--- a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt
+++ b/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt
@@ -13,11 +13,11 @@ class YushukeMangasUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val id = intent?.data?.getQueryParameter("id")
- if (id != null) {
+ val pathSegment = intent?.data?.pathSegments
+ if (pathSegment != null && pathSegment.size > 1) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
- putExtra("query", "${YushukeMangas.PREFIX_SEARCH}$id")
+ putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
putExtra("filter", packageName)
}