Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 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.hentaistube.HentaisTubeUrlActivity"
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="www.hentaistube.com"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,12 @@
ext {
extName = 'HentaisTube'
extClass = '.HentaisTube'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:blogger-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,234 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaistube
import android.app.Application
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.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.lib.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
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 preferences by lazy {
Injekt.get<Application>().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")!!.run {
setUrlWithoutDomain(attr("href"))
title = 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()
.parseAs<ItemsListDto>().items
.asSequence()
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$id"))
.awaitSuccess()
.use(::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<SAnime>()
} else {
results.get(page - 1).map {
SAnime.create().apply {
title = it.title.substringBefore("- Episódios")
url = "/" + it.url
thumbnail_url = it.thumbnail
}
}
}
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()
}
override fun searchAnimeSelector(): String {
throw UnsupportedOperationException()
}
override fun searchAnimeFromElement(element: Element): SAnime {
throw UnsupportedOperationException()
}
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== 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() = "ul.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<Video> {
return response.asJsoup().select(videoListSelector())
.parallelCatchingFlatMapBlocking {
client.newCall(GET(it.attr("src"), headers)).await().let { res ->
extractVideosFromIframe(res.asJsoup())
}
}
}
private val bloggerExtractor by lazy { BloggerExtractor(client) }
private fun extractVideosFromIframe(iframe: Document): List<Video> {
val url = iframe.location()
return when {
url.contains("/hd.php") -> {
val video = iframe.selectFirst("video > source")!!
val videoUrl = video.attr("src")
val quality = video.attr("label").ifEmpty { "Unknown" }
listOf(Video(videoUrl, "Principal - $quality", videoUrl, headers))
}
url.contains("/index.php") -> {
val bloggerUrl = iframe.selectFirst("iframe")!!.attr("src")
bloggerExtractor.videosFromUrl(bloggerUrl, headers)
}
url.contains("/player.php") -> {
val ahref = iframe.selectFirst("a")!!.attr("href")
val internal = client.newCall(GET(ahref, headers)).execute().asJsoup()
val videoUrl = internal.selectFirst("video > source")!!.attr("src")
listOf(Video(videoUrl, "Alternativo", videoUrl, headers))
}
else -> emptyList()
}
}
override fun videoListSelector() = "iframe.meu-player"
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun Element.getInfo(key: String): String =
select("div.boxAnimeSobreLinha:has(b:contains($key)) > a")
.eachText()
.joinToString()
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "720p")
}
}

View file

