Add YuYu multsrc (#8080)

This commit is contained in:
Chopper 2025-03-15 03:29:01 -03:00 committed by Draff
parent a4cd04699c
commit 34b4284d7c
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
26 changed files with 314 additions and 357 deletions

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application> <application>
<activity <activity
android:name=".pt.yushukemangas.YushukeMangasUrlActivity" android:name="eu.kanade.tachiyomi.multisrc.yuyu.YuYuUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -11,11 +11,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="new.yushukemangas.com" android:host="${SOURCEHOST}"
android:pathPattern="/manga/..*" android:pathPattern="/..*"
android:scheme="https" /> android:scheme="${SOURCESCHEME}" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -0,0 +1,206 @@
package eu.kanade.tachiyomi.multisrc.yuyu
import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
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 keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
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 java.net.URLEncoder
abstract class YuYu(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val client = network.cloudflareClient
override val supportsLatest = true
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = ".top10-section .top10-item a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.absUrl("href"))
}
override fun popularMangaNextPageSelector() = null
// ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.build()
return GET(url, headers)
}
override fun latestUpdatesSelector() = ".manga-list .manga-card"
override fun latestUpdatesNextPageSelector() = "a.page-link:contains(>)"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
val url = element.selectFirst("a.manga-cover")!!.absUrl("href")
val uri = Uri.parse(url)
val pathSegments = uri.pathSegments
val lastSegment = URLEncoder.encode(pathSegments.last(), "UTF-8")
val encodedUrl = uri.buildUpon()
.path(pathSegments.dropLast(1).joinToString("/") + "/$lastSegment")
.toString()
title = element.selectFirst("a.manga-title")!!.text()
thumbnail_url = element.selectFirst("a.manga-cover img")?.absUrl("data-src")
setUrlWithoutDomain(encodedUrl)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
return MangasPage(mangas, document.hasNextPage())
}
private fun Document.hasNextPage() =
selectFirst(latestUpdatesNextPageSelector())?.absUrl("href")?.let {
selectFirst("a.page-link.active")
?.absUrl("href")
.equals(it, ignoreCase = true).not()
} ?: false
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("search", query)
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map {
val manga = mangaDetailsParse(it.asJsoup())
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-result-item"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(".search-result-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(
element.attr("onclick").let {
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
},
)
}
override fun searchMangaNextPageSelector() = null
// ============================== Manga Details =========================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val details = document.selectFirst(".manga-banner .container")!!
title = details.selectFirst("h1")!!.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() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".capitulo-numero")!!.ownText()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
return Observable.just(chapters)
}
private fun fetchChapterListPage(mangaId: String, page: Int): Response {
val url = "$baseUrl/ajax/lzmvke.php?order=DESC".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("page", page.toString())
.build()
return client
.newCall(GET(url, headers))
.execute()
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select("picture img").mapIndexed { idx, element ->
Page(idx, imageUrl = element.absUrl("src"))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================== Utilities ===========================
@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()
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.yushukemangas package eu.kanade.tachiyomi.multisrc.yuyu
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
class YushukeMangasUrlActivity : Activity() { class YuYuUrlActivity : Activity() {
private val tag = javaClass.simpleName private val tag = javaClass.simpleName
@ -17,7 +17,7 @@ class YushukeMangasUrlActivity : Activity() {
if (pathSegment != null && pathSegment.size > 1) { 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}${pathSegment[1]}") putExtra("query", "${YuYu.PREFIX_SEARCH}${pathSegment[1]}")
putExtra("filter", packageName) putExtra("filter", packageName)
} }

View File

@ -0,0 +1,10 @@
ext {
extName = 'Ego Toons'
extClass = '.EgoToons'
themePkg = 'yuyu'
baseUrl = 'https://egotoons.com'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.egotoons
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class EgoToons : YuYu(
"Ego Toons",
"https://egotoons.com",
"pt-BR",
) {
override val client = super.client.newBuilder()
.rateLimit(2)
.build()
}

View File

@ -1,9 +1,9 @@
ext { ext {
extName = 'Galinha Samurai Scan' extName = 'Galinha Samurai Scan'
extClass = '.GalinhaSamuraiScan' extClass = '.GalinhaSamuraiScan'
themePkg = 'madara' themePkg = 'yuyu'
baseUrl = 'https://galinhasamurai.com' baseUrl = 'https://galinhasamurai.com'
overrideVersionCode = 0 overrideVersionCode = 41
isNsfw = false isNsfw = false
} }

View File

@ -1,15 +1,17 @@
package eu.kanade.tachiyomi.extension.pt.galinhasamuraiscan package eu.kanade.tachiyomi.extension.pt.galinhasamuraiscan
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import java.text.SimpleDateFormat import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.util.Locale
class GalinhaSamuraiScan : Madara( class GalinhaSamuraiScan : YuYu(
"Galinha Samurai Scan", "Galinha Samurai Scan",
"https://galinhasamurai.com", "https://galinhasamurai.com",
"pt-BR", "pt-BR",
dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR")),
) { ) {
override val useLoadMoreRequest = LoadMoreStrategy.Always override val client = super.client.newBuilder()
override val useNewChapterEndpoint = true .rateLimit(2)
.build()
// Moved from Madara to YuYu
override val versionId = 2
} }

View File

@ -0,0 +1,10 @@
ext {
extName = 'Neko Toons'
extClass = '.NekoToons'
themePkg = 'yuyu'
baseUrl = 'https://nekotoons.site'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.nekotoons
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class NekoToons : YuYu(
"Neko Toons",
"https://nekotoons.site",
"pt-BR",
) {
override val client = super.client.newBuilder()
.rateLimit(2)
.build()
}

View File

@ -1,9 +1,9 @@
ext { ext {
extName = 'Pluma Comics' extName = 'Pluma Comics'
extClass = '.PlumaComics' extClass = '.PlumaComics'
themePkg = 'madara' themePkg = 'yuyu'
baseUrl = 'https://plumacomics.cloud' baseUrl = 'https://new.plumacomics.cloud'
overrideVersionCode = 0 overrideVersionCode = 41
isNsfw = false isNsfw = false
} }

View File

@ -1,18 +1,17 @@
package eu.kanade.tachiyomi.extension.pt.plumacomics package eu.kanade.tachiyomi.extension.pt.plumacomics
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import okhttp3.Response import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.text.SimpleDateFormat
import java.util.Locale
class PlumaComics : Madara( class PlumaComics : YuYu(
"Pluma Comics", "Pluma Comics",
"https://plumacomics.cloud", "https://new.plumacomics.cloud",
"pt-BR", "pt-BR",
SimpleDateFormat("dd 'de' MMM 'de' yyyy", Locale("pt", "BR")),
) { ) {
override val useNewChapterEndpoint = true override val client = super.client.newBuilder()
.rateLimit(2)
.build()
override fun chapterListParse(response: Response) = // Moved from Madara to YuYu
super.chapterListParse(response).reversed() override val versionId = 3
} }

View File

@ -1,9 +1,9 @@
ext { ext {
extName = 'Spectral Scan' extName = 'Spectral Scan'
extClass = '.SpectralScan' extClass = '.SpectralScan'
themePkg = 'madara' themePkg = 'yuyu'
baseUrl = 'https://spectralscan.xyz' baseUrl = 'https://spectralscan.xyz'
overrideVersionCode = 0 overrideVersionCode = 41
isNsfw = false isNsfw = false
} }

View File

@ -1,16 +1,17 @@
package eu.kanade.tachiyomi.extension.pt.spectralscan package eu.kanade.tachiyomi.extension.pt.spectralscan
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import java.text.SimpleDateFormat import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.util.Locale
class SpectralScan : Madara( class SpectralScan : YuYu(
"Spectral Scan", "Spectral Scan",
"https://spectralscan.xyz", "https://spectralscan.xyz",
"pt-BR", "pt-BR",
dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale("pt", "BR")),
) { ) {
override val useLoadMoreRequest = LoadMoreStrategy.Never override val client = super.client.newBuilder()
override val useNewChapterEndpoint = true .rateLimit(2)
override val mangaDetailsSelectorStatus = "div.post-content_item:contains(Estado) > div.summary-content" .build()
// Moved from Madara to YuYu
override val versionId = 2
} }

View File

@ -1,7 +1,10 @@
ext { ext {
extName = 'Yushuke Mangas' extName = 'Yushuke Mangas'
extClass = '.YushukeMangas' extClass = '.YushukeMangas'
extVersionCode = 6 themePkg = 'yuyu'
baseUrl = 'https://new.yushukemangas.com'
overrideVersionCode = 6
isNsfw = false
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,325 +1,17 @@
package eu.kanade.tachiyomi.extension.pt.yushukemangas package eu.kanade.tachiyomi.extension.pt.yushukemangas
import android.net.Uri import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
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.HttpUrl.Companion.toHttpUrlOrNull
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
import java.net.URLEncoder
class YushukeMangas : ParsedHttpSource() { class YushukeMangas : YuYu(
"Yushuke Mangas",
"https://new.yushukemangas.com",
"pt-BR",
) {
override val name = "Yushuke Mangas" override val client = super.client.newBuilder()
override val baseUrl = "https://new.yushukemangas.com"
override val lang = "pt-BR"
override val supportsLatest = true
private var nextHash: String? = null
override val versionId = 2
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1, 2) .rateLimit(1, 2)
.build() .build()
private val json: Json by injectLazy() override val versionId = 2
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = ".top10-section .top10-item a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.absUrl("href"))
}
override fun popularMangaNextPageSelector() = null
// ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.build()
return GET(url, headers)
}
override fun latestUpdatesSelector() = ".manga-list .manga-card"
override fun latestUpdatesNextPageSelector() = "a.page-link:contains(>)"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
val url = element.selectFirst("a.manga-cover")!!.absUrl("href")
val uri = Uri.parse(url)
val pathSegments = uri.pathSegments
val lastSegment = URLEncoder.encode(pathSegments.last(), "UTF-8")
val encodedUrl = uri.buildUpon()
.path(pathSegments.dropLast(1).joinToString("/") + "/$lastSegment")
.toString()
title = element.selectFirst("a.manga-title")!!.text()
thumbnail_url = element.selectFirst("a.manga-cover img")?.absUrl("data-src")
setUrlWithoutDomain(encodedUrl)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val nextUrl = document.selectFirst(latestUpdatesNextPageSelector())?.attr("href")
val baseNextUrl = baseUrl + nextUrl
nextHash = baseNextUrl?.toHttpUrlOrNull()?.queryParameter("pagina")
return MangasPage(mangas, !nextHash.isNullOrEmpty())
}
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlFilterBuilder = filters.fold(baseUrl.toHttpUrl().newBuilder()) { urlBuilder, filter ->
when (filter) {
is RadioFilter -> {
val selected = filter.selected()
if (selected == all) return@fold urlBuilder
urlBuilder.addQueryParameter(filter.query, selected)
}
is GenreFilter -> {
filter.state
.filter(GenreCheckBox::state)
.fold(urlBuilder) { builder, genre ->
builder.addQueryParameter(filter.query, genre.id)
}
}
else -> urlBuilder
}
}
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> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map {
val manga = mangaDetailsParse(it.asJsoup())
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
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(".search-result-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(
element.attr("onclick").let {
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
},
)
}
override fun searchMangaNextPageSelector() = null
// ============================== Manga Details =========================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val details = document.selectFirst(".manga-banner .container")!!
title = details.selectFirst("h1")!!.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() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
val capituloTexto = element.select(".capitulo-numero")
.textNodes()
.joinToString(" ") { it.text().trim() }
.split(" ")
.take(2)
.joinToString(" ")
name = capituloTexto
setUrlWithoutDomain(element.absUrl("href"))
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
return Observable.just(chapters)
}
private fun fetchChapterListPage(mangaId: String, page: Int): Response {
val url = "$baseUrl/ajax/lzmvke.php?order=DESC".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("page", page.toString())
.build()
return client
.newCall(GET(url, headers))
.execute()
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select("div.select-nav + * picture")
.mapIndexedNotNull { index, pictureElement ->
val imgElement = pictureElement.selectFirst("img")
val imageUrl = imgElement?.attr("src")?.takeIf { it.isNotBlank() } ?: return@mapIndexedNotNull null
Page(index, imageUrl = "$baseUrl$imageUrl")
}
}
override fun imageUrlParse(document: Document) = ""
// ============================== Filters =============================
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<String>,
state: Int = 0,
) : Filter.Select<String>(displayName, vals, state) {
fun selected() = vals[state]
}
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 statusList = arrayOf(
all,
"Em andamento",
"Completo",
"Cancelado",
"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 {
const val PREFIX_SEARCH = "id:"
val SEARCH_URL_REGEX = "'([^']+)".toRegex()
val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
}
} }