TioAnime improvements and VidGuardExtractor added

This commit is contained in:
imper1aldev 2024-09-14 00:06:34 -06:00
parent 336fc9d891
commit addec95219
17 changed files with 377 additions and 259 deletions

View file

@ -0,0 +1,8 @@
plugins {
id("lib-android")
}
dependencies {
implementation(project(":lib:playlist-utils"))
implementation("org.mozilla:rhino:1.7.14")
}

View file

@ -0,0 +1,104 @@
package eu.kanade.tachiyomi.lib.vidguardextractor
import android.util.Base64
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.mozilla.javascript.Context
import org.mozilla.javascript.NativeJSON
import org.mozilla.javascript.NativeObject
import org.mozilla.javascript.Scriptable
import uy.kohesive.injekt.injectLazy
class VidGuardExtractor(private val client: OkHttpClient) {
private val playlistUtils by lazy { PlaylistUtils(client) }
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String) = videosFromUrl(url) { "$prefix $it" }
fun videosFromUrl(url: String, videoNameGen: (String) -> String = { quality -> "VidGuard:$quality" }): List<Video> {
val res = client.newCall(GET(url)).execute().asJsoup()
val scriptData = res.selectFirst("script:containsData(eval)")?.data() ?: return emptyList()
val jsonStr2 = json.decodeFromString<SvgObject>(runJS2(scriptData))
val playlistUrl = sigDecode(jsonStr2.stream)
return playlistUtils.extractFromHls(playlistUrl, videoNameGen = videoNameGen)
}
private fun sigDecode(url: String): String {
val sig = url.split("sig=")[1].split("&")[0]
val t = sig.chunked(2)
.joinToString("") { (Integer.parseInt(it, 16) xor 2).toChar().toString() }
.let {
val padding = when (it.length % 4) {
2 -> "=="
3 -> "="
else -> ""
}
String(Base64.decode((it + padding).toByteArray(Charsets.UTF_8), Base64.DEFAULT))
}
.dropLast(5)
.reversed()
.toCharArray()
.apply {
for (i in indices step 2) {
if (i + 1 < size) {
this[i] = this[i + 1].also { this[i + 1] = this[i] }
}
}
}
.concatToString()
.dropLast(5)
return url.replace(sig, t)
}
private fun runJS2(hideMyHtmlContent: String): String {
var result = ""
val r = Runnable {
val rhino = Context.enter()
rhino.initSafeStandardObjects()
rhino.optimizationLevel = -1
val scope: Scriptable = rhino.initSafeStandardObjects()
scope.put("window", scope, scope)
try {
rhino.evaluateString(
scope,
hideMyHtmlContent,
"JavaScript",
1,
null,
)
val svgObject = scope.get("svg", scope)
result = if (svgObject is NativeObject) {
NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null)
.toString()
} else {
Context.toString(svgObject)
}
} catch (e: Exception) {
Log.i("Error", e.toString())
} finally {
Context.exit()
}
}
val t = Thread(ThreadGroup("A"), r, "thread_rhino", 2000000) // StackSize 2Mb: Run in a thread because rhino requires more stack size for large scripts.
t.start()
t.join()
t.interrupt()
return result
}
}
@Serializable
data class SvgObject(
val stream: String,
val hash: String,
)

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'TioanimeH' extName = 'TioanimeH'
extClass = '.TioanimeHFactory' extClass = '.TioanimeHFactory'
extVersionCode = 18 extVersionCode = 19
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
@ -10,4 +10,6 @@ dependencies {
implementation(project(':lib:yourupload-extractor')) implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:okru-extractor')) implementation(project(':lib:okru-extractor'))
implementation(project(':lib:voe-extractor')) implementation(project(':lib:voe-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation(project(':lib:vidguard-extractor'))
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,208 @@
package eu.kanade.tachiyomi.animeextension.es.tioanimeh
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilter.CheckBox
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import java.util.Calendar
object TioAnimeHFilters {
open class QueryPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart(name: String) = vals[state].second.takeIf { it.isNotEmpty() }?.let { "&$name=${vals[state].second}" } ?: run { "" }
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private fun String.changePrefix() = this.takeIf { it.startsWith("&") }?.let { this.replaceFirst("&", "?") } ?: run { this }
data class FilterSearchParams(val filter: String = "") { fun getQuery() = filter.changePrefix() }
private fun Array<Pair<String, String>>.toCheckBoxVal(): List<CheckBox> = map { CheckBoxVal(it.first, false) }
private fun String.addSuffix(): String = takeIf { it.isNotBlank() }?.let { "$it,${Calendar.getInstance().get(Calendar.YEAR)}" } ?: this
internal fun getSearchParameters(filters: AnimeFilterList, origen: TioAnimeHFiltersData.ORIGEN): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val strFilter = buildString {
when (origen) {
TioAnimeHFiltersData.ORIGEN.ANIME -> {
append(filters.parseCheckbox<TypesFilter>(TioAnimeHFiltersData.TYPES, "type"))
append(filters.parseCheckbox<AnimeGenresFilter>(TioAnimeHFiltersData.ANIME_GENRES, "genero"))
append(filters.asQueryPart<YearsFilter>("year").addSuffix())
append(filters.asQueryPart<StatesFilter>("status"))
append(filters.asQueryPart<SortFilter>("sort"))
}
else -> {
append(filters.asQueryPart<HentaiGenresFilter>("genero"))
}
}
}
return FilterSearchParams(strFilter)
}
private val ANIME_FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
TypesFilter(),
AnimeGenresFilter(),
YearsFilter(),
StatesFilter(),
SortFilter(),
)
private val HENTAI_FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
HentaiGenresFilter(),
)
fun getFilterList(origen: TioAnimeHFiltersData.ORIGEN) = when (origen) {
TioAnimeHFiltersData.ORIGEN.ANIME -> ANIME_FILTER_LIST
else -> HENTAI_FILTER_LIST
}
class TypesFilter : CheckBoxFilterList("Tipo", TioAnimeHFiltersData.TYPES.toCheckBoxVal())
class AnimeGenresFilter : CheckBoxFilterList("Género", TioAnimeHFiltersData.ANIME_GENRES.toCheckBoxVal())
class HentaiGenresFilter : QueryPartFilter("Género", TioAnimeHFiltersData.HENTAI_GENRES)
class YearsFilter : QueryPartFilter("Año", TioAnimeHFiltersData.YEARS)
class StatesFilter : QueryPartFilter("Estado", TioAnimeHFiltersData.STATES)
class SortFilter : QueryPartFilter("Orden", TioAnimeHFiltersData.SORT)
object TioAnimeHFiltersData {
val YEARS = arrayOf(Pair("Todos", "")) + (1950..Calendar.getInstance().get(Calendar.YEAR)).map { Pair("$it", "$it") }.reversed().toTypedArray()
val TYPES = arrayOf(
Pair("TV", "0"),
Pair("Película", "1"),
Pair("OVA", "2"),
Pair("Especial", "3"),
)
val ANIME_GENRES = arrayOf(
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes-marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia-ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos-de-la-vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
)
val HENTAI_GENRES = arrayOf(
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Bestialidad", "bestialidad"),
Pair("Bondage", "bondage"),
Pair("Chikan", "chikan"),
Pair("Comedia", "comedia"),
Pair("Enfermeras", "enfermeras"),
Pair("Escolar", "escolar"),
Pair("Fantasia", "fantasia"),
Pair("Futanari", "futanari"),
Pair("Gangbang", "gangbang"),
Pair("Harem", "harem"),
Pair("Incesto", "incesto"),
Pair("Vamp", "vamp"),
Pair("Maids", "maids"),
Pair("Milf", "milf"),
Pair("Netorare", "netorare"),
Pair("Masturbacion", "masturbacion"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Tentaculos", "tentaculos"),
Pair("Tetonas", "tetonas"),
Pair("Violacion", "violacion"),
Pair("Virgenes", "virgenes"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Demonios", "demonios"),
Pair("Felacion", "felacion"),
)
val STATES = arrayOf(
Pair("Finalizado", "2"),
Pair("En emisión", "1"),
Pair("Próximamente", "3"),
)
val SORT = arrayOf(
Pair("Mas Reciente", "recent"),
Pair("Menos Reciente", "-recent"),
)
enum class ORIGEN {
ANIME,
HENTAI,
}
}
}

View file

@ -4,19 +4,20 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors.VidGuardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -54,21 +55,21 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/directorio?p=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/directorio?p=$page")
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create() return SAnime.create().apply {
anime.url = element.select("article a").attr("href") url = element.select("article a").attr("href")
anime.title = element.select("article a h3").text() title = element.select("article a h3").text()
anime.thumbnail_url = baseUrl + element.select("article a div figure img").attr("src") thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
return anime }
} }
override fun popularAnimeNextPageSelector(): String = "nav ul.pagination.d-inline-flex li.page-item a.page-link" override fun popularAnimeNextPageSelector(): String = ".pagination .active ~ li:not(.disabled)"
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
val epInfoScript = document.selectFirst("script:containsData(var episodes = )")!!.data() val epInfoScript = document.selectFirst("script:containsData(var episodes = )")!!.data()
if (epInfoScript.substringAfter("episodes = [").substringBefore("];").isEmpty()) { if (epInfoScript.substringAfter("episodes = [").substringBefore("];").isEmpty()) {
return listOf<SEpisode>() return emptyList()
} }
val epNumList = epInfoScript.substringAfter("episodes = [").substringBefore("];").split(",") val epNumList = epInfoScript.substringAfter("episodes = [").substringBefore("];").split(",")
@ -84,27 +85,36 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
} }
override fun episodeListSelector() = throw UnsupportedOperationException() override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException() override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
/*--------------------------------Video extractors------------------------------------*/
private val voeExtractor by lazy { VoeExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val videoList = mutableListOf<Video>()
val serverList = document.selectFirst("script:containsData(var videos =)")!!.data().substringAfter("var videos = [[").substringBefore("]];")
.replace("\"", "").split("],[")
serverList.forEach { val serverList = document.selectFirst("script:containsData(var videos =)")
?.data()?.substringAfter("var videos = [[")?.substringBefore("]];")
?.replace("\"", "")?.split("],[") ?: return emptyList()
return serverList.parallelCatchingFlatMapBlocking {
val servers = it.split(",") val servers = it.split(",")
val serverName = servers[0] val serverName = servers[0]
val serverUrl = servers[1].replace("\\/", "/") val serverUrl = servers[1].replace("\\/", "/")
when (serverName.lowercase()) { when (serverName.lowercase()) {
"voe" -> VoeExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll) "voe" -> voeExtractor.videosFromUrl(serverUrl)
"vidguard" -> VidGuardExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll) "vidguard" -> vidGuardExtractor.videosFromUrl(serverUrl)
"okru" -> OkruExtractor(client).videosFromUrl(serverUrl).let(videoList::addAll) "okru" -> okruExtractor.videosFromUrl(serverUrl)
"yourupload" -> YourUploadExtractor(client).videoFromUrl(serverUrl, headers = headers).let(videoList::addAll) "yourupload" -> yourUploadExtractor.videoFromUrl(serverUrl, headers = headers)
"mixdrop" -> mixDropExtractor.videosFromUrl(serverUrl)
else -> emptyList()
} }
} }
return videoList
} }
override fun videoListSelector() = throw UnsupportedOperationException() override fun videoListSelector() = throw UnsupportedOperationException()
@ -126,32 +136,29 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
} }
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters val params = TioAnimeHFilters.getSearchParameters(filters, TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.HENTAI)
val genreFilter = if (filterList.isNotEmpty())filterList.find { it is GenreFilter } as GenreFilter else { GenreFilter().apply { state = 0 } }
return when { return when {
query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers) query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/directorio?genero=${genreFilter.toUriPart()}&p=$page") params.filter.isNotBlank() -> GET("$baseUrl/directorio${params.getQuery()}&p=$page")
else -> GET("$baseUrl/directorio?p=$page ") else -> popularAnimeRequest(page)
} }
} }
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
return popularAnimeFromElement(element)
}
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create() return SAnime.create().apply {
anime.title = document.select("h1.title").text() title = document.select("h1.title").text()
anime.description = document.selectFirst("p.sinopsis")!!.ownText() description = document.selectFirst("p.sinopsis")!!.ownText()
anime.genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() } genre = document.select("p.genres span.btn.btn-sm.btn-primary.rounded-pill a").joinToString { it.text() }
anime.thumbnail_url = document.select(".thumb img").attr("abs:src") thumbnail_url = document.select(".thumb img").attr("abs:src")
anime.status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text()) status = parseStatus(document.select("a.btn.btn-success.btn-block.status").text())
return anime }
} }
private fun parseStatus(statusString: String): Int { private fun parseStatus(statusString: String): Int {
@ -165,59 +172,21 @@ open class TioanimeH(override val name: String, override val baseUrl: String) :
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create() return SAnime.create().apply {
anime.title = element.select("article a h3").text() title = element.select("article a h3").text()
anime.thumbnail_url = baseUrl + element.select("article a div figure img").attr("src") thumbnail_url = baseUrl + element.select("article a div figure img").attr("src")
val slug = if (baseUrl.contains("hentai")) "/hentai/" else "/anime/" val slug = if (baseUrl.contains("hentai")) "/hentai/" else "/anime/"
val fixUrl = element.select("article a").attr("href").split("-").toTypedArray() val fixUrl = element.select("article a").attr("href").split("-").toTypedArray()
val realUrl = fixUrl.copyOf(fixUrl.size - 1).joinToString("-").replace("/ver/", slug) val realUrl = fixUrl.copyOf(fixUrl.size - 1).joinToString("-").replace("/ver/", slug)
anime.setUrlWithoutDomain(realUrl) setUrlWithoutDomain(realUrl)
return anime }
} }
override fun latestUpdatesRequest(page: Int) = GET(baseUrl) override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
override fun latestUpdatesSelector() = ".episodes li" override fun latestUpdatesSelector() = ".episodes li"
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = TioAnimeHFilters.getFilterList(TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.HENTAI)
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
class GenreFilter : UriPartFilter(
"Generos",
arrayOf(
Pair("<selecionar>", ""),
Pair("Ahegao", "ahegao"),
Pair("Anal", "anal"),
Pair("Casadas", "casadas"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Enfermeras", "enfermeras"),
Pair("Futanari", "futanari"),
Pair("Harem", "Harem"),
Pair("Gore", "gore"),
Pair("Hardcore", "hardcore"),
Pair("Incesto", "incesto"),
Pair("Juegos Sexuales", "juegos-sexuales"),
Pair("Milfs", "milf"),
Pair("Orgia", "orgia"),
Pair("Romance", "romance"),
Pair("Shota", "shota"),
Pair("Succubus", "succubus"),
Pair("Tetonas", "tetonas"),
Pair("Violacion", "violacion"),
Pair("Virgenes(como tu)", "virgenes"),
Pair("Yaoi", "Yaoi"),
Pair("Yuri", "yuri"),
),
)
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {

View file

@ -15,65 +15,16 @@ class TioanimeHFactory : AnimeSourceFactory {
class TioAnime : TioanimeH("TioAnime", "https://tioanime.com") { class TioAnime : TioanimeH("TioAnime", "https://tioanime.com") {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters val params = TioAnimeHFilters.getSearchParameters(filters, TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.ANIME)
val genreFilter = if (filterList.isNotEmpty())filterList.find { it is GenreFilter } as GenreFilter else { GenreFilter().apply { state = 0 } }
return when { return when {
query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers) query.isNotBlank() -> GET("$baseUrl/directorio?q=$query&p=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/directorio?genero=${genreFilter.toUriPart()}&p=$page") params.filter.isNotBlank() -> GET("$baseUrl/directorio${params.getQuery()}&p=$page")
else -> GET("$baseUrl/directorio?p=$page ") else -> popularAnimeRequest(page)
} }
} }
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = TioAnimeHFilters.getFilterList(TioAnimeHFilters.TioAnimeHFiltersData.ORIGEN.ANIME)
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Géneros",
arrayOf(
Pair("<Selecionar>", "all"),
Pair("Acción", "accion"),
Pair("Artes Marciales", "artes_marciales"),
Pair("Aventuras", "aventura"),
Pair("Carreras", "carreras"),
Pair("Ciencia Ficción", "ciencia_ficcion"),
Pair("Comedia", "comedia"),
Pair("Demencia", "demencia"),
Pair("Demonios", "demonios"),
Pair("Deportes", "deportes"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolares", "escolares"),
Pair("Espacial", "espacial"),
Pair("Fantasía", "fantasia"),
Pair("Harem", "harem"),
Pair("Historico", "historico"),
Pair("Infantil", "infantil"),
Pair("Josei", "josei"),
Pair("Juegos", "juegos"),
Pair("Magia", "magia"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Misterio", "misterio"),
Pair("Música", "musica"),
Pair("Parodia", "parodia"),
Pair("Policía", "policia"),
Pair("Psicológico", "psicologico"),
Pair("Recuentos de la vida", "recuentos_de_la_vida"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
Pair("Suspenso", "suspenso"),
Pair("Terror", "terror"),
Pair("Vampiros", "vampiros"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
} }
class TioHentai : TioanimeH("TioHentai", "https://tiohentai.com") class TioHentai : TioanimeH("TioHentai", "https://tiohentai.com")

View file

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.tioanimeh.extractors
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class VidGuardExtractor(private val client: OkHttpClient) {
private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch) {
var payload: String = ""
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
?.absUrl("src")
?: return emptyList()
val headers = Headers.headersOf("Referer", url)
val script = client.newCall(GET(scriptUrl, headers)).execute()
.body.string()
val sources = getSourcesFromScript(script, url)
.takeIf { it.isNotBlank() && it != "undefined" }
?: return emptyList()
return sources.substringAfter("stream:[").substringBefore("}]")
.split('{')
.drop(1)
.mapNotNull { line ->
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
.takeIf(String::isNotBlank)
?.let(::fixUrl)
?: return@mapNotNull null
Video(videoUrl, "VidGuard:$resolution", videoUrl, headers)
}
}
private fun getSourcesFromScript(script: String, url: String): String {
val latch = CountDownLatch(1)
var webView: WebView? = null
val jsinterface = JsObject(latch)
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
cacheMode = WebSettings.LOAD_NO_CACHE
}
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
view?.evaluateJavascript(script) {}
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
}
}
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
}
latch.await(5, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
return jsinterface.payload
}
private fun fixUrl(url: String): String {
val httpUrl = url.toHttpUrl()
val originalSign = httpUrl.queryParameter("sig")!!
val newSign = originalSign.chunked(2).joinToString("") {
Char(it.toInt(16) xor 2).toString()
}
.let { String(Base64.decode(it, Base64.DEFAULT)) }
.substring(5)
.chunked(2)
.reversed()
.joinToString("")
.substring(5)
return httpUrl.newBuilder()
.removeAllQueryParameters("sig")
.addQueryParameter("sig", newSign)
.build()
.toString()
}
}