@ -0,0 +1,349 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaistube
import eu.kanade.tachiyomi.animeextension.pt.hentaistube.dto.SearchItemDto
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.TriState
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object HentaisTubeFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class TriStateFilterList(name: String, val vals: Array<String>) :
AnimeFilter.Group<TriState>(name, vals.map(::TriStateVal))
private class TriStateVal(name: String) : TriState(name)
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(): List<List<String>> {
return (getFirst<R>() as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to filter.name }
.groupBy { it.first } // group by state
.let { dict ->
val included = dict.get(TriState.STATE_INCLUDE)?.map { it.second }.orEmpty()
val excluded = dict.get(TriState.STATE_EXCLUDE)?.map { it.second }.orEmpty()
listOf(included, excluded)
}
}
class InitialLetterFilter : QueryPartFilter("Primeira letra", HentaisTubeFiltersData.INITIAL_LETTER)
class SortFilter : AnimeFilter.Sort(
"Ordem",
arrayOf("Alfabética"),
Selection(0, true),
)
class GenresFilter : TriStateFilterList("Gêneros", HentaisTubeFiltersData.GENRES)
class StudiosFilter : TriStateFilterList("Estúdios", HentaisTubeFiltersData.STUDIOS)
val FILTER_LIST get() = AnimeFilterList(
InitialLetterFilter(),
SortFilter(),
AnimeFilter.Separator(),
GenresFilter(),
StudiosFilter(),
)
data class FilterSearchParams(
val initialLetter: String = "",
val orderAscending: Boolean = true,
val blackListedGenres: List<String> = emptyList(),
val includedGenres: List<String> = emptyList(),
val blackListedStudios: List<String> = emptyList(),
val includedStudios: List<String> = emptyList(),
var animeName: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val isAscending = filters.getFirst<SortFilter>().state?.ascending ?: false
val (includedGenres, excludedGenres) = filters.parseTriFilter<GenresFilter>()
val (includedStudios, excludedStudios) = filters.parseTriFilter<StudiosFilter>()
return FilterSearchParams(
initialLetter = filters.asQueryPart<InitialLetterFilter>(),
orderAscending = isAscending,
blackListedGenres = excludedGenres,
includedGenres = includedGenres,
blackListedStudios = excludedStudios,
includedStudios = includedStudios,
)
}
private fun mustRemove(anime: SearchItemDto, params: FilterSearchParams): Boolean {
return when {
params.animeName != "" && !anime.title.contains(params.animeName, true) -> true
params.initialLetter != "" && !anime.title.lowercase().startsWith(params.initialLetter) -> true
params.blackListedGenres.size > 0 && params.blackListedGenres.any {
anime.tags.contains(it, true)
} -> true
params.includedGenres.size > 0 && params.includedGenres.any {
!anime.tags.contains(it, true)
} -> true
params.blackListedStudios.size > 0 && params.blackListedStudios.any {
anime.studios.contains(it, true)
} -> true
params.includedStudios.size > 0 && params.includedStudios.any {
!anime.studios.contains(it, true)
} -> true
else -> false
}
}
private inline fun <T, R : Comparable<R>> Sequence<T>.sortedByIf(
isAscending: Boolean,
crossinline selector: (T) -> R,
): Sequence<T> {
return when {
isAscending -> sortedBy(selector)
else -> sortedByDescending(selector)
}
}
fun Sequence<SearchItemDto>.applyFilterParams(params: FilterSearchParams): Sequence<SearchItemDto> {
return filterNot { mustRemove(it, params) }
.sortedByIf(params.orderAscending) { it.title.lowercase() }
}
private object HentaisTubeFiltersData {
val INITIAL_LETTER = arrayOf(Pair("Selecione", "")) + ('A'..'Z').map {
Pair(it.toString(), it.toString().lowercase())
}.toTypedArray()
val GENRES = arrayOf(
"Anal",
"Aventura",
"Boquete",
"Brinquedos",
"Comédia",
"Dark Skin",
"Demônios",
"Ecchi",
"Elfos",
"Empregada",
"Enfermeira",
"Esporte",
"Estupro",
"Ficção",
"Futanari",
"Gay",
"Harém",
"Hospital",
"Incesto",
"Lactante",
"Lolicon",
"Magia",
"Masturbação",
"Milf",
"Mistério",
"Monstros",
"Médico",
"Netorare",
"Ninjas",
"Orgia",
"Peitões",
"Policial",
"Professora",
"Romance",
"Shotacon",
"Submissão",
"Super Poderes",
"Tentáculos",
"Terror",
"Tetas",
"Travesti",
"Vampiros",
"Vida Escolar",
"Virgem",
"Yaoi",
"Yuri",
)
val STUDIOS = arrayOf(
"3D Pix",
"A1",
"AIC",
"APPP",
"AT2",
"Actas",
"Active",
"Agent 21",
"Alice Soft",
"Amam",
"Amumo",
"Angelfish",
"AniMan",
"Animac",
"Animate Film",
"Anime Antenna Iinkai",
"Animopron",
"Antechinus",
"Arms",
"Asahi Production",
"BOMB! CUTE! BOMB!",
"BOOTLEG",
"Blue Cat",
"Blue Eyes",
"Bootleg",
"BreakBottle",
"Bunnywalker",
"CLOCKUP",
"Central Park Media",
"Chaos Project",
"Cherry Lips",
"ChiChinoya",
"Chippai",
"Chocolat",
"ChuChu",
"Cinema Paradise",
"Circle Tribute",
"Collaboration Works",
"Comic Media",
"Cosmo",
"Cotton Doll",
"Cranberry",
"Crimson",
"D3",
"Daiei",
"Deep Forest",
"Digital Works",
"Discovery",
"Dream Force",
"Dreamroom",
"EDGE",
"Easy Film",
"Echo",
"Erozuki",
"Fan",
"Fans",
"Filmlink International",
"Five Ways",
"Flavors Soft",
"Front Line",
"Frontier Works",
"Frontline",
"Game 3D",
"GeG Entertainment",
"Gold Bear",
"Goldenboy",
"Green Bunny",
"Himajin Planning",
"Hoods Entertainment",
"Horipro",
"Hot Bear",
"IMK",
"Innocent Grey",
"J.C.Staff",
"Jam",
"JapanAnime",
"KSS",
"Kadokawa Shoten",
"King Bee",
"Kitty Media",
"Knack Productions",
"Knack",
"Kusama Art",
"L.",
"Leaf",
"Lemon Heart",
"Liberty Ship",
"Lune Pictures",
"MS Pictures",
"Majin",
"Marvelous Entertainment",
"Mary Jane",
"Media Blasters",
"Media Station",
"Metro Notes",
"Milkshake",
"Milky",
"Mitsu",
"Moonrock",
"Moonstone Cherry",
"Mousou Senka",
"Museum Pictures",
"Nihikime no Dozeu",
"NuTech Digital",
"Obtain Future",
"Office Take Off",
"Orbit",
"Orc Soft",
"Original Work",
"Otodeli",
"Oz Inc",
"Oz",
"Pashmina",
"Peachpie",
"Phoenix Entertainment",
"Pix",
"Pixy",
"PoRO",
"Poly Animation",
"Poro",
"Pìnk Pineapple",
"Queen Bee",
"SELFISH",
"Sakura Purin Animation",
"Schoolzone",
"Seisei",
"Selfish",
"Seven",
"Shaft",
"Shelf",
"Shinkuukan",
"Shinyusha",
"Shouten",
"Showten",
"Silkys",
"SoftCel Pictures",
"SoftCell Pictures",
"Sonsan Kikaku",
"Speed",
"Studio 9 Maiami",
"Studio Eromatick",
"Studio Fantasia",
"Studio Jack",
"Studio Sign",
"Studio Tulip",
"Studio Unicorn",
"Sugar Boy",
"Suzuki Mirano",
"T-Rex",
"TDK Core",
"Toho Company",
"Top Marschal",
"Toranoana",
"Triple X",
"Tufos",
"Union Cho",
"Ursaite",
"Valkyria",
"White Bear",
"Works",
"YOUC",
"ZIZ Entertainment",
"ZIZ",
"Zealot",
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaistube
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.hentaistube.com/<item> intents
* and redirects them to the main Aniyomi process.
*/
class HentaisTubeUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val item = pathSegments[0]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${HentaisTube.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)
}
}

View file

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.animeextension.pt.hentaistube.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ItemsListDto(
@SerialName("encontrado")
val items: List<SearchItemDto>,
)
@Serializable
data class SearchItemDto(
@SerialName("titulo") val title: String,
@SerialName("imagem") val thumbnail: String,
@SerialName("estudio") val studios: String,
val url: String,
val tags: String,
)