diff --git a/src/pt/hentaistube/AndroidManifest.xml b/src/pt/hentaistube/AndroidManifest.xml
new file mode 100644
index 000000000..c4460b119
--- /dev/null
+++ b/src/pt/hentaistube/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pt/hentaistube/build.gradle b/src/pt/hentaistube/build.gradle
new file mode 100644
index 000000000..1a9cf1bbc
--- /dev/null
+++ b/src/pt/hentaistube/build.gradle
@@ -0,0 +1,15 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+ext {
+ extName = 'HentaisTube'
+ pkgNameSuffix = 'pt.hentaistube'
+ extClass = '.HentaisTube'
+ extVersionCode = 1
+ containsNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/hentaistube/res/mipmap-hdpi/ic_launcher.png b/src/pt/hentaistube/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..8fa8bd8eb
Binary files /dev/null and b/src/pt/hentaistube/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/hentaistube/res/mipmap-mdpi/ic_launcher.png b/src/pt/hentaistube/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..35131ef26
Binary files /dev/null and b/src/pt/hentaistube/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/hentaistube/res/mipmap-xhdpi/ic_launcher.png b/src/pt/hentaistube/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bc65721fe
Binary files /dev/null and b/src/pt/hentaistube/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaistube/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/hentaistube/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..befc75e83
Binary files /dev/null and b/src/pt/hentaistube/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaistube/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/hentaistube/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..019e26ea1
Binary files /dev/null and b/src/pt/hentaistube/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/hentaistube/src/eu/kanade/tachiyomi/animeextension/pt/hentaistube/HentaisTube.kt b/src/pt/hentaistube/src/eu/kanade/tachiyomi/animeextension/pt/hentaistube/HentaisTube.kt
new file mode 100644
index 000000000..1fb94638f
--- /dev/null
+++ b/src/pt/hentaistube/src/eu/kanade/tachiyomi/animeextension/pt/hentaistube/HentaisTube.kt
@@ -0,0 +1,254 @@
+package eu.kanade.tachiyomi.animeextension.pt.hentaistube
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.animeextension.pt.hentaistube.HentaisTubeFilters.applyFilterParams
+import eu.kanade.tachiyomi.animeextension.pt.hentaistube.dto.ItemsListDto
+import eu.kanade.tachiyomi.animeextension.pt.hentaistube.extractors.BloggerExtractor
+import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+import eu.kanade.tachiyomi.animesource.model.AnimesPage
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+
+class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
+
+ override val name = "HentaisTube"
+
+ override val baseUrl = "https://www.hentaistube.com"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", baseUrl)
+ .add("Origin", baseUrl)
+
+ private val json: Json by injectLazy()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+
+ // ============================== Popular ===============================
+ override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ranking-hentais?paginacao=$page", headers)
+
+ override fun popularAnimeSelector() = "ul.ul_sidebar > li"
+
+ override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
+ thumbnail_url = element.selectFirst("img")!!.attr("src")
+ element.selectFirst("div.rt a.series")!!.also {
+ setUrlWithoutDomain(it.attr("href"))
+ title = it.text().substringBefore(" - Episódios")
+ }
+ }
+
+ override fun popularAnimeNextPageSelector() = "div.paginacao > a:contains(»)"
+
+ // =============================== Latest ===============================
+ override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page/", headers)
+
+ override fun latestUpdatesSelector() = "div.epiContainer:first-child div.epiItem > a"
+
+ override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
+ setUrlWithoutDomain(element.attr("href").substringBeforeLast("-") + "s")
+ title = element.attr("title")
+ thumbnail_url = element.selectFirst("img")!!.attr("src")
+ }
+
+ override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
+
+ // =============================== Search ===============================
+ private val animeList by lazy {
+ val headers = headersBuilder().add("X-Requested-With", "XMLHttpRequest").build()
+ client.newCall(GET("$baseUrl/json-lista-capas.php", headers)).execute()
+ .use { it.body.string() }
+ .let { json.decodeFromString(it) }
+ .items
+ .asSequence()
+ }
+
+ override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable {
+ return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
+ val id = query.removePrefix(PREFIX_SEARCH)
+ client.newCall(GET("$baseUrl/$id"))
+ .asObservableSuccess()
+ .map(::searchAnimeByIdParse)
+ } else {
+ val params = HentaisTubeFilters.getSearchParameters(filters).apply {
+ animeName = query
+ }
+ val filtered = animeList.applyFilterParams(params)
+ val results = filtered.chunked(30).toList()
+ val hasNextPage = results.size > page
+ val currentPage = if (results.size == 0) {
+ emptyList()
+ } else {
+ results.get(page - 1).map {
+ SAnime.create().apply {
+ title = it.title.substringBefore("- Episódios")
+ url = "/" + it.url
+ thumbnail_url = it.thumbnail
+ }
+ }
+ }
+ Observable.just(AnimesPage(currentPage, hasNextPage))
+ }
+ }
+
+ override fun getFilterList(): AnimeFilterList = HentaisTubeFilters.FILTER_LIST
+
+ private fun searchAnimeByIdParse(response: Response): AnimesPage {
+ val details = animeDetailsParse(response.asJsoup())
+ return AnimesPage(listOf(details), false)
+ }
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ throw UnsupportedOperationException("Not used.")
+ }
+
+ override fun searchAnimeSelector(): String {
+ throw UnsupportedOperationException("Not used.")
+ }
+
+ override fun searchAnimeFromElement(element: Element): SAnime {
+ throw UnsupportedOperationException("Not used.")
+ }
+
+ override fun searchAnimeNextPageSelector(): String? {
+ throw UnsupportedOperationException("Not used.")
+ }
+
+ // =========================== Anime Details ============================
+ override fun animeDetailsParse(document: Document) = SAnime.create().apply {
+ setUrlWithoutDomain(document.location())
+ val infos = document.selectFirst("div#anime")!!
+ thumbnail_url = infos.selectFirst("img")!!.attr("src")
+ title = infos.getInfo("Hentai:")
+ genre = infos.getInfo("Tags")
+ artist = infos.getInfo("Estúdio")
+ description = infos.selectFirst("div#sinopse2")?.text().orEmpty()
+ }
+
+ // ============================== Episodes ==============================
+ override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
+
+ override fun episodeListSelector() = "div.pagAniListaContainer > li > a"
+
+ override fun episodeFromElement(element: Element) = SEpisode.create().apply {
+ setUrlWithoutDomain(element.attr("href"))
+ name = element.text()
+ episode_number = element.text().substringAfter(" ").toFloatOrNull() ?: 1F
+ }
+
+ // ============================ Video Links =============================
+ override fun videoListParse(response: Response): List