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