diff --git a/src/pt/yushukemangas/AndroidManifest.xml b/lib-multisrc/yuyu/AndroidManifest.xml similarity index 73% rename from src/pt/yushukemangas/AndroidManifest.xml rename to lib-multisrc/yuyu/AndroidManifest.xml index eee4b4423..fd06f4b58 100644 --- a/src/pt/yushukemangas/AndroidManifest.xml +++ b/lib-multisrc/yuyu/AndroidManifest.xml @@ -2,7 +2,7 @@ @@ -11,11 +11,10 @@ - + android:host="${SOURCEHOST}" + android:pathPattern="/..*" + android:scheme="${SOURCESCHEME}" /> diff --git a/lib-multisrc/yuyu/build.gradle.kts b/lib-multisrc/yuyu/build.gradle.kts new file mode 100644 index 000000000..dc076cc37 --- /dev/null +++ b/lib-multisrc/yuyu/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("lib-multisrc") +} + +baseVersionCode = 1 diff --git a/lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt b/lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt new file mode 100644 index 000000000..1e4c24c49 --- /dev/null +++ b/lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYu.kt @@ -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 { + 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> { + val mangaId = manga.fetchMangaId() + val chapters = mutableListOf() + var page = 1 + do { + val dto = fetchChapterListPage(mangaId, page++).parseAs() + 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 { + 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() + } +} diff --git a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt b/lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYuUrlActivity.kt similarity index 83% rename from src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt rename to lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYuUrlActivity.kt index 20d6940cf..a9025a243 100644 --- a/src/pt/yushukemangas/src/eu/kanade/tachiyomi/extension/pt/yushukemangas/YushukeMangasUrlActivity.kt +++ b/lib-multisrc/yuyu/src/eu/kanade/tachiyomi/multisrc/yuyu/YuYuUrlActivity.kt @@ -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) } diff --git a/src/pt/egotoons/build.gradle b/src/pt/egotoons/build.gradle new file mode 100644 index 000000000..003ce072e --- /dev/null +++ b/src/pt/egotoons/build.gradle @@ -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" diff --git a/src/pt/egotoons/res/mipmap-hdpi/ic_launcher.png b/src/pt/egotoons/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..7f9136bb7 Binary files /dev/null and b/src/pt/egotoons/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/egotoons/res/mipmap-mdpi/ic_launcher.png b/src/pt/egotoons/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e8d2cc62f Binary files /dev/null and b/src/pt/egotoons/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/egotoons/res/mipmap-xhdpi/ic_launcher.png b/src/pt/egotoons/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..9c68b9ebf Binary files /dev/null and b/src/pt/egotoons/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/egotoons/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/egotoons/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..8693728bb Binary files /dev/null and b/src/pt/egotoons/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/egotoons/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/egotoons/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5ed21812f Binary files /dev/null and b/src/pt/egotoons/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/egotoons/src/eu/kanade/tachiyomi/extension/pt/egotoons/EgoToons.kt b/src/pt/egotoons/src/eu/kanade/tachiyomi/extension/pt/egotoons/EgoToons.kt new file mode 100644 index 000000000..b86d00afb --- /dev/null +++ b/src/pt/egotoons/src/eu/kanade/tachiyomi/extension/pt/egotoons/EgoToons.kt @@ -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() +} diff --git a/src/pt/galinhasamuraiscan/build.gradle b/src/pt/galinhasamuraiscan/build.gradle index f582070e1..2d35a3be5 100644 --- a/src/pt/galinhasamuraiscan/build.gradle +++ b/src/pt/galinhasamuraiscan/build.gradle @@ -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 } diff --git a/src/pt/galinhasamuraiscan/src/eu/kanade/tachiyomi/extension/pt/galinhasamuraiscan/GalinhaSamuraiScan.kt b/src/pt/galinhasamuraiscan/src/eu/kanade/tachiyomi/extension/pt/galinhasamuraiscan/GalinhaSamuraiScan.kt index 3af3bcb1d..8189b658b 100644 --- a/src/pt/galinhasamuraiscan/src/eu/kanade/tachiyomi/extension/pt/galinhasamuraiscan/GalinhaSamuraiScan.kt +++ b/src/pt/galinhasamuraiscan/src/eu/kanade/tachiyomi/extension/pt/galinhasamuraiscan/GalinhaSamuraiScan.kt @@ -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 } diff --git a/src/pt/nekotoons/build.gradle b/src/pt/nekotoons/build.gradle new file mode 100644 index 000000000..521c2723b --- /dev/null +++ b/src/pt/nekotoons/build.gradle @@ -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" diff --git a/src/pt/nekotoons/res/mipmap-hdpi/ic_launcher.png b/src/pt/nekotoons/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..3a286bbff Binary files /dev/null and b/src/pt/nekotoons/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/nekotoons/res/mipmap-mdpi/ic_launcher.png b/src/pt/nekotoons/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..11b9e0e69 Binary files /dev/null and b/src/pt/nekotoons/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/nekotoons/res/mipmap-xhdpi/ic_launcher.png b/src/pt/nekotoons/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..548e13c5d Binary files /dev/null and b/src/pt/nekotoons/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/nekotoons/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/nekotoons/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e2860733f Binary files /dev/null and b/src/pt/nekotoons/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/nekotoons/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/nekotoons/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..02354af6e Binary files /dev/null and b/src/pt/nekotoons/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/nekotoons/src/eu/kanade/tachiyomi/extension/pt/nekotoons/NekoToons.kt b/src/pt/nekotoons/src/eu/kanade/tachiyomi/extension/pt/nekotoons/NekoToons.kt new file mode 100644 index 000000000..f6d64c58f --- /dev/null +++ b/src/pt/nekotoons/src/eu/kanade/tachiyomi/extension/pt/nekotoons/NekoToons.kt @@ -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() +} diff --git a/src/pt/plumacomics/build.gradle b/src/pt/plumacomics/build.gradle index 7777c6885..fb1cae056 100644 --- a/src/pt/plumacomics/build.gradle +++ b/src/pt/plumacomics/build.gradle @@ -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 } diff --git a/src/pt/plumacomics/src/eu/kanade/tachiyomi/extension/pt/plumacomics/PlumaComics.kt b/src/pt/plumacomics/src/eu/kanade/tachiyomi/extension/pt/plumacomics/PlumaComics.kt index 24cea3448..3d255bd48 100644 --- a/src/pt/plumacomics/src/eu/kanade/tachiyomi/extension/pt/plumacomics/PlumaComics.kt +++ b/src/pt/plumacomics/src/eu/kanade/tachiyomi/extension/pt/plumacomics/PlumaComics.kt @@ -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 } diff --git a/src/pt/spectralscan/build.gradle b/src/pt/spectralscan/build.gradle index 064146b8b..2b1f16de4 100644 --- a/src/pt/spectralscan/build.gradle +++ b/src/pt/spectralscan/build.gradle @@ -1,9 +1,9 @@ ext { extName = 'Spectral Scan' extClass = '.SpectralScan' - themePkg = 'madara' + themePkg = 'yuyu' baseUrl = 'https://spectralscan.xyz' - overrideVersionCode = 0 + overrideVersionCode = 41 isNsfw = false } diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/SpectralScan.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/SpectralScan.kt index 35a5b97e8..451487ba9 100644 --- a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/SpectralScan.kt +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/SpectralScan.kt @@ -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 } diff --git a/src/pt/yushukemangas/build.gradle b/src/pt/yushukemangas/build.gradle index 45e2d8769..415561c46 100644 --- a/src/pt/yushukemangas/build.gradle +++ b/src/pt/yushukemangas/build.gradle @@ -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" 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 46bc13cd1..f1582e5ef 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 @@ -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 { - 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> { - val mangaId = manga.fetchMangaId() - val chapters = mutableListOf() - var page = 1 - do { - val dto = fetchChapterListPage(mangaId, page++).parseAs() - 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 { - 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, - state: Int = 0, - ) : Filter.Select(displayName, vals, state) { - fun selected() = vals[state] - } - - 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 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() - } + override val versionId = 2 }