Merge remote-tracking branch 'almightyhak/main'

This commit is contained in:
Dark25 2024-09-02 12:54:32 +02:00
commit a71a132370
20 changed files with 743 additions and 112 deletions

View file

@ -12,24 +12,19 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jsoup.Jsoup
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) { class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val playlistUtils by lazy { PlaylistUtils(client, headers) } private val playlistUtils by lazy { PlaylistUtils(client, headers) }
companion object { companion object {
private val REGEX_MASTER_JS by lazy { Regex("""JScript[\w+]?\s*=\s*'([^']+)""") } private val REGEX_MASTER_JS by lazy { Regex("""\s*=\s*'([^']+)""") }
private val REGEX_EVAL_KEY by lazy { Regex("""eval\(\S+\("(\S+)",\d+,"(\S+)",(\d+),(\d+),""") }
private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") } private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") }
private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") } private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") }
private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""") } private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""") }
private val REGEX_SUBS by lazy { Regex("""\[(.*?)\](https?://[^\s,]+)""") }
// matches "[language]https://...," private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
private val REGEX_SUBS by lazy { Regex("""\[(.*?)\](.*?)"?\,""") }
private const val KEY_SOURCE = "https://rowdy-avocado.github.io/multi-keys/"
} }
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> { fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
@ -42,7 +37,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList() val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
val aesJson = json.decodeFromString<CryptoInfo>(master) val aesJson = json.decodeFromString<CryptoInfo>(master)
val key = fetchKey() val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key) val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
.replace("\\n", "\n") .replace("\\n", "\n")
.replace("\\", "") .replace("\\", "")
@ -52,29 +47,10 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1) ?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
?: return emptyList() ?: return emptyList()
val subtitleList = buildList<Track> { val subtitleList = buildList {
body.takeIf { it.contains("<track kind=\"captions\"") } val subtitles = REGEX_SUBS.findAll(decryptedScript)
?.let(Jsoup::parse) subtitles.forEach {
?.select("track[kind=captions]") add(Track(it.groupValues[2], decodeUnicodeEscape(it.groupValues[1])))
?.forEach {
add(Track(it.attr("src"), it.attr("label")))
}
decryptedScript.takeIf { it.contains("subtitle:") }
?.substringAfter("subtitle: ")
?.substringBefore("\n")
?.let(REGEX_SUBS::findAll)
?.forEach { add(Track(it.groupValues[2], it.groupValues[1])) }
decryptedScript.takeIf { it.contains("tracks:") }
?.substringAfter("tracks: ")
?.substringBefore("\n")
?.also {
runCatching {
json.decodeFromString<List<TrackDto>>(it)
.filter { it.kind == "captions" }
.forEach { add(Track(it.file, it.label)) }
}
} }
} }
@ -87,25 +63,15 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
} }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
private fun fetchKey(): String { private fun fetchKey(): String? {
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.get(0) return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
} }
private fun getKey(body: String): String { private fun decodeUnicodeEscape(input: String): String {
val (encrypted, pass, offset, index) = REGEX_EVAL_KEY.find(body)!!.groupValues.drop(1) val regex = Regex("u([0-9a-fA-F]{4})")
val decrypted = decryptScript(encrypted, pass, offset.toInt(), index.toInt()) return regex.replace(input) {
return decrypted.substringAfter("'").substringBefore("'") it.groupValues[1].toInt(16).toChar().toString()
} }
private fun decryptScript(encrypted: String, pass: String, offset: Int, index: Int): String {
val trimmedPass = pass.substring(0, index)
val bits = encrypted.split(pass[index]).map { item ->
trimmedPass.foldIndexed(item) { index, acc, it ->
acc.replace(it.toString(), index.toString())
}
}.filter(String::isNotBlank)
return bits.joinToString("") { Char(it.toInt(index) - offset).toString() }
} }
@Serializable @Serializable
@ -116,16 +82,10 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
val salt: String, val salt: String,
) )
@Serializable
data class TrackDto(
val kind: String,
val label: String = "",
val file: String,
)
@Serializable @Serializable
data class KeysData( data class KeysData(
@SerialName("chillx") @SerialName("chillx")
val keys: List<String> val keys: List<String>
) )
} }
class ErrorLoadingException(message: String) : Exception(message)

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hikari' extName = 'Hikari'
extClass = '.Hikari' extClass = '.Hikari'
extVersionCode = 6 extVersionCode = 7
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -296,8 +296,39 @@ class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
Pair(iframeSrc, name) Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() } }.filter { it.first.isNotEmpty() }
} }
val embedUrls = subEmbedUrls + dubEmbedUrls
val sdEmbedUrls = html.select(".servers-sub.\\&.dub div.item.server-item").flatMap { item ->
val name = item.text().trim() + " (Sub + Dub)"
val onClick = item.selectFirst("a")?.attr("onclick")
if (onClick == null) {
Log.e("Hikari", "onClick attribute is null for item: $item")
return@flatMap emptyList<Pair<String, String>>()
}
val match = embedRegex.find(onClick)?.groupValues
if (match == null) {
Log.e("Hikari", "No match found for onClick: $onClick")
return@flatMap emptyList<Pair<String, String>>()
}
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
val iframeSrc = Jsoup.parseBodyFragment(it).selectFirst("iframe")?.attr("src")
if (iframeSrc == null) {
Log.e("Hikari", "iframe src is null for URL: $url")
return@map Pair("", "")
}
Pair(iframeSrc, name)
}.filter { it.first.isNotEmpty() }
}
val embedUrls = sdEmbedUrls.ifEmpty {
subEmbedUrls + dubEmbedUrls
}
return embedUrls.parallelCatchingFlatMapBlocking { return embedUrls.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second) getVideosFromEmbed(it.first, it.second)
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Tokuzilla' extName = 'Tokuzilla'
extClass = '.Tokuzilla' extClass = '.Tokuzilla'
extVersionCode = 16 extVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -0,0 +1,8 @@
ext {
extName = 'VeoHentai'
extClass = '.VeoHentai'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,267 @@
package eu.kanade.tachiyomi.animeextension.es.veohentai
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
open class VeoHentai : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "VeoHentai"
override val baseUrl = "https://veohentai.com"
override val lang = "es"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "VeoHentai"
private val SERVER_LIST = arrayOf("VeoHentai")
}
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
title = document.selectFirst(".pb-2 h1")?.text()?.trim() ?: ""
status = SAnime.UNKNOWN
description = document.select(".entry-content p").joinToString { it.text() }
genre = document.select(".tags a").joinToString { it.text() }
thumbnail_url = document.selectFirst("#thumbnail-post img")?.getImageUrl()
document.select(".gap-4 div").map { it.text() }.map { textContent ->
when {
"Marca" in textContent -> author = textContent.substringAfter("Marca").trim()
}
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/mas-visitados/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(".gap-6 a")
val nextPage = document.select(".nav-links a:contains(Next)").any()
val animeList = elements.map { element ->
SAnime.create().apply {
title = element.selectFirst("h2")?.text()?.trim() ?: ""
thumbnail_url = element.selectFirst("img:not([class*=cover])")?.getImageUrl()
setUrlWithoutDomain(element.attr("abs:href"))
}
}
return AnimesPage(animeList, nextPage)
}
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page", headers)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$query", headers)
genreFilter.state != 0 -> GET("$baseUrl/${genreFilter.toUriPart()}/page/$page", headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
return listOf(
SEpisode.create().apply {
episode_number = 1f
name = "Capítulo"
setUrlWithoutDomain(document.location())
},
)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val frame = document.selectFirst("iframe[webkitallowfullscreen]")
val src = frame?.attr("abs:src")?.takeIf { !it.startsWith("about") }
val dataLitespeedSrc = frame?.attr("data-litespeed-src")?.takeIf { !it.startsWith("about") }
val link = when {
src != null -> src
dataLitespeedSrc != null -> dataLitespeedSrc
else -> return emptyList()
}
val docPlayer = client.newCall(GET(link)).execute().asJsoup()
val dataId = docPlayer.selectFirst("[data-id]")?.attr("data-id") ?: return emptyList()
val host = docPlayer.location().toHttpUrl().host
val realPlayer = client.newCall(GET("https://$host$dataId")).execute().asJsoup()
val scriptPlayer = realPlayer.selectFirst("script:containsData(jwplayer.key)")?.data() ?: return emptyList()
val subs = scriptPlayer.substringAfter("tracks:").substringBefore("]").getItems().map {
it.substringAfter("file\": \"").substringAfter("file: \"").substringBefore("\"") to
it.substringAfter("label\": \"").substringAfter("label: \"").substringBefore("\"")
}.filter { (file, _) -> file.isNotEmpty() }.map { (file, label) -> Track(file, label) }
return scriptPlayer.substringAfter("sources:").substringBefore("]").getItems().map {
val file = it.substringAfter("file\": \"").substringAfter("file: \"").substringBefore("\"")
val type = when {
file.contains(".m3u") -> "HSL"
file.contains(".mp4") -> "MP4"
else -> ""
}
Video(file, "VeoHentai:$type", file, subtitleTracks = subs)
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(server, true) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Género",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("3D", "genero/3d"),
Pair("Ahegao", "genero/ahegao"),
Pair("Anal", "genero/anal"),
Pair("Bondage", "genero/bondage"),
Pair("Casadas", "genero/casadas"),
Pair("Censurado", "genero/censurado"),
Pair("Chikan", "genero/chikan"),
Pair("Corridas", "genero/corridas"),
Pair("Ecchi", "genero/ecchi"),
Pair("Enfermeras", "genero/enfermeras"),
Pair("Escolares", "genero/hentai-escolares"),
Pair("Fantasia", "genero/fantasia"),
Pair("Futanari", "genero/futanari"),
Pair("Gore", "genero/gore"),
Pair("Hardcore", "genero/hardcore"),
Pair("Harem", "genero/harem"),
Pair("Incesto", "genero/incesto"),
Pair("Josei", "genero/josei"),
Pair("Juegos Sexuales", "genero/juegos-sexuales"),
Pair("Lesbiana", "genero/lesbiana"),
Pair("Lolicon", "genero/lolicon"),
Pair("Maids", "genero/maids"),
Pair("Manga", "genero/manga"),
Pair("Masturbación", "genero/masturbacion"),
Pair("Milfs", "genero/milfs"),
Pair("Netorare", "genero/netorare"),
Pair("Ninfomania", "genero/ninfomania"),
Pair("Ninjas", "genero/ninjas"),
Pair("Orgias", "genero/orgias"),
Pair("Romance", "genero/romance"),
Pair("Sexo oral", "genero/sexo-oral"),
Pair("Shota", "genero/shota"),
Pair("Sin Censura", "genero/hentai-sin-censura"),
Pair("Softcore", "genero/softcore"),
Pair("Succubus", "genero/succubus"),
Pair("Teacher", "genero/teacher"),
Pair("Tentaculos", "genero/tentaculos"),
Pair("Tetonas", "genero/tetonas"),
Pair("Vanilla", "genero/vanilla"),
Pair("Violacion", "genero/violacion"),
Pair("Virgenes", "genero/virgenes"),
Pair("Yaoi", "genero/yaoi"),
Pair("Yuri", "genero/yuri"),
),
)
private 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
}
protected open fun org.jsoup.nodes.Element.getImageUrl(): String? {
return when {
isValidUrl("data-src") -> attr("abs:data-src")
isValidUrl("data-lazy-src") -> attr("abs:data-lazy-src")
isValidUrl("srcset") -> attr("abs:srcset").substringBefore(" ")
isValidUrl("src") -> attr("abs:src")
else -> ""
}
}
protected open fun org.jsoup.nodes.Element.isValidUrl(attrName: String): Boolean {
if (!hasAttr(attrName)) return false
return !attr(attrName).contains("data:image/")
}
private fun String.getItems(): List<String> {
val pattern = "\\{([^}]*)\\}".toRegex()
return pattern.findAll(this).map { it.groupValues[1] }.toList()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
setDefaultValue(PREF_SERVER_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)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = QUALITY_LIST
entryValues = QUALITY_LIST
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)
}
}

