Add ExHentai (#4313)

* Add ExHentai

* Rename
This commit is contained in:
Chopper 2024-07-30 04:24:15 -03:00 committed by GitHub
parent 1f69a26bfe
commit 217c50fa5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 240 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.exhentainetbr.ExHentaiNetBRUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="exhentai.net.br"
android:pathPattern="/manga/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'ExHentai.net.br'
extClass = '.ExHentaiNetBR'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,173 @@
package eu.kanade.tachiyomi.extension.pt.exhentainetbr
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class ExHentaiNetBR : ParsedHttpSource() {
override val name = "ExHentai.net.br"
override val baseUrl = "https://exhentai.net.br"
override val lang = "pt-BR"
override val supportsLatest = false
override val client = network.cloudflareClient.newBuilder()
.rateLimit(3)
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/lista-de-mangas/page/$page")
override fun popularMangaSelector() = "article.itemP"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.ownText()
thumbnail_url = element.selectFirst("img")?.imgAttr()
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun popularMangaNextPageSelector() = ".content-pagination li[class='active'] + li:not([class='next'])"
override fun latestUpdatesFromElement(element: Element) =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() =
throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException()
override fun latestUpdatesSelector() =
throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
.build()
val filter = filters.filterIsInstance<AlphabetFilter>().first()
if (query.isBlank() && filter.selected() != DEFAULT_FILTER_VALUE) {
url = "$baseUrl/lista-de-mangas".toHttpUrl().newBuilder()
.addQueryParameter("letra", filter.selected())
.build()
}
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH).not()) {
return super.fetchSearchManga(page, query, filters)
}
val slug = query.substringAfter(PREFIX_SEARCH)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map { MangasPage(listOf(mangaDetailsParse(it)), false) }
}
override fun searchMangaParse(response: Response): MangasPage {
val url = response.request.url
return if (url.queryParameter("letra") != null) {
popularMangaParse(response)
} else {
super.searchMangaParse(response)
}
}
override fun searchMangaSelector() = ".post ${popularMangaSelector()}"
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst(".stats_box h3")!!.text()
description = document.selectFirst(".sinopse_manga .info_p:last-child")?.text()
thumbnail_url = document.selectFirst(".anime_cover img")?.imgAttr()
artist = document.selectFirst(".sinopse_manga h5:contains(Artista) + span")?.text()
author = artist
genre = document.select(".tag-btn").joinToString { it.ownText() }
val statusLabel = document.selectFirst(".stats_box span")?.ownText() ?: ""
status = when {
statusLabel.equals("Completo", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
setUrlWithoutDomain(document.location())
}
override fun chapterListSelector() = ".chapter_content a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".name_chapter")!!.text()
val date = element.selectFirst("span.release-date")?.ownText() ?: ""
date_upload = date.substringAfter(":").trim().toDate()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListParse(response: Response) =
super.chapterListParse(response).reversed()
override fun pageListParse(document: Document) =
document.select("div.manga_image > img").mapIndexed { index, element ->
Page(index, imageUrl = element.imgAttr())
}
override fun imageUrlParse(document: Document) = ""
override fun getFilterList(): FilterList {
val alphabet = mutableListOf(DEFAULT_FILTER_VALUE).also {
it += ('A'..'Z').map { "$it" }
}
return FilterList(
Filter.Header(
"""
Busca por título possue prioridade.
Deixe em branco para pesquisar por letra
""".trimIndent(),
),
AlphabetFilter("Alfabeto", alphabet),
)
}
private fun Element.imgAttr(): String {
return when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
else -> attr("abs:src")
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.ROOT)
const val PREFIX_SEARCH = "id:"
const val DEFAULT_FILTER_VALUE = "Padrão"
}
class AlphabetFilter(displayName: String, private val vals: List<String>, state: Int = 0) :
Filter.Select<String>(displayName, vals.toTypedArray(), state) {
fun selected() = vals[state]
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.pt.exhentainetbr
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class ExHentaiNetBRUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${ExHentaiNetBR.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}