Add YuYu multsrc (#8080)
@ -2,7 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".pt.yushukemangas.YushukeMangasUrlActivity"
|
||||
android:name="eu.kanade.tachiyomi.multisrc.yuyu.YuYuUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
@ -11,11 +11,10 @@
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="new.yushukemangas.com"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
5
lib-multisrc/yuyu/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
206
lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.yushukemangas
|
||||
package eu.kanade.tachiyomi.multisrc.yuyu
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
@ -7,7 +7,7 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class YushukeMangasUrlActivity : Activity() {
|
||||
class YuYuUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
@ -17,7 +17,7 @@ class YushukeMangasUrlActivity : Activity() {
|
||||
if (pathSegment != null && pathSegment.size > 1) {
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${YushukeMangas.PREFIX_SEARCH}${pathSegment[1]}")
|
||||
putExtra("query", "${YuYu.PREFIX_SEARCH}${pathSegment[1]}")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
10
src/pt/egotoons/build.gradle
Normal 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"
|
BIN
src/pt/egotoons/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/pt/egotoons/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src/pt/egotoons/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/pt/egotoons/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/pt/egotoons/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 26 KiB |
@ -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()
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
ext {
|
||||
extName = 'Galinha Samurai Scan'
|
||||
extClass = '.GalinhaSamuraiScan'
|
||||
themePkg = 'madara'
|
||||
themePkg = 'yuyu'
|
||||
baseUrl = 'https://galinhasamurai.com'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 41
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,17 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.galinhasamuraiscan
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
|
||||
class GalinhaSamuraiScan : Madara(
|
||||
class GalinhaSamuraiScan : YuYu(
|
||||
"Galinha Samurai Scan",
|
||||
"https://galinhasamurai.com",
|
||||
"pt-BR",
|
||||
dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR")),
|
||||
) {
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||
override val useNewChapterEndpoint = true
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
// Moved from Madara to YuYu
|
||||
override val versionId = 2
|
||||
}
|
||||
|
10
src/pt/nekotoons/build.gradle
Normal 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"
|
BIN
src/pt/nekotoons/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/pt/nekotoons/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/pt/nekotoons/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/pt/nekotoons/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
src/pt/nekotoons/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -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()
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
ext {
|
||||
extName = 'Pluma Comics'
|
||||
extClass = '.PlumaComics'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://plumacomics.cloud'
|
||||
overrideVersionCode = 0
|
||||
themePkg = 'yuyu'
|
||||
baseUrl = 'https://new.plumacomics.cloud'
|
||||
overrideVersionCode = 41
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,17 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.plumacomics
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import okhttp3.Response
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
|
||||
class PlumaComics : Madara(
|
||||
class PlumaComics : YuYu(
|
||||
"Pluma Comics",
|
||||
"https://plumacomics.cloud",
|
||||
"https://new.plumacomics.cloud",
|
||||
"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) =
|
||||
super.chapterListParse(response).reversed()
|
||||
// Moved from Madara to YuYu
|
||||
override val versionId = 3
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
ext {
|
||||
extName = 'Spectral Scan'
|
||||
extClass = '.SpectralScan'
|
||||
themePkg = 'madara'
|
||||
themePkg = 'yuyu'
|
||||
baseUrl = 'https://spectralscan.xyz'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 41
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,17 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.spectralscan
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
|
||||
class SpectralScan : Madara(
|
||||
class SpectralScan : YuYu(
|
||||
"Spectral Scan",
|
||||
"https://spectralscan.xyz",
|
||||
"pt-BR",
|
||||
dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale("pt", "BR")),
|
||||
) {
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
||||
override val useNewChapterEndpoint = true
|
||||
override val mangaDetailsSelectorStatus = "div.post-content_item:contains(Estado) > div.summary-content"
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
// Moved from Madara to YuYu
|
||||
override val versionId = 2
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
ext {
|
||||
extName = 'Yushuke Mangas'
|
||||
extClass = '.YushukeMangas'
|
||||
extVersionCode = 6
|
||||
themePkg = 'yuyu'
|
||||
baseUrl = 'https://new.yushukemangas.com'
|
||||
overrideVersionCode = 6
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,325 +1,17 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.yushukemangas
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
|
||||
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 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()
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(1, 2)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// ============================== 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()
|
||||
}
|
||||
override val versionId = 2
|
||||
}
|
||||
|