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

View File

@ -3,10 +3,7 @@ package eu.kanade.tachiyomi.animeextension.pt.animesvision
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animesvision.dto.AVResponseDto
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.animeextension.pt.animesvision.extractors.AnimesVisionExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
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.Video
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.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
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.Interceptor
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -51,8 +38,6 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.addInterceptor(::loginInterceptor)
.build()
private val json: Json by injectLazy()
private val preferences by lazy {
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 {
val params = AVFilters.getSearchParameters(filters)
val url = "$baseUrl/search?".toHttpUrl().newBuilder()
val url = "$baseUrl/search-anime".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("nome", query)
.addQueryParameter("tipo", params.type)
@ -122,7 +107,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.addQueryParameter("generos", params.genres)
.build()
return GET(url.toString(), headers)
return GET(url, headers)
}
override fun searchAnimeSelector() = "div.film_list-wrap div.film-poster"
@ -139,6 +124,7 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val content = doc.selectFirst("div#ani_detail div.anis-content")!!
val detail = content.selectFirst("div.anisc-detail")!!
@ -152,13 +138,13 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
status = parseStatus(infos.getInfo("Status"))
description = buildString {
append(infos.getInfo("Sinopse") + "\n")
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("Foi ao ar em")?.also { append("\nFoi ao ar em: $it") }
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") }
infos.getInfo("Duração")?.also { append("\nDuração: $it") }
infos.getInfo("Fansub")?.also { append("\nFansub: $it") }
appendLine(infos.getInfo("Sinopse"))
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("Foi ao ar em")?.also { append("\nFoi ao ar em: ", it) }
infos.getInfo("Temporada")?.also { append("\nTemporada: ", it) }
infos.getInfo("Duração")?.also { append("\nDuração: ", it) }
infos.getInfo("Fansub")?.also { append("\nFansub: ", it) }
}
}
@ -193,62 +179,13 @@ class AnimesVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val body = response.body.string()
val internalVideos = GlobalVisionExtractor()
.videoListFromHtml(body)
.toMutableList()
val externalVideos = externalVideosFromEpisode(response.asJsoup(body))
return internalVideos + externalVideos
val doc = response.asJsoup()
val encodedScript = doc.selectFirst("div.player-frame div#playerglobalapi ~ script")?.data()
// "ERROR: Script not found."
?: throw Exception("ERRO: Script não encontrado.")
return AnimesVisionExtractor.videoListFromScript(encodedScript)
}
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 videoFromElement(element: Element) = 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
)
} ?: ""
}
}