View file

@ -3,7 +3,7 @@ ext {
extClass = '.AnimeSAGA' extClass = '.AnimeSAGA'
themePkg = 'dooplay' themePkg = 'dooplay'
baseUrl = 'https://www.animesaga.in' baseUrl = 'https://www.animesaga.in'
overrideVersionCode = 10 overrideVersionCode = 11
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Toonitalia' extName = 'Toonitalia'
extClass = '.Toonitalia' extClass = '.Toonitalia'
extVersionCode = 22 extVersionCode = 23
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -277,11 +277,18 @@ class Toonitalia : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun bypassUprot(url: String): String? = private fun bypassUprot(url: String): String {
client.newCall(GET(url, headers)).execute() val page = client.newCall(GET(url, headers)).execute().body.string()
.asJsoup() Regex("""<a[^>]+href="([^"]+)".*Continue""").findAll(page)
.selectFirst("a:has(button.button.is-info)") .map { it.groupValues[1] }
?.attr("href") .toList()
.forEach { link ->
if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") || link.contains("https://streamtape") || link.contains("https://voe") && link != url) {
return link
}
}
return "something went wrong"
}
companion object { companion object {
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Anitube' extName = 'Anitube'
extClass = '.Anitube' extClass = '.Anitube'
extVersionCode = 15 extVersionCode = 18
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube package eu.kanade.tachiyomi.animeextension.pt.anitube
import android.app.Application import android.app.Application
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor
@ -38,7 +39,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl) .add("Referer", "$baseUrl/")
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
// ============================== Popular =============================== // ============================== Popular ===============================
@ -78,7 +79,11 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH) val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path")) client.newCall(GET("$baseUrl/$path"))
@ -97,6 +102,7 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(listOf(details), false) return AnimesPage(listOf(details), false)
} }
override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AnitubeFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@ -108,7 +114,14 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val char = params.initialChar val char = params.initialChar
when { when {
season.isNotBlank() -> "$baseUrl/temporada/$season/$year" season.isNotBlank() -> "$baseUrl/temporada/$season/$year"
genre.isNotBlank() -> "$baseUrl/genero/$genre/page/$page/${char.replace("todos", "")}" genre.isNotBlank() ->
"$baseUrl/genero/$genre/page/$page/${
char.replace(
"todos",
"",
)
}"
else -> "$baseUrl/anime/page/$page/letra/$char" else -> "$baseUrl/anime/page/$page/letra/$char"
} }
} else { } else {
@ -176,7 +189,9 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListParse(response: Response) = AnitubeExtractor.getVideoList(response, headers, client) private val extractor by lazy { AnitubeExtractor(headers, client, preferences) }
override fun videoListParse(response: Response) = extractor.getVideoList(response)
override fun videoListSelector() = throw UnsupportedOperationException() override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException() override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException() override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
@ -196,7 +211,22 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let(screen::addPreference) }.also(screen::addPreference)
// Auth Code
EditTextPreference(screen.context).apply {
key = PREF_AUTHCODE_KEY
title = "Auth Code"
setDefaultValue(PREF_AUTHCODE_DEFAULT)
summary = PREF_AUTHCODE_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_AUTHCODE_DEFAULT }
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
@ -250,6 +280,9 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private const val PREF_AUTHCODE_KEY = "authcode"
private const val PREF_AUTHCODE_SUMMARY = "Código de Autenticação"
private const val PREF_AUTHCODE_DEFAULT = ""
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida" private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "HD" private const val PREF_QUALITY_DEFAULT = "HD"

