New source: Vizer.tv (#967)

* Add vizer base

* Add MixDrop extractor

* Use StreamTape extractor

* Use Fembed extractor

* Dont count with WarezCDN

* Add containsNsfw flag to build.gradle
This commit is contained in:
Claudemirovsky
2022-10-22 09:27:26 -03:00
committed by GitHub
parent aaf4c6af4c
commit 5656b066f0
12 changed files with 616 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.vizer.VizerUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="vizer.tv"
android:pathPattern="/..*/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

20
src/pt/vizer/build.gradle Normal file
View File

@ -0,0 +1,20 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Vizer.tv'
pkgNameSuffix = 'pt.vizer'
extClass = '.Vizer'
extVersionCode = 1
libVersion = '13'
containsNsfw = true
}
dependencies {
implementation(project(':lib-fembed-extractor'))
implementation(project(':lib-streamtape-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

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: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,311 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.EpisodeListDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.PlayersDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchItemDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.SearchResultDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.VideoLanguagesDto
import eu.kanade.tachiyomi.animeextension.pt.vizer.extractors.MixDropExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Vizer.tv"
override val baseUrl = "https://vizer.tv"
private val API_URL = "$baseUrl/includes/ajax"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json = Json {
ignoreUnknownKeys = true
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val initialUrl = "$API_URL/ajaxPagination.php?categoryFilterOrderBy=vzViews&page=$page&categoryFilterOrderWay=desc&categoryFilterYearMin=1950&categoryFilterYearMax=2022"
val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, "movie")!!
val finalUrl = if ("movie" in pageType) {
initialUrl + "&saga=0&categoriesListMovies=all"
} else {
(initialUrl + "&categoriesListSeries=all").let {
if ("anime" in pageType) it + "&anime=1"
else it + "&anime=0"
}
}
return GET(finalUrl)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val result = response.parseAs<SearchResultDto>()
val animes = result.list.map(::animeFromObject).toList()
val hasNext = result.quantity == 35
return AnimesPage(animes, hasNext)
}
private fun animeFromObject(item: SearchItemDto): SAnime =
SAnime.create().apply {
var slug = if (item.status.isBlank()) "filme" else "serie"
url = "/$slug/online/${item.url}"
slug = if (slug == "filme") "movies" else "series"
title = item.title
status = when (item.status) {
"Retornando" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
thumbnail_url = "$baseUrl/content/$slug/posterPt/342/${item.id}.webp"
}
// ============================== Episodes ==============================
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
val id = seasonElement.attr("data-season-id")
val sname = seasonElement.text()
val response = client.newCall(apiRequest("getEpisodes=$id")).execute()
val episodes = response.parseAs<EpisodeListDto>().episodes.mapNotNull {
if (it.released)
SEpisode.create().apply {
name = "Temp $sname: Ep ${it.name} - ${it.title}"
episode_number = it.name.toFloatOrNull() ?: 0F
url = it.id
}
else null
}
return episodes
}
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
val seasons = doc.select("div#seasonsList div.item[data-season-id]")
return if (seasons.size > 0) {
seasons.flatMap(::getSeasonEps).reversed()
} else {
listOf(
SEpisode.create().apply {
name = "Filme"
episode_number = 1F
url = response.request.url.toString()
}
)
}
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val url = episode.url
return if (url.startsWith("https")) {
// Its an real url, maybe from a movie
GET(url, headers)
} else {
// Fake url, its an ID that will be used to get episode languages
// (sub/dub) and then return the video link
apiRequest("getEpisodeLanguages=$url")
}
}
override fun videoListParse(response: Response): List<Video> {
val body = response.body?.string().orEmpty()
val videoObjectList = if (body.startsWith("{")) {
json.decodeFromString<VideoLanguagesDto>(body).videos
} else {
val videoJson = body.substringAfterLast("videoPlayerBox(").substringBefore(");")
json.decodeFromString<VideoLanguagesDto>(videoJson).videos
}
return videoObjectList.flatMap(::getVideosFromObject)
}
private fun getVideosFromObject(videoObj: VideoDto): List<Video> {
val players = client.newCall(apiRequest("getVideoPlayers=${videoObj.id}"))
.execute()
.parseAs<PlayersDto>()
val langPrefix = if (videoObj.lang == "1") "SUB" else "DUB"
val videoList = players.iterator().mapNotNull loop@{ (name, status) ->
if (status == "0") return@loop null
val url = getPlayerUrl(videoObj.id, name)
when {
name == "mixdrop" ->
MixDropExtractor(client)
.videoFromUrl(url, langPrefix)?.let(::listOf)
name == "streamtape" ->
StreamTapeExtractor(client)
.videoFromUrl(url, "StreamTape($langPrefix)")?.let(::listOf)
name == "fembed" ->
runCatching {
FembedExtractor(client)
.videosFromUrl(url, langPrefix)
}.getOrNull()
else -> null
}
}.flatten()
return videoList
}
// =============================== Search ===============================
override fun getFilterList(): AnimeFilterList = VizerFilters.filterList
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH).replace("/", "/online/")
client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess()
.map { response ->
searchAnimeByPathParse(response, path)
}
} else {
val params = VizerFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
private fun searchAnimeByPathParse(response: Response, path: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/" + path
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: VizerFilters.FilterSearchParams): Request {
val urlBuilder = "$API_URL/ajaxPagination.php".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("search", query)
.addQueryParameter("saga", "0")
.addQueryParameter("categoryFilterYearMin", filters.minYear)
.addQueryParameter("categoryFilterYearMax", filters.maxYear)
.addQueryParameter("categoryFilterOrderBy", filters.orderBy)
.addQueryParameter("categoryFilterOrderWay", filters.orderWay)
if (filters.type == "Movies")
urlBuilder.addQueryParameter("categoriesListMovies", filters.genre)
else
urlBuilder.addQueryParameter("categoriesListSeries", filters.genre)
if (filters.type == "anime")
urlBuilder.addQueryParameter("anime", "1")
return GET(urlBuilder.build().toString(), headers)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
return SAnime.create().apply {
title = doc.selectFirst("section.ai > h2").text()
thumbnail_url = doc.selectFirst("meta[property=og:image]").attr("content")
var desc = doc.selectFirst("span.desc").text() + "\n"
doc.selectFirst("div.year")?.let { desc += "\nAno: ${it.text()}" }
doc.selectFirst("div.tm")?.let { desc += "\nDuração: ${it.text()}" }
doc.selectFirst("a.rating")?.let { desc += "\nNota: ${it.text()}" }
description = desc
}
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = apiRequest("getHomeSliderSeries=1")
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsedData = response.parseAs<SearchResultDto>()
val animes = parsedData.list.map(::animeFromObject).toList()
return AnimesPage(animes, false)
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val popularPage = ListPreference(screen.context).apply {
key = PREF_POPULAR_PAGE_KEY
title = PREF_POPULAR_PAGE_TITLE
entries = PREF_POPULAR_PAGE_ENTRIES
entryValues = PREF_POPULAR_PAGE_VALUES
setDefaultValue("anime")
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()
}
}
screen.addPreference(popularPage)
}
// ============================= Utilities ==============================
private fun getPlayerUrl(id: String, name: String): String {
val req = GET("$baseUrl/embed/getPlay.php?id=$id&sv=$name")
val body = client.newCall(req).execute().body?.string().orEmpty()
return body.substringAfter("location.href=\"").substringBefore("\";")
}
private fun apiRequest(body: String): Request {
val reqBody = body.toRequestBody("application/x-www-form-urlencoded".toMediaType())
val newHeaders = headersBuilder().add("x-requested-with", "XMLHttpRequest")
.build()
return POST("$API_URL/publicFunctions.php", newHeaders, body = reqBody)
}
private inline fun <reified T> Response.parseAs(): T {
val responseBody = body?.string().orEmpty()
return json.decodeFromString(responseBody)
}
companion object {
private const val PREF_POPULAR_PAGE_KEY = "pref_popular_page"
private const val PREF_POPULAR_PAGE_TITLE = "Página de Populares"
private val PREF_POPULAR_PAGE_ENTRIES = arrayOf(
"Animes", "Filmes", "Séries"
)
private val PREF_POPULAR_PAGE_VALUES = arrayOf(
"anime", "movie", "serie"
)
const val PREFIX_SEARCH = "path:"
}
}

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object VizerFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray()
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.getFirst<R>().let {
(it as QueryPartFilter).toQueryPart()
}
}
class TypeFilter : QueryPartFilter("Tipo", VizerFiltersData.types)
class MinYearFilter : QueryPartFilter("Ano (min)", VizerFiltersData.minYears)
class MaxYearFilter : QueryPartFilter("Ano (max)", VizerFiltersData.maxYears)
class GenreFilter : QueryPartFilter("Categoria", VizerFiltersData.genres)
class SortFilter : AnimeFilter.Sort(
"Ordernar por",
VizerFiltersData.orders.map { it.first }.toTypedArray(),
Selection(0, true)
)
val filterList = AnimeFilterList(
TypeFilter(),
MinYearFilter(),
MaxYearFilter(),
GenreFilter(),
SortFilter()
)
data class FilterSearchParams(
val type: String = "anime",
val minYear: String = "1890",
val maxYear: String = "2022",
val genre: String = "all",
var orderBy: String = "rating",
var orderWay: String = "desc"
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
val searchParams = FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<MinYearFilter>(),
filters.asQueryPart<MaxYearFilter>(),
filters.asQueryPart<GenreFilter>()
)
filters.getFirst<SortFilter>().state?.let {
val order = VizerFiltersData.orders[it.index].second
searchParams.orderBy = order
searchParams.orderWay = if (it.ascending) "asc" else "desc"
}
return searchParams
}
private object VizerFiltersData {
val types = arrayOf(
Pair("Animes", "anime"),
Pair("Filmes", "Movies"),
Pair("Series", "Series")
)
val maxYears = (2022 downTo 1890).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val minYears = maxYears.reversed().toTypedArray()
val orders = arrayOf(
Pair("Popularidade", "vzViews"),
Pair("Ano", "year"),
Pair("Título", "title"),
Pair("Rating", "rating")
)
val genres = arrayOf(
Pair("Todas", "all"),
Pair("Animação", "animacao"),
Pair("Aventura", "aventura"),
Pair("Ação", "acao"),
Pair("Comédia", "comedia"),
Pair("Crime", "crime"),
Pair("Documentário", "documentario"),
Pair("Drama", "drama"),
Pair("Família", "familia"),
Pair("Fantasia", "fantasia"),
Pair("Faroeste", "faroeste"),
Pair("Guerra", "guerra"),
Pair("LGBTQ+", "lgbt"),
Pair("Mistério", "misterio"),
Pair("Músical", "musical"),
Pair("Romance", "romance"),
Pair("Suspense", "suspense"),
Pair("Terror", "terror"),
)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://vizer.tv/[anime|filme|serie]/online/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class VizerUrlActivity : Activity() {
private val TAG = "VizerUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val query = "${pathSegments[0]}/${pathSegments[2]}"
val searchQuery = Vizer.PREFIX_SEARCH + query
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class SearchResultDto(
val quantity: Int = 0,
@Serializable(with = GenericListSerializer::class)
@EncodeDefault
val list: List<SearchItemDto> = emptyList()
)
@Serializable
data class SearchItemDto(
val id: String,
val title: String,
val url: String,
@EncodeDefault
val status: String = ""
)
@Serializable
data class EpisodeListDto(
@Serializable(with = GenericListSerializer::class)
@SerialName("list")
val episodes: List<EpisodeItemDto>
)
@Serializable
data class EpisodeItemDto(
val id: String,
val img: String,
val name: String,
val released: Boolean,
val title: String
)
@Serializable
data class VideoLanguagesDto(
@SerialName("list")
@Serializable(with = GenericListSerializer::class)
val videos: List<VideoDto>
)
@Serializable
data class VideoDto(
val id: String,
val lang: String
)
@Serializable
data class PlayersDto(
val mixdrop: String = "0",
val streamtape: String = "0",
val fembed: String = "0"
) {
operator fun iterator(): List<Pair<String, String>> {
return listOf(
"mixdrop" to mixdrop,
"streamtape" to streamtape,
"fembed" to fembed
)
}
}
class GenericListSerializer<T>(
private val itemSerializer: KSerializer<T>
) : JsonTransformingSerializer<List<T>>(ListSerializer(itemSerializer)) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val jsonObj = element as JsonObject
return JsonArray(jsonObj.values.toList())
}
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.animeextension.pt.vizer.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class MixDropExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, lang: String = ""): Video? {
val doc = client.newCall(GET(url)).execute().asJsoup()
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data()
?.let { JsUnpacker.unpackAndCombine(it) }
?: return null
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
.substringBefore("\"")
val quality = ("MixDrop").let {
if (lang.isNotBlank()) {
"$it($lang)"
} else it
}
return Video(url, quality, videoUrl)
}
}