fix(pt/animesvision): Fix search & video extractor (#2932)

This commit is contained in:
Claudemirovsky 2024-02-14 18:57:32 -03:00 committed by GitHub
parent 17f9e4163e
commit bf30fdeaba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1505 additions and 1542 deletions

View File

@ -1,13 +1,7 @@
ext { ext {
extName = 'AnimesVision' extName = 'AnimesVision'
extClass = '.AnimesVision' extClass = '.AnimesVision'
extVersionCode = 24 extVersionCode = 25
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor'))
}

View File

@ -5,27 +5,23 @@ import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AVFilters { object AVFilters {
open class QueryPartFilter( internal open class SelectFilter(
displayName: String, displayName: String,
val vals: Array<Pair<String, String>>, val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>( ) : AnimeFilter.Select<String>(
displayName, displayName,
vals.map { it.first }.toTypedArray(), vals.map { it.first }.toTypedArray(),
) { ) {
fun toQueryPart() = vals[state].second inline val selected get() = vals[state].second
} }
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values) internal 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 class CheckBoxVal(name: String) : AnimeFilter.CheckBox(name, false)
private inline fun <reified R> AnimeFilterList.getFirst(): R { private inline fun <reified R : CheckBoxFilterList> AnimeFilterList.parseCheckbox(
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>, options: Array<Pair<String, String>>,
): String { ): String {
return (getFirst<R>() as CheckBoxFilterList).state return (first { it is R } as CheckBoxFilterList).state
.asSequence() .asSequence()
.filter { it.state } .filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second } .map { checkbox -> options.find { it.first == checkbox.name }!!.second }
@ -33,27 +29,27 @@ object AVFilters {
.joinToString(",") .joinToString(",")
} }
private inline fun <reified R> AnimeFilterList.asQueryPart(): String { private inline fun <reified R : SelectFilter> AnimeFilterList.getSelected(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart() return (first { it is R } as SelectFilter).selected
} }
class TypeFilter : QueryPartFilter("Tipo", AVFiltersData.TYPES) internal class TypeFilter : SelectFilter("Tipo", TYPES)
class StatusFilter : QueryPartFilter("Status", AVFiltersData.STATUS) internal class StatusFilter : SelectFilter("Status", STATUS)
class LanguageFilter : QueryPartFilter("Idioma", AVFiltersData.LANGUAGES) internal class LanguageFilter : SelectFilter("Idioma", LANGUAGES)
class SortFilter : QueryPartFilter("Ordenar", AVFiltersData.ORDERS) internal class SortFilter : SelectFilter("Ordenar", ORDERS)
class InitialYearFilter : QueryPartFilter("Ano Inicial", AVFiltersData.INITIAL_YEAR) internal class InitialYearFilter : SelectFilter("Ano Inicial", INITIAL_YEAR)
class LastYearFilter : QueryPartFilter("Ano Final", AVFiltersData.LAST_YEAR) internal class LastYearFilter : SelectFilter("Ano Final", LAST_YEAR)
class FansubFilter : QueryPartFilter("Fansubs", AVFiltersData.FANSUBS) internal class FansubFilter : SelectFilter("Fansubs", FANSUBS)
class SeasonFilter : QueryPartFilter("Temporada", AVFiltersData.SEASONS) internal class SeasonFilter : SelectFilter("Temporada", SEASONS)
class StudioFilter : QueryPartFilter("Estúdio", AVFiltersData.STUDIOS) internal class StudioFilter : SelectFilter("Estúdio", STUDIOS)
class ProducerFilter : QueryPartFilter("Produtora", AVFiltersData.PRODUCERS) internal class ProducerFilter : SelectFilter("Produtora", PRODUCERS)
class GenresFilter : CheckBoxFilterList( internal class GenresFilter : CheckBoxFilterList(
"Gêneros", "Gêneros",
AVFiltersData.GENRES.map { CheckBoxVal(it.first, false) }, GENRES.map { CheckBoxVal(it.first) },
) )
val FILTER_LIST get() = AnimeFilterList( internal val FILTER_LIST get() = AnimeFilterList(
TypeFilter(), TypeFilter(),
StatusFilter(), StatusFilter(),
LanguageFilter(), LanguageFilter(),
@ -85,23 +81,22 @@ object AVFilters {
if (filters.isEmpty()) return FilterSearchParams() if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams( return FilterSearchParams(
filters.asQueryPart<TypeFilter>(), filters.getSelected<TypeFilter>(),
filters.asQueryPart<StatusFilter>(), filters.getSelected<StatusFilter>(),
filters.asQueryPart<LanguageFilter>(), filters.getSelected<LanguageFilter>(),
filters.asQueryPart<SortFilter>(), filters.getSelected<SortFilter>(),
filters.asQueryPart<InitialYearFilter>(), filters.getSelected<InitialYearFilter>(),
filters.asQueryPart<LastYearFilter>(), filters.getSelected<LastYearFilter>(),
filters.asQueryPart<FansubFilter>(), filters.getSelected<FansubFilter>(),
filters.asQueryPart<SeasonFilter>(), filters.getSelected<SeasonFilter>(),
filters.asQueryPart<StudioFilter>(), filters.getSelected<StudioFilter>(),
filters.asQueryPart<ProducerFilter>(), filters.getSelected<ProducerFilter>(),
filters.parseCheckbox<GenresFilter>(AVFiltersData.GENRES), filters.parseCheckbox<GenresFilter>(GENRES),
) )
} }
private object AVFiltersData { private val EVERY = Pair("Todos", "")
val EVERY = Pair("Todos", "") private val TYPES = arrayOf(
val TYPES = arrayOf(
EVERY, EVERY,
Pair("Animes", "1"), Pair("Animes", "1"),
Pair("Filmes", "2"), Pair("Filmes", "2"),
@ -110,20 +105,20 @@ object AVFilters {
Pair("Live Actions", "6"), Pair("Live Actions", "6"),
) )
val STATUS = arrayOf( private val STATUS = arrayOf(
EVERY, EVERY,
Pair("Finalizado", "1"), Pair("Finalizado", "1"),
Pair("Sendo exibido", "2"), Pair("Sendo exibido", "2"),
Pair("Ainda não exibido", "3"), Pair("Ainda não exibido", "3"),
) )
val LANGUAGES = arrayOf( private val LANGUAGES = arrayOf(
EVERY, EVERY,
Pair("Legendados", "1"), Pair("Legendados", "1"),
Pair("Dublados", "2"), Pair("Dublados", "2"),
) )
val ORDERS = arrayOf( private val ORDERS = arrayOf(
Pair("Padrão", ""), Pair("Padrão", ""),
Pair("Adicionado Recentemente", "adicionado_recentemente"), Pair("Adicionado Recentemente", "adicionado_recentemente"),
Pair("Atualizado Recentemente", "atualizado_recentemente"), Pair("Atualizado Recentemente", "atualizado_recentemente"),
@ -131,13 +126,13 @@ object AVFilters {
Pair("Mais visualizados", "mais_visualizados"), Pair("Mais visualizados", "mais_visualizados"),
) )
val INITIAL_YEAR = (1917..2024).map { private val INITIAL_YEAR = (1917..2024).map {
Pair(it.toString(), it.toString()) Pair(it.toString(), it.toString())
}.toTypedArray() }.toTypedArray()
val LAST_YEAR = INITIAL_YEAR.reversed().toTypedArray() private val LAST_YEAR = INITIAL_YEAR.reversed().toTypedArray()
val SEASONS = arrayOf( private val SEASONS = arrayOf(
EVERY, EVERY,
Pair("Inverno 2024", "167"), Pair("Inverno 2024", "167"),
Pair("Outono 2023 ", "166"), Pair("Outono 2023 ", "166"),
@ -294,7 +289,7 @@ object AVFilters {
Pair("Outono 1978", "17"), Pair("Outono 1978", "17"),
) )
val FANSUBS = arrayOf( private val FANSUBS = arrayOf(
EVERY, EVERY,
Pair("AMA", "ama"), Pair("AMA", "ama"),
Pair("ANSK", "ansk"), Pair("ANSK", "ansk"),
@ -321,7 +316,7 @@ object AVFilters {
Pair("SubVision", "subvision"), Pair("SubVision", "subvision"),
) )
val STUDIOS = arrayOf( private val STUDIOS = arrayOf(
EVERY, EVERY,
Pair("3xCube", "329"), Pair("3xCube", "329"),
Pair("8bit", "75"), Pair("8bit", "75"),
@ -669,7 +664,7 @@ object AVFilters {
Pair("Zexcs", "162"), Pair("Zexcs", "162"),
) )
val PRODUCERS = arrayOf( private val PRODUCERS = arrayOf(
EVERY, EVERY,
Pair("12 Diary Holders", "67"), Pair("12 Diary Holders", "67"),
Pair("1st PLACE", "432"), Pair("1st PLACE", "432"),
@ -1451,7 +1446,7 @@ object AVFilters {
Pair("ZOOM ENTERPRISE", "667"), Pair("ZOOM ENTERPRISE", "667"),
) )
val GENRES = arrayOf( private val GENRES = arrayOf(
Pair("Amor de meninas", "amor-de-meninas"), Pair("Amor de meninas", "amor-de-meninas"),
Pair("Amor de meninos", "amor-de-meninos"), Pair("Amor de meninos", "amor-de-meninos"),
Pair("Artes Marciais", "artes-marciais"), Pair("Artes Marciais", "artes-marciais"),
@ -1511,4 +1506,3 @@ object AVFilters {
Pair("Yuri", "yuri"), Pair("Yuri", "yuri"),
) )
} }
}

View File

@ -3,10 +3,7 @@ package eu.kanade.tachiyomi.animeextension.pt.animesvision
import android.app.Application import android.app.Application
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animesvision.dto.AVResponseDto import eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors.AnimesVisionExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesvision.dto.PayloadData
import eu.kanade.tachiyomi.animeextension.pt.animesvision.dto.PayloadItem
import eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors.GlobalVisionExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -14,27 +11,17 @@ 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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -51,8 +38,6 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.addInterceptor(::loginInterceptor) .addInterceptor(::loginInterceptor)
.build() .build()
private val json: Json by injectLazy()
private val preferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -106,7 +91,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AVFilters.getSearchParameters(filters) val params = AVFilters.getSearchParameters(filters)
val url = "$baseUrl/search?".toHttpUrl().newBuilder() val url = "$baseUrl/search-anime".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString()) .addQueryParameter("page", page.toString())
.addQueryParameter("nome", query) .addQueryParameter("nome", query)
.addQueryParameter("tipo", params.type) .addQueryParameter("tipo", params.type)
@ -122,7 +107,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.addQueryParameter("generos", params.genres) .addQueryParameter("generos", params.genres)
.build() .build()
return GET(url.toString(), headers) return GET(url, headers)
} }
override fun searchAnimeSelector() = "div.film_list-wrap div.film-poster" override fun searchAnimeSelector() = "div.film_list-wrap div.film-poster"
@ -139,6 +124,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply { override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document) val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val content = doc.selectFirst("div#ani_detail div.anis-content")!! val content = doc.selectFirst("div#ani_detail div.anis-content")!!
val detail = content.selectFirst("div.anisc-detail")!! val detail = content.selectFirst("div.anisc-detail")!!
@ -152,13 +138,13 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
status = parseStatus(infos.getInfo("Status")) status = parseStatus(infos.getInfo("Status"))
description = buildString { description = buildString {
append(infos.getInfo("Sinopse") + "\n") appendLine(infos.getInfo("Sinopse"))
infos.getInfo("Inglês")?.also { append("\nTítulo em inglês: $it") } infos.getInfo("Inglês")?.also { append("\nTítulo em inglês: ", it) }
infos.getInfo("Japonês")?.also { append("\nTítulo em japonês: $it") } infos.getInfo("Japonês")?.also { append("\nTítulo em japonês: ", it) }
infos.getInfo("Foi ao ar em")?.also { append("\nFoi ao ar em: $it") } infos.getInfo("Foi ao ar em")?.also { append("\nFoi ao ar em: ", it) }
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") } infos.getInfo("Temporada")?.also { append("\nTemporada: ", it) }
infos.getInfo("Duração")?.also { append("\nDuração: $it") } infos.getInfo("Duração")?.also { append("\nDuração: ", it) }
infos.getInfo("Fansub")?.also { append("\nFansub: $it") } infos.getInfo("Fansub")?.also { append("\nFansub: ", it) }
} }
} }
@ -193,62 +179,13 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val body = response.body.string() val doc = response.asJsoup()
val internalVideos = GlobalVisionExtractor() val encodedScript = doc.selectFirst("div.player-frame div#playerglobalapi ~ script")?.data()
.videoListFromHtml(body) // "ERROR: Script not found."
.toMutableList() ?: throw Exception("ERRO: Script não encontrado.")
return AnimesVisionExtractor.videoListFromScript(encodedScript)
val externalVideos = externalVideosFromEpisode(response.asJsoup(body))
return internalVideos + externalVideos
} }
private fun externalVideosFromEpisode(doc: Document): List<Video> {
val wireDiv = doc.selectFirst("div[wire:id]")!!
val initialData = wireDiv.attr("wire:initial-data").dropLast(1)
val wireToken = doc.html()
.substringAfter("livewire_token")
.substringAfter("'")
.substringBefore("'")
val headers = headersBuilder()
.add("x-livewire", "true")
.add("x-csrf-token", wireToken)
.add("content-type", "application/json")
.build()
val players = doc.select("div.server-item > a.btn")
return players.parallelFlatMapBlocking {
val id = it.attr("wire:click")
.substringAfter("(")
.substringBefore(")")
.toIntOrNull() ?: 1
val updateItem = PayloadItem(PayloadData(listOf(id)))
val updateString = json.encodeToString(updateItem)
val body = "$initialData, \"updates\": [$updateString]}"
val reqBody = body.toRequestBody()
val url = "$baseUrl/livewire/message/components.episodio.player-episodio-component"
val response = client.newCall(POST(url, headers, reqBody)).await()
val responseBody = response.body.string()
val resJson = json.decodeFromString<AVResponseDto>(responseBody)
(resJson.serverMemo?.data?.framePlay ?: resJson.effects?.html)
?.let(::parsePlayerData).orEmpty()
}
}
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val voeExtractor by lazy { VoeExtractor(client) }
private fun parsePlayerData(data: String) = runCatching {
when {
"streamtape" in data -> voeExtractor.videosFromUrl(data)
"dood" in data -> doodExtractor.videosFromUrl(data)
"voe.sx" in data -> voeExtractor.videosFromUrl(data)
else -> emptyList()
}
}.getOrElse { emptyList() }
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()

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object AnimesVisionExtractor {
private val REGEX_URL = Regex(""""file":"(\S+?)",.*?"label":"(.*?)"""")
fun videoListFromScript(encodedScript: String): List<Video> {
val decodedScript = JsDecoder.decodeScript(encodedScript)
return REGEX_URL.findAll(decodedScript).map {
val videoUrl = it.groupValues[1].replace("\\", "")
val qualityName = it.groupValues[2]
Video(videoUrl, "PlayerVision $qualityName", videoUrl)
}.toList()
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors
import eu.kanade.tachiyomi.animesource.model.Video
class GlobalVisionExtractor {
companion object {
private val REGEX_URL = Regex(""""file":"(\S+?)",.*?"label":"(.*?)"""")
private const val PREFIX = "GlobalVision"
}
fun videoListFromHtml(html: String): List<Video> {
return REGEX_URL.findAll(html).map {
val videoUrl = it.groupValues[1].replace("\\", "")
val qualityName = it.groupValues[2]
Video(videoUrl, "$PREFIX $qualityName", videoUrl)
}.toList()
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.animeextension.pt.animesvision.extractors
import kotlin.math.pow
// From pt/goanimes, but without b64-decoding.
object JsDecoder {
private fun convertToNum(thing: String, limit: Float): Int {
return thing.split("")
.reversed()
.map { it.toIntOrNull() ?: 0 }
.reduceIndexed { index: Int, acc, num ->
acc + (num * limit.pow(index - 1)).toInt()
}
}
fun decodeScript(encodedString: String, magicStr: String, offset: Int, limit: Int): String {
val regex = "\\w".toRegex()
return encodedString
.split(magicStr[limit])
.dropLast(1)
.map { str ->
val replaced = regex.replace(str) { magicStr.indexOf(it.value).toString() }
val charInt = convertToNum(replaced, limit.toFloat()) - offset
Char(charInt)
}.joinToString("")
}
fun decodeScript(script: String): String {
val regex = """\}\("(\w+)",.*?"(\w+)",(\d+),(\d+),.*?\)""".toRegex()
return regex.find(script)
?.run {
decodeScript(
groupValues[1], // encoded data
groupValues[2], // magic string
groupValues[3].toIntOrNull() ?: 0, // offset
groupValues[4].toIntOrNull() ?: 0, // limit
)
} ?: ""
}
}