feat(src/pt): New source: HentaisTube (#1885)
This commit is contained in:
22
src/pt/hentaistube/AndroidManifest.xml
Normal file
22
src/pt/hentaistube/AndroidManifest.xml
Normal 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>
|
15
src/pt/hentaistube/build.gradle
Normal file
15
src/pt/hentaistube/build.gradle
Normal file
@ -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"
|
BIN
src/pt/hentaistube/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/pt/hentaistube/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
src/pt/hentaistube/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/pt/hentaistube/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
src/pt/hentaistube/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/pt/hentaistube/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
BIN
src/pt/hentaistube/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/pt/hentaistube/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
src/pt/hentaistube/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/pt/hentaistube/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -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<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")!!.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<ItemsListDto>(it) }
|
||||||
|
.items
|
||||||
|
.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||||
|
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<SAnime>()
|
||||||
|
} 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<Video> {
|
||||||
|
return response.asJsoup().select(videoListSelector()).parallelMap {
|
||||||
|
runCatching {
|
||||||
|
client.newCall(GET(it.attr("src"), headers)).execute().use { res ->
|
||||||
|
extractVideosFromIframe(res.asJsoup())
|
||||||
|
}
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
}.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
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(client).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("Not used.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun videoUrlParse(document: Document): String {
|
||||||
|
throw UnsupportedOperationException("Not used.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== 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"
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(key, entry).commit()
|
||||||
|
}
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
private fun Element.getInfo(key: String): String =
|
||||||
|
select("div.boxAnimeSobreLinha:has(b:contains($key)) > a")
|
||||||
|
.eachText()
|
||||||
|
.joinToString()
|
||||||
|
|
||||||
|
private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
|
||||||
|
runBlocking {
|
||||||
|
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
val included = it.get(TriState.STATE_INCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||||
|
val excluded = it.get(TriState.STATE_EXCLUDE)?.map { it.second } ?: emptyList<String>()
|
||||||
|
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",
|
||||||
|
"Silky’s",
|
||||||
|
"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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.pt.hentaistube.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class BloggerExtractor(private val client: OkHttpClient) {
|
||||||
|
fun videosFromUrl(url: String, headers: Headers): List<Video> {
|
||||||
|
return client.newCall(GET(url, headers)).execute()
|
||||||
|
.use { it.body.string() }
|
||||||
|
.takeIf { !it.contains("errorContainer") }
|
||||||
|
.let { it ?: return emptyList() }
|
||||||
|
.substringAfter("\"streams\":[")
|
||||||
|
.substringBefore("]")
|
||||||
|
.split("},")
|
||||||
|
.map {
|
||||||
|
val videoUrl = it.substringAfter("{\"play_url\":\"").substringBefore('"')
|
||||||
|
val format = it.substringAfter("\"format_id\":").substringBefore("}")
|
||||||
|
val quality = when (format) {
|
||||||
|
"18" -> "360p"
|
||||||
|
"22" -> "720p"
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
Video(videoUrl, quality, videoUrl, headers = headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user