View file

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors package eu.kanade.tachiyomi.animeextension.pt.anitube.extractors
import android.content.SharedPreferences
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
@ -7,10 +9,163 @@ import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
object AnitubeExtractor { class AnitubeExtractor(
fun getVideoList(response: Response, headers: Headers, client: OkHttpClient): List<Video> { private val headers: Headers,
private val client: OkHttpClient,
private val preferences: SharedPreferences,
) {
private val tag by lazy { javaClass.simpleName }
private fun getAdsUrl(
serverUrl: String,
thumbUrl: String,
link: String,
linkHeaders: Headers,
): String {
val videoName = serverUrl.split('/').last()
Log.d(tag, "Accessing the link $link")
val response = client.newCall(GET(link, headers = linkHeaders)).execute()
val docLink = response.asJsoup()
val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content")
if (!refresh.isNullOrBlank()) {
val newLink = refresh.substringAfter("=")
val newHeaders = linkHeaders.newBuilder().set("Referer", link).build()
Log.d(tag, "Following link redirection to $newLink")
return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders)
}
val referer: String = docLink.location() ?: link
Log.d(tag, "Final URL: $referer")
Log.d(tag, "Fetching ADS URL")
val newHeaders = linkHeaders.newBuilder().set("Referer", referer).build()
try {
val now = System.currentTimeMillis()
val adsUrl =
client.newCall(
GET(
"$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl",
headers = newHeaders,
),
)
.execute()
.body.string()
.let {
Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""")
.find(it)?.groups?.get(1)?.value
?: ""
}
if (adsUrl.startsWith("http")) {
Log.d(tag, "ADS URL: $adsUrl")
return adsUrl
}
} catch (e: Exception) {
}
// Try default url
Log.e(tag, "Failed to get the ADS URL, trying the default")
return "https://www.popads.net/js/adblock.js"
}
private fun getAuthCode(serverUrl: String, thumbUrl: String, link: String): String {
var authCode = preferences.getString(PREF_AUTHCODE_KEY, "")!!
if (authCode.isNotBlank()) {
Log.d(tag, "AuthCode found in preferences")
val request = Request.Builder()
.head()
.url("${serverUrl}$authCode")
.headers(headers)
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful || response.code == 500) {
Log.d(tag, "AuthCode is OK")
return authCode
}
Log.d(tag, "AuthCode is invalid")
}
Log.d(tag, "Fetching new authCode")
val adsUrl = getAdsUrl(serverUrl, thumbUrl, link, headers)
val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
val body = FormBody.Builder()
.add("category", "client")
.add("type", "premium")
.add("ad", adsContent)
.build()
val newHeaders = headers.newBuilder()
.set("Referer", SITE_URL)
.add("Accept", "*/*")
.add("Cache-Control", "no-cache")
.add("Pragma", "no-cache")
.add("Connection", "keep-alive")
.add("Sec-Fetch-Dest", "empty")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Site", "same-site")
.build()
val publicidade =
client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body))
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (publicidade.isBlank()) {
Log.e(
tag,
"Failed to fetch \"publicidade\" code, the current response: $publicidade",
)
throw Exception("Por favor, abra o vídeo uma vez no navegador para liberar o IP")
}
authCode =
client.newCall(
GET(
"$ADS_URL/?token=$publicidade",
headers = newHeaders,
),
)
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (authCode.startsWith("?")) {
Log.d(tag, "Auth code fetched successfully")
preferences.edit().putString(PREF_AUTHCODE_KEY, authCode).commit()
} else {
Log.e(
tag,
"Failed to fetch auth code, the current response: $authCode",
)
}
return authCode
}
fun getVideoList(response: Response): List<Video> {
val doc = response.asJsoup() val doc = response.asJsoup()
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!! val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
@ -28,37 +183,10 @@ object AnitubeExtractor {
} }
} + listOf("appfullhd") } + listOf("appfullhd")
val videoName = serverUrl.split('/').last() val firstLink =
doc.selectFirst("div.video_container > a, div.playerContainer > a")!!.attr("href")
val adsUrl = val authCode = getAuthCode(serverUrl, thumbUrl, firstLink)
client.newCall(GET("https://www.anitube.vip/playerricas.php?name=apphd/$videoName&img=$thumbUrl&url=$serverUrl"))
.execute()
.body.string()
.substringAfter("ADS_URL")
.substringAfter('"')
.substringBefore('"')
val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
val body = FormBody.Builder()
.add("category", "client")
.add("type", "premium")
.add("ad", adsContent)
.build()
val publicidade = client.newCall(POST("https://ads.anitube.vip/", body = body))
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
val authCode = client.newCall(GET("https://ads.anitube.vip/?token=$publicidade"))
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
return qualities.mapIndexed { index, quality -> return qualities.mapIndexed { index, quality ->
val path = paths[index] val path = paths[index]
@ -66,4 +194,10 @@ object AnitubeExtractor {
Video(url, quality, url, headers = headers) Video(url, quality, url, headers = headers)
}.reversed() }.reversed()
} }
companion object {
private const val PREF_AUTHCODE_KEY = "authcode"
private const val ADS_URL = "https://ads.anitube.vip"
private const val SITE_URL = "https://www.anitube.vip"
}
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hinata Soul' extName = 'Hinata Soul'
extClass = '.HinataSoul' extClass = '.HinataSoul'
extVersionCode = 5 extVersionCode = 8
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul package eu.kanade.tachiyomi.animeextension.pt.hinatasoul
import android.app.Application import android.app.Application
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors.HinataSoulExtractor import eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors.HinataSoulExtractor
@ -162,7 +163,7 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
private val extractor by lazy { HinataSoulExtractor(headers) } private val extractor by lazy { HinataSoulExtractor(headers, client, preferences) }
override fun videoListParse(response: Response) = extractor.getVideoList(response) override fun videoListParse(response: Response) = extractor.getVideoList(response)
@ -186,6 +187,21 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.also(screen::addPreference) }.also(screen::addPreference)
// Auth Code
EditTextPreference(screen.context).apply {
key = PREF_AUTHCODE_KEY
title = "Auth Code"
setDefaultValue(PREF_AUTHCODE_DEFAULT)
summary = PREF_AUTHCODE_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_AUTHCODE_DEFAULT }
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
@ -241,6 +257,9 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
const val PREFIX_SEARCH = "slug:" const val PREFIX_SEARCH = "slug:"
private const val PREF_AUTHCODE_KEY = "authcode"
private const val PREF_AUTHCODE_SUMMARY = "Código de Autenticação"
private const val PREF_AUTHCODE_DEFAULT = ""
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida" private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "FULLHD" private const val PREF_QUALITY_DEFAULT = "FULLHD"

View file

@ -1,11 +1,169 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors package eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors
import android.content.SharedPreferences
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
class HinataSoulExtractor(private val headers: Headers) { class HinataSoulExtractor(
private val headers: Headers,
private val client: OkHttpClient,
private val preferences: SharedPreferences,
) {
private val tag by lazy { javaClass.simpleName }
private fun getAdsUrl(
serverUrl: String,
thumbUrl: String,
link: String,
linkHeaders: Headers,
): String {
val videoName = serverUrl.split('/').last()
Log.d(tag, "Accessing the link $link")
val response = client.newCall(GET(link, headers = linkHeaders)).execute()
val docLink = response.asJsoup()
val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content")
if (!refresh.isNullOrBlank()) {
val newLink = refresh.substringAfter("=")
val newHeaders = linkHeaders.newBuilder().set("Referer", link).build()
Log.d(tag, "Following link redirection to $newLink")
return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders)
}
val referer: String = docLink.location() ?: link
Log.d(tag, "Final URL: $referer")
Log.d(tag, "Fetching ADS URL")
val newHeaders = linkHeaders.newBuilder().set("Referer", referer).build()
try {
val now = System.currentTimeMillis()
val adsUrl =
client.newCall(
GET(
"$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl",
headers = newHeaders,
),
)
.execute()
.body.string()
.let {
Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""")
.find(it)?.groups?.get(1)?.value
?: ""
}
if (adsUrl.startsWith("http")) {
Log.d(tag, "ADS URL: $adsUrl")
return adsUrl
}
} catch (e: Exception) {
}
// Try default url
Log.e(tag, "Failed to get the ADS URL, trying the default")
return "https://www.popads.net/js/adblock.js"
}
private fun getAuthCode(serverUrl: String, thumbUrl: String, link: String): String {
var authCode = preferences.getString(PREF_AUTHCODE_KEY, "")!!
if (authCode.isNotBlank()) {
Log.d(tag, "AuthCode found in preferences")
val request = Request.Builder()
.head()
.url("${serverUrl}$authCode")
.headers(headers)
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful || response.code == 500) {
Log.d(tag, "AuthCode is OK")
return authCode
}
Log.d(tag, "AuthCode is invalid")
}
Log.d(tag, "Fetching new authCode")
val adsUrl = getAdsUrl(serverUrl, thumbUrl, link, headers)
val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
val body = FormBody.Builder()
.add("category", "client")
.add("type", "premium")
.add("ad", adsContent)
.build()
val newHeaders = headers.newBuilder()
.set("Referer", SITE_URL)
.add("Accept", "*/*")
.add("Cache-Control", "no-cache")
.add("Pragma", "no-cache")
.add("Connection", "keep-alive")
.add("Sec-Fetch-Dest", "empty")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Site", "same-site")
.build()
val publicidade =
client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body))
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (publicidade.isBlank()) {
Log.e(
tag,
"Failed to fetch \"publicidade\" code, the current response: $publicidade",
)
throw Exception("Por favor, abra o vídeo uma vez no navegador para liberar o IP")
}
authCode =
client.newCall(
GET(
"$ADS_URL/?token=$publicidade",
headers = newHeaders,
),
)
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (authCode.startsWith("?")) {
Log.d(tag, "Auth code fetched successfully")
preferences.edit().putString(PREF_AUTHCODE_KEY, authCode).commit()
} else {
Log.e(
tag,
"Failed to fetch auth code, the current response: $authCode",
)
}
return authCode
}
fun getVideoList(response: Response): List<Video> { fun getVideoList(response: Response): List<Video> {
val doc = response.asJsoup() val doc = response.asJsoup()
@ -13,19 +171,33 @@ class HinataSoulExtractor(private val headers: Headers) {
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!! val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content") .attr("content")
.replace("cdn1", "cdn3") .replace("cdn1", "cdn3")
val type = serverUrl.split('/').get(3) val thumbUrl = doc.selectFirst("meta[itemprop=thumbnailUrl]")!!
val qualities = listOfNotNull("SD", "HD", "FULLHD".takeIf { hasFHD }) .attr("content")
val type = serverUrl.split("/").get(3)
val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
val paths = listOf("appsd", "apphd").let { val paths = listOf("appsd", "apphd").let {
if (type.endsWith("2")) { if (type.endsWith("2")) {
it.map { path -> path + "2" } it.map { path -> path + "2" }
} else { } else {
it it
} }
} + listOfNotNull("appfullhd".takeIf { hasFHD }) } + listOf("appfullhd")
val firstLink =
doc.selectFirst("div.video_container > a, div.playerContainer > a")!!.attr("href")
val authCode = getAuthCode(serverUrl, thumbUrl, firstLink)
return qualities.mapIndexed { index, quality -> return qualities.mapIndexed { index, quality ->
val path = paths[index] val path = paths[index]
val url = serverUrl.replace(type, path) val url = serverUrl.replace(type, path) + authCode
Video(url, quality, url, headers = headers) Video(url, quality, url, headers = headers)
}.reversed() }.reversed()
} }
companion object {
private const val PREF_AUTHCODE_KEY = "authcode"
private const val ADS_URL = "https://ads.anitube.vip"
private const val SITE_URL = "https://www.anitube.vip"
}
} }