diff --git a/src/pt/exhentainetbr/AndroidManifest.xml b/src/pt/exhentainetbr/AndroidManifest.xml new file mode 100644 index 000000000..02db30f9f --- /dev/null +++ b/src/pt/exhentainetbr/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/exhentainetbr/build.gradle b/src/pt/exhentainetbr/build.gradle new file mode 100644 index 000000000..680aaa126 --- /dev/null +++ b/src/pt/exhentainetbr/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'ExHentai.net.br' + extClass = '.ExHentaiNetBR' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/exhentainetbr/res/mipmap-hdpi/ic_launcher.png b/src/pt/exhentainetbr/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9566df779 Binary files /dev/null and b/src/pt/exhentainetbr/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/exhentainetbr/res/mipmap-mdpi/ic_launcher.png b/src/pt/exhentainetbr/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..899ad2b9a Binary files /dev/null and b/src/pt/exhentainetbr/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/exhentainetbr/res/mipmap-xhdpi/ic_launcher.png b/src/pt/exhentainetbr/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7c087f207 Binary files /dev/null and b/src/pt/exhentainetbr/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/exhentainetbr/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/exhentainetbr/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..c07789925 Binary files /dev/null and b/src/pt/exhentainetbr/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/exhentainetbr/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/exhentainetbr/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..7063d764b Binary files /dev/null and b/src/pt/exhentainetbr/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBR.kt b/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBR.kt new file mode 100644 index 000000000..10b13c2cd --- /dev/null +++ b/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBR.kt @@ -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().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 { + 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, state: Int = 0) : + Filter.Select(displayName, vals.toTypedArray(), state) { + fun selected() = vals[state] + } +} diff --git a/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBRUrlActivity.kt b/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBRUrlActivity.kt new file mode 100644 index 000000000..48d0639d8 --- /dev/null +++ b/src/pt/exhentainetbr/src/eu/kanade/tachiyomi/extension/pt/exhentainetbr/ExHentaiNetBRUrlActivity.kt @@ -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) + } +}