Multsrc: TerraScan - Add MangaBR (#3568)

* Add lib-multsrc and mangaBR

* Cleanup

* Remove extra newline

* Remove filter separator
This commit is contained in:
Chopper 2024-06-17 09:31:35 -03:00 committed by GitHub
parent 4be14b9c07
commit 6b79b5b04b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 295 additions and 244 deletions

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.mangaterra.MangaTerraUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.terrascan.TerraScanUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -13,9 +13,9 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="manga-terra.com"
android:pathPattern="/manga/..*" />
android:host="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

View File

@ -0,0 +1,245 @@
package eu.kanade.tachiyomi.multisrc.terrascan
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
abstract class TerraScan(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale("pt", "BR")),
) : ParsedHttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient
private val noRedirectClient = network.cloudflareClient.newBuilder()
.followRedirects(false)
.build()
private val json: Json by injectLazy()
private var genresList: List<Genre> = emptyList()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?q=p&page=$page", headers)
open val popularMangaTitleSelector: String = "p, h3"
open val popularMangaThumbnailSelector: String = "img"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(popularMangaTitleSelector)!!.ownText()
thumbnail_url = element.selectFirst(popularMangaThumbnailSelector)?.srcAttr()
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun popularMangaNextPageSelector() = ".pagination > .page-item:not(.disabled):last-child"
override fun popularMangaSelector(): String = ".series-paginated .grid-item-series, .series-paginated .series"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (genresList.isEmpty()) {
genresList = parseGenres(document)
}
val mangas = document.select(popularMangaSelector())
.map(::popularMangaFromElement)
return MangasPage(mangas, document.selectFirst(popularMangaNextPageSelector()) != null)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga?q=u&page=$page", headers)
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val slug = query.substringAfter(URL_SEARCH_PREFIX)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess().map { response ->
MangasPage(listOf(mangaDetailsParse(response)), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
url.addPathSegment("search")
.addQueryParameter("q", query)
return GET(url.build(), headers)
}
url.addPathSegment("manga")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
filter.state.forEach {
if (it.state) {
url.addQueryParameter(it.query, it.value)
}
}
}
else -> {}
}
}
url.addQueryParameter("page", "$page")
return GET(url.build(), headers)
}
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = null
override fun searchMangaSelector() = ".col-6.col-sm-3.col-md-3.col-lg-2.p-1"
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.pathSegments.contains("search")) {
return searchByQueryMangaParse(response)
}
return super.searchMangaParse(response)
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<out Any>>()
if (genresList.isNotEmpty()) {
filters += GenreFilter(
title = "Gêneros",
genres = genresList,
)
} else {
filters += Filter.Header("Aperte 'Redefinir' mostrar os gêneros disponíveis")
}
return FilterList(filters)
}
open val mangaDetailsContainerSelector: String = "main"
open val mangaDetailsTitleSelector: String = "h1"
open val mangaDetailsThumbnailSelector: String = "img"
open val mangaDetailsDescriptionSelector: String = "p"
open val mangaDetailsGenreSelector: String = ".card:has(h5:contains(Categorias)) a, .card:has(h5:contains(Categorias)) div"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst(mangaDetailsContainerSelector)!!) {
title = selectFirst(mangaDetailsTitleSelector)!!.text()
thumbnail_url = selectFirst(mangaDetailsThumbnailSelector)?.absUrl("href")
description = selectFirst(mangaDetailsDescriptionSelector)?.text()
genre = document.select(mangaDetailsGenreSelector)
.joinToString { it.ownText() }
}
setUrlWithoutDomain(document.location())
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
with(element.selectFirst("h5")!!) {
name = ownText()
date_upload = selectFirst("div")!!.ownText().toDate()
}
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListSelector() = ".col-chapter a"
override fun pageListParse(document: Document): List<Page> {
val mangaChapterUrl = document.location()
val maxPage = findPageCount(mangaChapterUrl)
return (1..maxPage).map { page -> Page(page - 1, "$mangaChapterUrl/$page") }
}
override fun imageUrlParse(document: Document) = document.selectFirst("main img")!!.srcAttr()
private fun searchByQueryMangaParse(response: Response): MangasPage {
val fragment = Jsoup.parseBodyFragment(
json.decodeFromString<String>(response.body.string()),
baseUrl,
)
return MangasPage(
mangas = fragment.select(searchMangaSelector()).map(::searchMangaFromElement),
hasNextPage = false,
)
}
private fun findPageCount(pageUrl: String): Int {
var lowerBound = 1
var upperBound = 100
while (lowerBound <= upperBound) {
val midpoint = lowerBound + (upperBound - lowerBound) / 2
val request = Request.Builder().apply {
url("$pageUrl/$midpoint")
headers(headers)
head()
}.build()
val response = try {
noRedirectClient.newCall(request).execute()
} catch (e: Exception) {
throw Exception("Failed to fetch $pageUrl")
}
if (response.code == 302) {
upperBound = midpoint - 1
} else {
lowerBound = midpoint + 1
}
}
return lowerBound
}
private fun Element.srcAttr(): String = when {
hasAttr("data-src") -> absUrl("data-src")
else -> absUrl("src")
}
private fun String.toDate() = try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
open val genreFilterSelector: String = "form div > div:has(input) div"
private fun parseGenres(document: Document): List<Genre> {
return document.select(genreFilterSelector)
.map { element ->
val input = element.selectFirst("input")!!
Genre(
name = element.selectFirst("label")!!.ownText(),
query = input.attr("name"),
value = input.attr("value"),
)
}
}
companion object {
const val URL_SEARCH_PREFIX = "slug:"
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.mangaterra
package eu.kanade.tachiyomi.multisrc.terrascan
import eu.kanade.tachiyomi.source.model.Filter

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.mangaterra
package eu.kanade.tachiyomi.multisrc.terrascan
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 MangaTerraUrlActivity : Activity() {
class TerraScanUrlActivity : Activity() {
private val tag = javaClass.simpleName
@ -35,5 +35,5 @@ class MangaTerraUrlActivity : Activity() {
}
private fun slug(pathSegments: List<String>) =
"${MangaTerra.PREFIX_SEARCH}${pathSegments[pathSegments.size - 1]}"
"${TerraScan.URL_SEARCH_PREFIX}${pathSegments[pathSegments.size - 1]}"
}

View File

@ -0,0 +1,11 @@
ext {
extName = 'Manga BR'
extClass = '.MangaBR'
themePkg = 'terrascan'
baseUrl = 'https://mangabr.net'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.mangabr
import eu.kanade.tachiyomi.multisrc.terrascan.TerraScan
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import java.util.concurrent.TimeUnit
class MangaBR : TerraScan(
"Manga BR",
"https://mangabr.net",
"pt-BR",
) {
override val client = super.client.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS)
.build()
}

View File

@ -1,7 +1,9 @@
ext {
extName = 'Manga Terra'
extClass = '.MangaTerra'
extVersionCode = 2
themePkg = 'terrascan'
baseUrl = 'https://manga-terra.com'
overrideVersionCode = 2
isNsfw = true
}

View File

@ -1,242 +1,15 @@
package eu.kanade.tachiyomi.extension.pt.mangaterra
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.multisrc.terrascan.TerraScan
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.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class MangaTerra : ParsedHttpSource() {
override val lang: String = "pt-BR"
override val supportsLatest: Boolean = true
override val name: String = "Manga Terra"
override val baseUrl: String = "https://manga-terra.com"
override val client = network.cloudflareClient.newBuilder()
.rateLimit(5, 2, TimeUnit.SECONDS)
class MangaTerra : TerraScan(
"Manga Terra",
"https://manga-terra.com",
"pt-BR",
) {
override val client = super.client.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS)
.build()
private val noRedirectClient = network.cloudflareClient.newBuilder()
.followRedirects(false)
.build()
private val json: Json by injectLazy()
private var fetchGenresAttempts: Int = 0
private var genresList: List<Genre> = emptyList()
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst("h5")!!.ownText()
date_upload = element.selectFirst("h5 > div")!!.ownText().toDate()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListSelector() = ".card-list-chapter a"
override fun imageUrlParse(document: Document) = document.selectFirst("img")!!.srcAttr()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga?q=u&page=$page", headers)
override fun latestUpdatesSelector() = popularMangaSelector()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst(".card-body h1")!!.ownText()
description = document.selectFirst(".card-body p")?.ownText()
thumbnail_url = document.selectFirst(".card-body img")?.srcAttr()
genre = document.select(".card-series-about a").joinToString { it.ownText() }
setUrlWithoutDomain(document.location())
}
override fun pageListParse(document: Document): List<Page> {
val mangaChapterUrl = document.location()
val maxPage = findPageCount(mangaChapterUrl)
return (1..maxPage).map { page -> Page(page - 1, "$mangaChapterUrl/$page") }
}
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("p")!!.ownText()
thumbnail_url = element.selectFirst("img")?.srcAttr()
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun popularMangaNextPageSelector() = ".pagination > .page-item:not(.disabled):last-child"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?q=p&page=$page", headers)
override fun popularMangaSelector(): String = ".card-body .row > div"
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.pathSegments.contains("search")) {
return searchByQueryMangaParse(response)
}
return super.searchMangaParse(response)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
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 { response ->
MangasPage(listOf(mangaDetailsParse(response)), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
url.addPathSegment("search")
.addQueryParameter("q", query)
return GET(url.build(), headers)
}
url.addPathSegment("manga")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
filter.state.forEach {
if (it.state) {
url.addQueryParameter(it.query, it.value)
}
}
}
else -> {}
}
}
url.addQueryParameter("page", "$page")
return GET(url.build(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun getFilterList(): FilterList {
CoroutineScope(Dispatchers.IO).launch { fetchGenres() }
val filters = mutableListOf<Filter<out Any>>()
if (genresList.isNotEmpty()) {
filters += GenreFilter(
title = "Gêneros",
genres = genresList,
)
} else {
filters += listOf(
Filter.Separator(),
Filter.Header("Aperte 'Redefinir' mostrar os gêneros disponíveis"),
)
}
return FilterList(filters)
}
private fun searchByQueryMangaParse(response: Response): MangasPage {
val fragment = Jsoup.parseBodyFragment(
json.decodeFromString<String>(response.body.string()),
baseUrl,
)
return MangasPage(
mangas = fragment.select("div.grid-item-series").map(::searchMangaFromElement),
hasNextPage = false,
)
}
private fun findPageCount(pageUrl: String): Int {
var lowerBound = 1
var upperBound = 100
while (lowerBound <= upperBound) {
val midpoint = lowerBound + (upperBound - lowerBound) / 2
val request = Request.Builder().apply {
url("$pageUrl/$midpoint")
headers(headers)
head()
}.build()
val response = try {
noRedirectClient.newCall(request).execute()
} catch (e: Exception) {
throw Exception("Failed to fetch $pageUrl")
}
if (response.code == 302) {
upperBound = midpoint - 1
} else {
lowerBound = midpoint + 1
}
}
return lowerBound
}
private fun Element.srcAttr(): String = when {
hasAttr("data-src") -> absUrl("data-src")
else -> absUrl("src")
}
private fun String.toDate() = try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
private fun fetchGenres() {
if (fetchGenresAttempts < 3 && genresList.isEmpty()) {
try {
genresList = client.newCall(GET("$baseUrl/manga")).execute()
.use { parseGenres(it.asJsoup()) }
} catch (_: Exception) {
} finally {
fetchGenresAttempts++
}
}
}
private fun parseGenres(document: Document): List<Genre> {
return document.select(".form-filters .custom-checkbox")
.map { element ->
val input = element.selectFirst("input")!!
Genre(
name = element.selectFirst("label")!!.ownText(),
query = input.attr("name"),
value = input.attr("value"),
)
}
}
companion object {
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale("pt", "BR"))
const val PREFIX_SEARCH = "slug:"
}
}