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) }