Add YuYu multsrc (#8080)
@ -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>
|
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.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)
|
||||||
}
|
}
|
||||||
|
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|