|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".pt.anidong.AniDongUrlActivity"
|
|
||||||
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="anidong.net"
|
|
||||||
android:pathPattern="/anime/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'AniDong'
|
|
||||||
extClass = '.AniDong'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 8.3 KiB |
|
@ -1,264 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.anidong
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.EpisodeListDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.anidong.dto.SearchResultDto
|
|
||||||
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.ParsedAnimeHttpSource
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class AniDong : ParsedAnimeHttpSource() {
|
|
||||||
|
|
||||||
override val name = "AniDong"
|
|
||||||
|
|
||||||
override val baseUrl = "https://anidong.net"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val apiHeaders by lazy {
|
|
||||||
headersBuilder() // sets user-agent
|
|
||||||
.add("Referer", baseUrl)
|
|
||||||
.add("x-requested-with", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
|
||||||
|
|
||||||
override fun popularAnimeSelector() = "article.top10_animes_item > a"
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
title = element.attr("title")
|
|
||||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector() = null
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos/page/$page/")
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "article.main_content_article > a"
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "div.paginacao > a.next"
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
|
||||||
val id = query.removePrefix(PREFIX_SEARCH)
|
|
||||||
client.newCall(GET("$baseUrl/anime/$id"))
|
|
||||||
.awaitSuccess()
|
|
||||||
.use(::searchAnimeByIdParse)
|
|
||||||
} else {
|
|
||||||
super.getSearchAnime(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
|
||||||
val details = animeDetailsParse(response.asJsoup()).apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = AniDongFilters.FILTER_LIST
|
|
||||||
|
|
||||||
private val nonce by lazy {
|
|
||||||
client.newCall(GET("$baseUrl/?js_global=1&ver=6.2.2")).execute()
|
|
||||||
.body.string()
|
|
||||||
.substringAfter("search_nonce")
|
|
||||||
.substringAfter("'")
|
|
||||||
.substringBefore("'")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
val params = AniDongFilters.getSearchParameters(filters)
|
|
||||||
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("letra", "")
|
|
||||||
.add("action", "show_animes_ajax")
|
|
||||||
.add("nome", query)
|
|
||||||
.add("status", params.status)
|
|
||||||
.add("formato", params.format)
|
|
||||||
.add("search_nonce", nonce)
|
|
||||||
.add("paged", page.toString())
|
|
||||||
.apply {
|
|
||||||
params.genres.forEach { add("generos[]", it) }
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return POST("$baseUrl/wp-admin/admin-ajax.php", headers = apiHeaders, body = body)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
|
||||||
val searchData: SearchResultDto = response.body.string()
|
|
||||||
.takeIf { it.trim() != "402" }
|
|
||||||
?.let(json::decodeFromString)
|
|
||||||
?: return AnimesPage(emptyList(), false)
|
|
||||||
|
|
||||||
val animes = searchData.animes.map {
|
|
||||||
SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(it.url)
|
|
||||||
title = it.title
|
|
||||||
thumbnail_url = it.thumbnail_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = searchData.pages > 1 && searchData.animes.size == 10
|
|
||||||
|
|
||||||
return AnimesPage(animes, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector(): String? {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
|
||||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
|
||||||
val doc = getRealDoc(document)
|
|
||||||
val infos = doc.selectFirst("div.anime_infos")!!
|
|
||||||
|
|
||||||
setUrlWithoutDomain(doc.location())
|
|
||||||
title = infos.selectFirst("div > h3")!!.ownText()
|
|
||||||
thumbnail_url = infos.selectFirst("img")?.attr("src")
|
|
||||||
genre = infos.select("div[itemprop=genre] a").eachText().joinToString()
|
|
||||||
artist = infos.selectFirst("div[itemprop=productionCompany]")?.text()
|
|
||||||
|
|
||||||
status = doc.selectFirst("div:contains(Status) span")?.text().let {
|
|
||||||
when {
|
|
||||||
it == null -> SAnime.UNKNOWN
|
|
||||||
it == "Completo" -> SAnime.COMPLETED
|
|
||||||
it.contains("Lançamento") -> SAnime.ONGOING
|
|
||||||
else -> SAnime.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
description = buildString {
|
|
||||||
infos.selectFirst("div.anime_name + div.anime_info")?.text()?.also {
|
|
||||||
append("Nomes alternativos: $it\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.selectFirst("div[itemprop=description]")?.text()?.also {
|
|
||||||
append("\n$it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
|
||||||
override fun episodeListSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val doc = getRealDoc(response.asJsoup())
|
|
||||||
|
|
||||||
val id = doc.selectFirst("link[rel=shortlink]")!!.attr("href").substringAfter("=")
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("action", "show_videos")
|
|
||||||
.add("anime_id", id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val res = client.newCall(POST("$baseUrl/api", apiHeaders, body)).execute()
|
|
||||||
.body.string()
|
|
||||||
val data = json.decodeFromString<EpisodeListDto>(res)
|
|
||||||
|
|
||||||
return buildList {
|
|
||||||
data.episodes.forEach { add(episodeFromObject(it, "Episódio")) }
|
|
||||||
data.movies.forEach { add(episodeFromObject(it, "Filme")) }
|
|
||||||
data.ovas.forEach { add(episodeFromObject(it, "OVA")) }
|
|
||||||
sortByDescending { it.episode_number }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun episodeFromObject(episode: EpisodeDto, prefix: String) = SEpisode.create().apply {
|
|
||||||
setUrlWithoutDomain(episode.epi_url)
|
|
||||||
episode_number = episode.epi_num.toFloatOrNull() ?: 0F
|
|
||||||
name = "$prefix ${episode.epi_num}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return doc.select("div.player_option").flatMap {
|
|
||||||
val url = it.attr("data-playerlink")
|
|
||||||
val playerName = it.text().trim()
|
|
||||||
videosFromUrl(url, playerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun videosFromUrl(url: String, playerName: String): List<Video> {
|
|
||||||
val scriptData = client.newCall(GET(url, apiHeaders)).execute()
|
|
||||||
.asJsoup()
|
|
||||||
.selectFirst("script:containsData(sources)")
|
|
||||||
?.data() ?: return emptyList()
|
|
||||||
|
|
||||||
return scriptData.substringAfter("sources: [").substringBefore("]")
|
|
||||||
.split("{")
|
|
||||||
.drop(1)
|
|
||||||
.map {
|
|
||||||
val videoUrl = it.substringAfter("file: \"").substringBefore('"')
|
|
||||||
val label = it.substringAfter("label: \"", "Unknown").substringBefore('"')
|
|
||||||
val quality = "$playerName - $label"
|
|
||||||
Video(videoUrl, quality, videoUrl, headers = apiHeaders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
private fun getRealDoc(document: Document): Document {
|
|
||||||
if (!document.location().contains("/video/")) return document
|
|
||||||
|
|
||||||
return document.selectFirst(".episodioControleItem:has(i.ri-grid-fill)")?.let {
|
|
||||||
client.newCall(GET(it.attr("href"), headers)).execute()
|
|
||||||
.asJsoup()
|
|
||||||
} ?: document
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREFIX_SEARCH = "id:"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.anidong
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
|
|
||||||
object AniDongFilters {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
|
|
||||||
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
|
|
||||||
|
|
||||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
|
||||||
|
|
||||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
|
||||||
return (getFirst<R>() as QueryPartFilter).toQueryPart()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
|
||||||
return first { it is R } as R
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
|
||||||
options: Array<Pair<String, String>>,
|
|
||||||
): List<String> {
|
|
||||||
return (getFirst<R>() as CheckBoxFilterList).state
|
|
||||||
.asSequence()
|
|
||||||
.filter { it.state }
|
|
||||||
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
|
|
||||||
.filter(String::isNotBlank)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusFilter : QueryPartFilter("Status", AniDongFiltersData.STATUS_LIST)
|
|
||||||
class FormatFilter : QueryPartFilter("Formato", AniDongFiltersData.FORMAT_LIST)
|
|
||||||
|
|
||||||
class GenresFilter : CheckBoxFilterList("Gêneros", AniDongFiltersData.GENRES_LIST)
|
|
||||||
|
|
||||||
val FILTER_LIST get() = AnimeFilterList(
|
|
||||||
StatusFilter(),
|
|
||||||
FormatFilter(),
|
|
||||||
GenresFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class FilterSearchParams(
|
|
||||||
val status: String = "",
|
|
||||||
val format: String = "",
|
|
||||||
val genres: List<String> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
|
||||||
if (filters.isEmpty()) return FilterSearchParams()
|
|
||||||
|
|
||||||
return FilterSearchParams(
|
|
||||||
filters.asQueryPart<StatusFilter>(),
|
|
||||||
filters.asQueryPart<FormatFilter>(),
|
|
||||||
filters.parseCheckbox<GenresFilter>(AniDongFiltersData.GENRES_LIST),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object AniDongFiltersData {
|
|
||||||
private val SELECT = Pair("<Selecione>", "")
|
|
||||||
|
|
||||||
val STATUS_LIST = arrayOf(
|
|
||||||
SELECT,
|
|
||||||
Pair("Lançamento", "Lançamento"),
|
|
||||||
Pair("Completo", "Completo"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val FORMAT_LIST = arrayOf(
|
|
||||||
SELECT,
|
|
||||||
Pair("Donghua", "Anime"),
|
|
||||||
Pair("Filme", "Filme"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val GENRES_LIST = arrayOf(
|
|
||||||
Pair("Artes Marciais", "9"),
|
|
||||||
Pair("Aventura", "6"),
|
|
||||||
Pair("Ação", "2"),
|
|
||||||
Pair("Boys Love", "43"),
|
|
||||||
Pair("Comédia", "15"),
|
|
||||||
Pair("Corrida", "94"),
|
|
||||||
Pair("Cultivo", "12"),
|
|
||||||
Pair("Demônios", "18"),
|
|
||||||
Pair("Detetive", "24"),
|
|
||||||
Pair("Drama", "16"),
|
|
||||||
Pair("Escolar", "77"),
|
|
||||||
Pair("Espaço", "54"),
|
|
||||||
Pair("Esporte", "95"),
|
|
||||||
Pair("Fantasia", "7"),
|
|
||||||
Pair("Guerra", "26"),
|
|
||||||
Pair("Harém", "17"),
|
|
||||||
Pair("Histórico", "8"),
|
|
||||||
Pair("Horror", "44"),
|
|
||||||
Pair("Isekai", "72"),
|
|
||||||
Pair("Jogo", "25"),
|
|
||||||
Pair("Mecha", "40"),
|
|
||||||
Pair("Militar", "21"),
|
|
||||||
Pair("Mistério", "3"),
|
|
||||||
Pair("Mitolgia", "96"),
|
|
||||||
Pair("Mitologia", "19"),
|
|
||||||
Pair("O Melhor Donghua", "91"),
|
|
||||||
Pair("Polícia", "57"),
|
|
||||||
Pair("Política", "63"),
|
|
||||||
Pair("Psicológico", "33"),
|
|
||||||
Pair("Reencarnação", "30"),
|
|
||||||
Pair("Romance", "11"),
|
|
||||||
Pair("Sci-Fi", "39"),
|
|
||||||
Pair("Slice of Life", "84"),
|
|
||||||
Pair("Sobrenatural", "4"),
|
|
||||||
Pair("Super Poder", "67"),
|
|
||||||
Pair("Suspense", "32"),
|
|
||||||
Pair("Tragédia", "58"),
|
|
||||||
Pair("Vampiro", "82"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.anidong
|
|
||||||
|
|
||||||
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://anidong.net/anime/<item> intents
|
|
||||||
* and redirects them to the main Aniyomi process.
|
|
||||||
*/
|
|
||||||
class AniDongUrlActivity : Activity() {
|
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val item = pathSegments[1]
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.ANIMESEARCH"
|
|
||||||
putExtra("query", "${AniDong.PREFIX_SEARCH}$item")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.anidong.dto
|
|
||||||
|
|
||||||
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 animes: List<AnimeDto>,
|
|
||||||
@SerialName("total_pages")
|
|
||||||
val pages: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AnimeDto(
|
|
||||||
@SerialName("anime_capa")
|
|
||||||
val thumbnail_url: String,
|
|
||||||
@SerialName("anime_permalink")
|
|
||||||
val url: String,
|
|
||||||
@SerialName("anime_title")
|
|
||||||
val title: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeListDto(
|
|
||||||
@Serializable(with = EpisodeListSerializer::class)
|
|
||||||
@SerialName("episodios")
|
|
||||||
val episodes: List<EpisodeDto>,
|
|
||||||
@Serializable(with = EpisodeListSerializer::class)
|
|
||||||
@SerialName("filmes")
|
|
||||||
val movies: List<EpisodeDto>,
|
|
||||||
@Serializable(with = EpisodeListSerializer::class)
|
|
||||||
val ovas: List<EpisodeDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeDto(
|
|
||||||
val epi_num: String,
|
|
||||||
val epi_url: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
object EpisodeListSerializer :
|
|
||||||
JsonTransformingSerializer<List<EpisodeDto>>(ListSerializer(EpisodeDto.serializer())) {
|
|
||||||
override fun transformDeserialize(element: JsonElement): JsonElement =
|
|
||||||
when (element) {
|
|
||||||
is JsonObject -> JsonArray(element.values.toList())
|
|
||||||
else -> JsonArray(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".pt.flixei.FlixeiUrlActivity"
|
|
||||||
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="flixei.com"
|
|
||||||
android:pathPattern="/assistir/..*/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,12 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Flixei'
|
|
||||||
extClass = '.Flixei'
|
|
||||||
extVersionCode = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
|
||||||
implementation(project(":lib:mixdrop-extractor"))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 8.9 KiB |
|
@ -1,346 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.flixei
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.flixei.dto.AnimeDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.flixei.dto.ApiResultsDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.flixei.dto.EpisodeDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.flixei.dto.PlayersDto
|
|
||||||
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.ParsedAnimeHttpSource
|
|
||||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
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
|
|
||||||
|
|
||||||
class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
||||||
|
|
||||||
override val name = "Flixei"
|
|
||||||
|
|
||||||
override val baseUrl = "https://flixei.com"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
|
||||||
val body = "slider=3".toFormBody()
|
|
||||||
return POST("$baseUrl/includes/ajax/home.php", body = body)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
|
||||||
val results = response.parseAs<ApiResultsDto<AnimeDto>>()
|
|
||||||
val animes = results.items.values.map(::parseAnimeFromObject)
|
|
||||||
return AnimesPage(animes, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseAnimeFromObject(anime: AnimeDto) = SAnime.create().apply {
|
|
||||||
title = anime.title
|
|
||||||
setUrlWithoutDomain("/assistir/filme/${anime.url}/online/gratis")
|
|
||||||
thumbnail_url = "$baseUrl/content/movies/posterPt/185/${anime.id}.webp"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String? {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filmes/estreia/$page")
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.generalMoviesList > a.gPoster"
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
|
|
||||||
title = element.selectFirst("div.i span")!!.text()
|
|
||||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "div.paginationSystem a.next"
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
|
||||||
val path = query.removePrefix(PREFIX_SEARCH)
|
|
||||||
client.newCall(GET("$baseUrl/assistir/$path/online/gratis"))
|
|
||||||
.awaitSuccess()
|
|
||||||
.use(::searchAnimeByPathParse)
|
|
||||||
} else {
|
|
||||||
super.getSearchAnime(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
|
||||||
val details = animeDetailsParse(response.asJsoup()).apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
return GET("$baseUrl/pesquisar/$query")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeSelector() = latestUpdatesSelector()
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element)
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector() = null
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
|
||||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
thumbnail_url = document.selectFirst("meta[property=og:image]")?.attr("content")
|
|
||||||
val container = document.selectFirst("div.moviePresent")!!
|
|
||||||
with(container) {
|
|
||||||
title = selectFirst("h2.tit")!!.text()
|
|
||||||
genre = select("div.genres > span").eachText().joinToString()
|
|
||||||
author = getInfo("Diretor")
|
|
||||||
artist = getInfo("Produtoras")
|
|
||||||
description = buildString {
|
|
||||||
selectFirst("p")?.text()?.also { append(it + "\n\n") }
|
|
||||||
getInfo("Título")?.also { append("Título original: $it\n") }
|
|
||||||
getInfo("Serie de")?.also { append("ano: $it\n") }
|
|
||||||
getInfo("Elenco")?.also { append("Elenco: $it\n") }
|
|
||||||
getInfo("Qualidade")?.also { append("Qualidade: $it\n") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
|
||||||
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
|
|
||||||
val id = seasonElement.attr("data-load-episodes")
|
|
||||||
val sname = seasonElement.text()
|
|
||||||
val body = "getEpisodes=$id".toFormBody()
|
|
||||||
val response = client.newCall(POST("$EMBED_WAREZ_URL/serieAjax.php", body = body)).execute()
|
|
||||||
val episodes = response.parseAs<ApiResultsDto<EpisodeDto>>().items.values.map {
|
|
||||||
SEpisode.create().apply {
|
|
||||||
name = "Temp $sname: Ep ${it.name}"
|
|
||||||
episode_number = it.name.toFloatOrNull() ?: 0F
|
|
||||||
url = it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return episodes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val docUrl = response.asJsoup().selectFirst("div#playButton")!!
|
|
||||||
.attr("onclick")
|
|
||||||
.substringAfter("'")
|
|
||||||
.substringBefore("'")
|
|
||||||
return if (response.request.url.toString().contains("/serie/")) {
|
|
||||||
client.newCall(GET(docUrl)).execute()
|
|
||||||
.asJsoup()
|
|
||||||
.select("div#seasons div.item[data-load-episodes]")
|
|
||||||
.flatMap(::getSeasonEps)
|
|
||||||
.reversed()
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
SEpisode.create().apply {
|
|
||||||
name = "Filme"
|
|
||||||
episode_number = 1F
|
|
||||||
url = "$EMBED_WAREZ_URL/filme/" + docUrl.substringAfter("=")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ 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 {
|
|
||||||
POST("$EMBED_WAREZ_URL/serieAjax.php", body = "getAudios=$url".toFormBody())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val body = response.body.string()
|
|
||||||
// Pair<Language, Query>
|
|
||||||
val items = if (body.startsWith("{")) {
|
|
||||||
val data = json.decodeFromString<ApiResultsDto<PlayersDto>>(body)
|
|
||||||
data.items.values.flatMap {
|
|
||||||
val lang = if (it.audio == "1") "LEGENDADO" else "DUBLADO"
|
|
||||||
it.iterator().mapNotNull { (server, status) ->
|
|
||||||
if (status == "3") {
|
|
||||||
Pair(lang, "?id=${it.id}&sv=$server")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val doc = response.asJsoup(body)
|
|
||||||
doc.select("div.selectAudioButton").flatMap {
|
|
||||||
val lang = it.text()
|
|
||||||
val id = it.attr("data-load-hosts")
|
|
||||||
doc.select("div[data-load-embed=$id]").map { element ->
|
|
||||||
lang to "?id=$id&sv=${element.attr("data-load-embed-host")}"
|
|
||||||
}
|
|
||||||
}.ifEmpty {
|
|
||||||
val lang = doc.selectFirst("div.selectAudio > b")!!.text()
|
|
||||||
.substringBefore("/")
|
|
||||||
.uppercase()
|
|
||||||
val id = doc.selectFirst("*[data-load-embed]")!!.attr("data-load-embed")
|
|
||||||
doc.select("div.buttonLoadHost").map {
|
|
||||||
lang to "?id=$id&sv=${it.attr("data-load-embed-host")}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items.parallelCatchingFlatMapBlocking(::getVideosFromItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
|
||||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
|
||||||
|
|
||||||
private fun getVideosFromItem(item: Pair<String, String>): List<Video> {
|
|
||||||
val (lang, query) = item
|
|
||||||
val headers = headersBuilder().set("referer", WAREZ_URL).build()
|
|
||||||
val hostUrl = if ("warezcdn" in query) {
|
|
||||||
"$WAREZ_URL/player/player.php$query"
|
|
||||||
} else {
|
|
||||||
client.newCall(GET("$WAREZ_URL/embed/getPlay.php$query", headers))
|
|
||||||
.execute()
|
|
||||||
.body.string()
|
|
||||||
.substringAfter("location.href=\"")
|
|
||||||
.substringBefore("\";")
|
|
||||||
}
|
|
||||||
|
|
||||||
return when (query.substringAfter("sv=")) {
|
|
||||||
"streamtape" -> streamtapeExtractor.videosFromUrl(hostUrl, "Streamtape($lang)")
|
|
||||||
"mixdrop" -> mixdropExtractor.videoFromUrl(hostUrl, lang)
|
|
||||||
else -> null // TODO: Add warezcdn extractor
|
|
||||||
}.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element): Video {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector(): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_PLAYER_KEY
|
|
||||||
title = PREF_PLAYER_TITLE
|
|
||||||
entries = PREF_PLAYER_ARRAY
|
|
||||||
entryValues = PREF_PLAYER_ARRAY
|
|
||||||
setDefaultValue(PREF_PLAYER_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_LANGUAGE_KEY
|
|
||||||
title = PREF_LANGUAGE_TITLE
|
|
||||||
entries = PREF_LANGUAGE_ENTRIES
|
|
||||||
entryValues = PREF_LANGUAGE_VALUES
|
|
||||||
setDefaultValue(PREF_LANGUAGE_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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
private fun Element.getInfo(item: String) = selectFirst("*:containsOwn($item) b")?.text()
|
|
||||||
|
|
||||||
private fun String.toFormBody() = toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
|
||||||
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
|
|
||||||
val language = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
|
||||||
return sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ it.quality.contains(player) },
|
|
||||||
{ it.quality.contains(language) },
|
|
||||||
),
|
|
||||||
).reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val PREFIX_SEARCH = "path:"
|
|
||||||
|
|
||||||
private const val EMBED_WAREZ_URL = "https://embed.warezcdn.net"
|
|
||||||
private const val WAREZ_URL = "https://warezcdn.com"
|
|
||||||
|
|
||||||
private const val PREF_PLAYER_KEY = "pref_player"
|
|
||||||
private const val PREF_PLAYER_DEFAULT = "MixDrop"
|
|
||||||
private const val PREF_PLAYER_TITLE = "Player/Server favorito"
|
|
||||||
private val PREF_PLAYER_ARRAY = arrayOf(
|
|
||||||
"MixDrop",
|
|
||||||
"Streamtape",
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val PREF_LANGUAGE_KEY = "pref_language"
|
|
||||||
private const val PREF_LANGUAGE_DEFAULT = "LEG"
|
|
||||||
private const val PREF_LANGUAGE_TITLE = "Língua/tipo preferido"
|
|
||||||
private val PREF_LANGUAGE_ENTRIES = arrayOf("Legendado", "Dublado")
|
|
||||||
private val PREF_LANGUAGE_VALUES = arrayOf("LEG", "DUB")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.flixei
|
|
||||||
|
|
||||||
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://flixei.com/assistir/<type>/<item> intents
|
|
||||||
* and redirects them to the main Aniyomi process.
|
|
||||||
*/
|
|
||||||
class FlixeiUrlActivity : Activity() {
|
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 2) {
|
|
||||||
val type = pathSegments[1]
|
|
||||||
val item = pathSegments[2]
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.ANIMESEARCH"
|
|
||||||
putExtra("query", "${Flixei.PREFIX_SEARCH}$type/$item")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.flixei.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ApiResultsDto<T>(
|
|
||||||
@SerialName("list")
|
|
||||||
val items: Map<String, T>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AnimeDto(val id: String, val title: String, val url: String)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class EpisodeDto(val id: String, val name: String)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PlayersDto(
|
|
||||||
val id: String,
|
|
||||||
val audio: String,
|
|
||||||
val mixdropStatus: String = "0",
|
|
||||||
val streamtapeStatus: String = "0",
|
|
||||||
val warezcdnStatus: String = "0",
|
|
||||||
) {
|
|
||||||
operator fun iterator(): List<Pair<String, String>> {
|
|
||||||
return listOf(
|
|
||||||
"streamtape" to streamtapeStatus,
|
|
||||||
"mixdrop" to mixdropStatus,
|
|
||||||
"warezcdn" to warezcdnStatus,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Lista de Animes'
|
|
||||||
extClass = '.ListaDeAnimes'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 24 KiB |
|
@ -1,112 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.listadeanimes
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
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.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class ListaDeAnimes : ParsedAnimeHttpSource() {
|
|
||||||
override val name = "Lista de Animes"
|
|
||||||
|
|
||||||
override val baseUrl = "https://www.listadeanimes.com"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", baseUrl)
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page")
|
|
||||||
|
|
||||||
override fun popularAnimeSelector() = "article.post.excerpt > div.capa:not(:has(a[href=$baseUrl/anime-lista-online]))"
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
|
||||||
val img = element.selectFirst("img")!!
|
|
||||||
title = titleCase(img.attr("title").substringBefore(" todos os episódios"))
|
|
||||||
thumbnail_url = img.attr("data-src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector() = "a.next.page-numbers"
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/page/$page?s=$query")
|
|
||||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
|
||||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
|
||||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
|
||||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
|
||||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
val titleText = document.selectFirst("h1.title.single-title")!!.text()
|
|
||||||
title = titleCase(titleText.substringBefore(" todos os episódios"))
|
|
||||||
thumbnail_url = document.selectFirst("img.aligncenter.size-full")?.attr("src")
|
|
||||||
val infos = document.selectFirst("div#content.post-single-content > center")
|
|
||||||
val infosText = infos?.run {
|
|
||||||
html()
|
|
||||||
.replace("<br>", "\n")
|
|
||||||
.replace("<b>", "")
|
|
||||||
.replace("</b>", "")
|
|
||||||
}?.let { "\n\n$it" }.orEmpty()
|
|
||||||
|
|
||||||
val sinopse = document.selectFirst("div#content > *:contains(Sinopse)")?.nextElementSibling()
|
|
||||||
description = (sinopse?.text() ?: "Sem sinopse.") + infosText
|
|
||||||
genre = document.select("a[rel=tag]").joinToString { it.text() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
|
||||||
override fun episodeListSelector() = "div.videos > ul"
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return doc.select("div.videos > ul > li:gt(0)")
|
|
||||||
.map(::episodeFromElement)
|
|
||||||
.reversed()
|
|
||||||
}
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode {
|
|
||||||
return SEpisode.create().apply {
|
|
||||||
episode_number = runCatching {
|
|
||||||
element.selectFirst("string")!!
|
|
||||||
.text()
|
|
||||||
.substringAfter(" ")
|
|
||||||
.toFloat()
|
|
||||||
}.getOrDefault(0F)
|
|
||||||
name = element.text().substringAfter("► ")
|
|
||||||
url = element.attr("id")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
|
||||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
|
||||||
return listOf(Video(episode.url, episode.name, episode.url))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
|
||||||
override fun videoListRequest(episode: SEpisode) = throw UnsupportedOperationException()
|
|
||||||
override fun videoListParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
|
||||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
private fun titleCase(str: String): String {
|
|
||||||
return str.split(' ')
|
|
||||||
.map { it.replaceFirstChar(Char::uppercase) }
|
|
||||||
.joinToString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".pt.megaflix.MegaflixUrlActivity"
|
|
||||||
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="megaflix.co"
|
|
||||||
android:pathPattern="/..*/..*/"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,20 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Megaflix'
|
|
||||||
extClass = '.Megaflix'
|
|
||||||
extVersionCode = 24
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(':lib:mixdrop-extractor'))
|
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
|
||||||
implementation(project(":lib:voe-extractor"))
|
|
||||||
implementation(project(":lib:filemoon-extractor"))
|
|
||||||
implementation(project(":lib:vidhide-extractor"))
|
|
||||||
implementation(project(":lib:streamwish-extractor"))
|
|
||||||
implementation(project(":lib:playlist-utils"))
|
|
||||||
// for mixdrop and megaflix
|
|
||||||
implementation(libs.jsunpacker)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 9.5 KiB |
|
@ -1,317 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors.MegaflixExtractor
|
|
||||||
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.ParsedAnimeHttpSource
|
|
||||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|
||||||
|
|
||||||
override val name = "Megaflix"
|
|
||||||
|
|
||||||
override val baseUrl = "https://megaflix.ac"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
|
|
||||||
|
|
||||||
override fun popularAnimeSelector() = "section#widget_list_movies_series-5 li > article"
|
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
|
||||||
title = element.selectFirst("h2.entry-title")!!.text()
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector() = null
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val pageType = preferences.getString(PREF_LATEST_PAGE_KEY, PREF_LATEST_PAGE_DEFAULT)!!
|
|
||||||
return GET("$baseUrl/$pageType/page/$page")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "li > article"
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "div.nav-links > a:containsOwn(PRÓXIMO)"
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
override suspend fun getSearchAnime(
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: AnimeFilterList,
|
|
||||||
): AnimesPage {
|
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
|
||||||
val path = query.removePrefix(PREFIX_SEARCH)
|
|
||||||
client.newCall(GET("$baseUrl/$path"))
|
|
||||||
.awaitSuccess()
|
|
||||||
.use(::searchAnimeByPathParse)
|
|
||||||
} else {
|
|
||||||
super.getSearchAnime(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
|
||||||
val details = animeDetailsParse(response.asJsoup()).apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
return if (query.isNotBlank()) {
|
|
||||||
GET("$baseUrl/page/$page/?s=$query")
|
|
||||||
} else {
|
|
||||||
val genre = MegaflixFilters.getGenre(filters)
|
|
||||||
GET("$baseUrl/categoria/$genre/page/$page")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeSelector() = latestUpdatesSelector()
|
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
|
||||||
|
|
||||||
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
|
||||||
|
|
||||||
override fun getFilterList() = MegaflixFilters.FILTER_LIST
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
|
||||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
val infos = document.selectFirst("div.bd > article.post.single")!!
|
|
||||||
title = infos.selectFirst("h1.entry-title")!!.text()
|
|
||||||
thumbnail_url = infos.selectFirst("img")?.absUrl("src")
|
|
||||||
genre = infos.select("span.genres > a").eachText().joinToString()
|
|
||||||
description = infos.selectFirst("div.description")?.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
|
||||||
override fun episodeListSelector() = "li > article.episodes"
|
|
||||||
private fun seasonListSelector() = "section.episodes div.choose-season > a"
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val seasons = doc.select(seasonListSelector())
|
|
||||||
return when {
|
|
||||||
seasons.isEmpty() -> listOf(
|
|
||||||
SEpisode.create().apply {
|
|
||||||
name = "Filme"
|
|
||||||
setUrlWithoutDomain(doc.location())
|
|
||||||
episode_number = 1F
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> seasons.parallelFlatMapBlocking(::episodesFromSeason).reversed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun episodesFromSeason(seasonElement: Element): List<SEpisode> {
|
|
||||||
return seasonElement.attr("href").let { url ->
|
|
||||||
client.newCall(GET(url, headers)).await()
|
|
||||||
.asJsoup()
|
|
||||||
.select(episodeListSelector())
|
|
||||||
.map(::episodeFromElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
|
||||||
name = element.selectFirst("h2.entry-title")!!.text()
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
|
|
||||||
episode_number = element.selectFirst("span.num-epi")?.run {
|
|
||||||
text().split("x").last().toFloatOrNull() ?: 0F
|
|
||||||
} ?: 0F
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val items = response.asJsoup().select(videoListSelector())
|
|
||||||
return items
|
|
||||||
.parallelCatchingFlatMapBlocking { element ->
|
|
||||||
val language = element.text().substringAfter("-")
|
|
||||||
val id = element.attr("href")
|
|
||||||
val url = element.closest("body")?.selectFirst("div$id iframe")
|
|
||||||
?.let {
|
|
||||||
val iframeUrl = it.attr("src")
|
|
||||||
client.newCall(GET(iframeUrl, headers))
|
|
||||||
.execute()
|
|
||||||
.asJsoup()
|
|
||||||
.selectFirst("iframe")
|
|
||||||
?.attr("src")
|
|
||||||
} ?: return@parallelCatchingFlatMapBlocking emptyList()
|
|
||||||
|
|
||||||
getVideoList(url, language)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*--------------------------------Video extractors------------------------------------*/
|
|
||||||
private val webViewResolver by lazy { WebViewResolver() }
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
|
||||||
|
|
||||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
|
||||||
private val megaflixExtractor by lazy { MegaflixExtractor(client, headers) }
|
|
||||||
private val voeExtractor by lazy { VoeExtractor(client) }
|
|
||||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
|
||||||
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
|
|
||||||
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
|
|
||||||
|
|
||||||
private fun getVideoList(url: String, language: String): List<Video> {
|
|
||||||
return when {
|
|
||||||
arrayOf("mixdrop", "mixdroop", "mix").any(url) -> mixdropExtractor.videoFromUrl(url, language)
|
|
||||||
arrayOf("streamtape", "stp", "stape").any(url) -> streamtapeExtractor.videosFromUrl(url, "StreamTape - $language")
|
|
||||||
arrayOf("mflix").any(url) -> megaflixExtractor.videosFromUrl(url, language)
|
|
||||||
arrayOf("voe").any(url) -> voeExtractor.videosFromUrl(url, "$language ")
|
|
||||||
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "$language Filemoon:")
|
|
||||||
arrayOf("vidhide", "vid.").any(url) -> vidHideExtractor.videosFromUrl(url, videoNameGen = { "$language VidHide:$it" })
|
|
||||||
arrayOf("wishembed", "streamwish", "strwish", "wish", "jwplayerhls").any(url) -> streamWishExtractor.videosFromUrl(url, videoNameGen = { "$language StreamWish:$it" })
|
|
||||||
arrayOf("fembedder").any(url) -> {
|
|
||||||
val webViewResult = webViewResolver.getUrl(url, headers)
|
|
||||||
if (webViewResult.isBlank()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
return if (webViewResult.contains("m3u8")) {
|
|
||||||
playlistUtils.extractFromHls(webViewResult)
|
|
||||||
} else {
|
|
||||||
listOf(Video(url, "Default", url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListSelector() = "aside.video-options li a"
|
|
||||||
|
|
||||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_QUALITY_KEY
|
|
||||||
title = PREF_QUALITY_TITLE
|
|
||||||
entries = PREF_QUALITY_ENTRIES
|
|
||||||
entryValues = PREF_QUALITY_ENTRIES
|
|
||||||
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)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_LANGUAGE_KEY
|
|
||||||
title = PREF_LANGUAGE_TITLE
|
|
||||||
entries = PREF_LANGUAGE_VALUES
|
|
||||||
entryValues = PREF_LANGUAGE_VALUES
|
|
||||||
setDefaultValue(PREF_LANGUAGE_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_LATEST_PAGE_KEY
|
|
||||||
title = PREF_LATEST_PAGE_TITLE
|
|
||||||
entries = PREF_LATEST_PAGE_ENTRIES
|
|
||||||
entryValues = PREF_LATEST_PAGE_VALUES
|
|
||||||
setDefaultValue(PREF_LATEST_PAGE_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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
|
||||||
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
|
||||||
return sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ it.quality.contains(quality) },
|
|
||||||
{ it.quality.contains(lang) },
|
|
||||||
),
|
|
||||||
).reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREFIX_SEARCH = "path:"
|
|
||||||
|
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
|
||||||
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
|
|
||||||
private const val PREF_QUALITY_DEFAULT = "720p"
|
|
||||||
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "480p", "720p", "1080p")
|
|
||||||
|
|
||||||
private const val PREF_LANGUAGE_KEY = "pref_language"
|
|
||||||
private const val PREF_LANGUAGE_DEFAULT = "Legendado"
|
|
||||||
private const val PREF_LANGUAGE_TITLE = "Língua/tipo preferido"
|
|
||||||
private val PREF_LANGUAGE_VALUES = arrayOf("Legendado", "Dublado")
|
|
||||||
|
|
||||||
private const val PREF_LATEST_PAGE_KEY = "pref_latest_page"
|
|
||||||
private const val PREF_LATEST_PAGE_DEFAULT = "series"
|
|
||||||
private const val PREF_LATEST_PAGE_TITLE = "Página de últimos adicionados"
|
|
||||||
private val PREF_LATEST_PAGE_ENTRIES = arrayOf(
|
|
||||||
"Filmes",
|
|
||||||
"Séries",
|
|
||||||
)
|
|
||||||
private val PREF_LATEST_PAGE_VALUES = arrayOf(
|
|
||||||
"filmes",
|
|
||||||
"series",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
|
|
||||||
object MegaflixFilters {
|
|
||||||
|
|
||||||
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.asQueryPart(): String {
|
|
||||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreFilter : QueryPartFilter("Gênero", MegaflixFiltersData.GENRES)
|
|
||||||
|
|
||||||
val FILTER_LIST get() = AnimeFilterList(
|
|
||||||
AnimeFilter.Header(MegaflixFiltersData.IGNORE_SEARCH_MSG),
|
|
||||||
GenreFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getGenre(filters: AnimeFilterList) = filters.asQueryPart<GenreFilter>()
|
|
||||||
|
|
||||||
private object MegaflixFiltersData {
|
|
||||||
const val IGNORE_SEARCH_MSG = "NOTA: O filtro é IGNORADO ao usar a pesquisa."
|
|
||||||
|
|
||||||
val GENRES = arrayOf(
|
|
||||||
Pair("Animação", "animacao"),
|
|
||||||
Pair("Aventura", "aventura"),
|
|
||||||
Pair("Ação", "acao"),
|
|
||||||
Pair("Biografia", "biografia"),
|
|
||||||
Pair("Comédia", "comedia"),
|
|
||||||
Pair("Crime", "crime"),
|
|
||||||
Pair("Documentário", "documentario"),
|
|
||||||
Pair("Drama", "drama"),
|
|
||||||
Pair("Esporte", "esporte"),
|
|
||||||
Pair("Família", "familia"),
|
|
||||||
Pair("Fantasia", "fantasia"),
|
|
||||||
Pair("Faroeste", "faroeste"),
|
|
||||||
Pair("Ficção científica", "ficcao-cientifica"),
|
|
||||||
Pair("Guerra", "guerra"),
|
|
||||||
Pair("História", "historia"),
|
|
||||||
Pair("Mistério", "misterio"),
|
|
||||||
Pair("Musical", "musical"),
|
|
||||||
Pair("Música", "musica"),
|
|
||||||
Pair("Romance", "romance"),
|
|
||||||
Pair("Show", "show"),
|
|
||||||
Pair("Terror", "terror"),
|
|
||||||
Pair("Thriller", "thriller"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix
|
|
||||||
|
|
||||||
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://megaflix.ac/<type>/<item> intents
|
|
||||||
* and redirects them to the main Aniyomi process.
|
|
||||||
*/
|
|
||||||
class MegaflixUrlActivity : Activity() {
|
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val path = "${pathSegments[0]}/${pathSegments[1]}"
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.ANIMESEARCH"
|
|
||||||
putExtra("query", "${Megaflix.PREFIX_SEARCH}$path")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import okhttp3.Headers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class WebViewResolver() {
|
|
||||||
private val context: Application by injectLazy()
|
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun getUrl(origRequestUrl: String, origRequestheader: Headers): String {
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
var webView: WebView? = null
|
|
||||||
var resultUrl = ""
|
|
||||||
val headers = origRequestheader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
val webview = WebView(context)
|
|
||||||
webView = webview
|
|
||||||
with(webview.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
useWideViewPort = false
|
|
||||||
loadWithOverviewMode = false
|
|
||||||
userAgentString = origRequestheader["User-Agent"]
|
|
||||||
}
|
|
||||||
webview.webViewClient = object : WebViewClient() {
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
): WebResourceResponse? {
|
|
||||||
val url = request.url.toString()
|
|
||||||
if (VIDEO_REGEX.containsMatchIn(url)) {
|
|
||||||
resultUrl = url
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webView?.loadUrl(origRequestUrl, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
webView?.stopLoading()
|
|
||||||
webView?.destroy()
|
|
||||||
webView = null
|
|
||||||
}
|
|
||||||
return resultUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TIMEOUT_SEC: Long = 20
|
|
||||||
private val VIDEO_REGEX by lazy { Regex("\\.(mp4|m3u8)") }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors
|
|
||||||
|
|
||||||
import dev.datlag.jsunpacker.JsUnpacker
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
class MegaflixExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, lang: String = ""): List<Video> {
|
|
||||||
val unpacked = client.newCall(GET(url, headers)).execute()
|
|
||||||
.body.string()
|
|
||||||
.let(JsUnpacker::unpackAndCombine)
|
|
||||||
?.replace("\\", "")
|
|
||||||
?: return emptyList()
|
|
||||||
|
|
||||||
val playlistUrl = unpacked.substringAfter("file':'").substringBefore("'")
|
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
|
||||||
playlistUrl,
|
|
||||||
"https://megaflix.ac",
|
|
||||||
videoNameGen = { "Megaflix($lang) - $it" },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Pobreflix'
|
|
||||||
extClass = '.Pobreflix'
|
|
||||||
themePkg = 'dooplay'
|
|
||||||
baseUrl = 'https://pobreflix.global'
|
|
||||||
overrideVersionCode = 18
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:filemoon-extractor"))
|
|
||||||
implementation(project(':lib:fireplayer-extractor'))
|
|
||||||
implementation(project(":lib:streamwish-extractor"))
|
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
|
||||||
implementation(project(":lib:playlist-utils"))
|
|
||||||
implementation(libs.jsunpacker)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 9.1 KiB |
|
@ -1,80 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.pobreflix
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.MyStreamExtractor
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.PlayerFlixExtractor
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.SuperFlixExtractor
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.fireplayerextractor.FireplayerExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
|
||||||
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class Pobreflix : DooPlay(
|
|
||||||
"pt-BR",
|
|
||||||
"Pobreflix",
|
|
||||||
"https://pobreflix.global",
|
|
||||||
) {
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeSelector() = "div.featured div.poster"
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/series/page/$page/", headers)
|
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
|
||||||
private val fireplayerExtractor by lazy { FireplayerExtractor(client) }
|
|
||||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
|
||||||
private val mystreamExtractor by lazy { MyStreamExtractor(client, headers) }
|
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
|
||||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
|
||||||
private val playerflixExtractor by lazy { PlayerFlixExtractor(client, headers, ::genericExtractor) }
|
|
||||||
private val superflixExtractor by lazy { SuperFlixExtractor(client, headers, ::genericExtractor) }
|
|
||||||
private val supercdnExtractor by lazy { SuperFlixExtractor(client, headers, ::genericExtractor, "https://supercdn.org") }
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
return doc.select("div.source-box > a").parallelCatchingFlatMapBlocking {
|
|
||||||
val data = it.attr("href").trim().toHttpUrl().queryParameter("auth")
|
|
||||||
?.let { Base64.decode(it, Base64.DEFAULT) }
|
|
||||||
?.let(::String)
|
|
||||||
?: return@parallelCatchingFlatMapBlocking emptyList()
|
|
||||||
val url = data.replace("\\", "").substringAfter("url\":\"").substringBefore('"')
|
|
||||||
genericExtractor(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun genericExtractor(url: String, language: String = ""): List<Video> {
|
|
||||||
val langSubstr = if (language.isBlank()) "" else "[$language] "
|
|
||||||
return when {
|
|
||||||
url.contains("superflix") ->
|
|
||||||
superflixExtractor.videosFromUrl(url)
|
|
||||||
url.contains("supercdn") ->
|
|
||||||
supercdnExtractor.videosFromUrl(url)
|
|
||||||
url.contains("filemoon") ->
|
|
||||||
filemoonExtractor.videosFromUrl(url, "${langSubstr}Filemoon - ", headers = headers)
|
|
||||||
url.contains("watch.brplayer") || url.contains("/watch?v=") ->
|
|
||||||
mystreamExtractor.videosFromUrl(url, language)
|
|
||||||
url.contains("brbeast") ->
|
|
||||||
fireplayerExtractor.videosFromUrl(url = url, videoNameGen = { "${langSubstr}BrBeast - $it" })
|
|
||||||
url.contains("embedplayer") ->
|
|
||||||
fireplayerExtractor.videosFromUrl(url = url, videoNameGen = { "${langSubstr}EmbedPlayer - $it" })
|
|
||||||
url.contains("superembeds") ->
|
|
||||||
fireplayerExtractor.videosFromUrl(url = url, videoNameGen = { "${langSubstr}SuperEmbeds - $it" })
|
|
||||||
url.contains("streamtape") ->
|
|
||||||
streamtapeExtractor.videosFromUrl(url, "${langSubstr}Streamtape")
|
|
||||||
url.contains("filelions") ->
|
|
||||||
streamwishExtractor.videosFromUrl(url, videoNameGen = { "${langSubstr}FileLions - $it" })
|
|
||||||
url.contains("streamwish") ->
|
|
||||||
streamwishExtractor.videosFromUrl(url, videoNameGen = { "${langSubstr}Streamwish - $it" })
|
|
||||||
url.contains("playerflix") ->
|
|
||||||
playerflixExtractor.videosFromUrl(url)
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
// From animeworldindia
|
|
||||||
class MyStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, language: String): List<Video> {
|
|
||||||
val host = url.substringBefore("/watch?")
|
|
||||||
|
|
||||||
val response = client.newCall(GET(url, headers)).execute()
|
|
||||||
val body = response.body.string()
|
|
||||||
|
|
||||||
val codePart = body
|
|
||||||
.substringAfter("sniff(") // Video function
|
|
||||||
.substringBefore(",[")
|
|
||||||
|
|
||||||
val streamCode = codePart
|
|
||||||
.substringAfterLast(",\"") // our beloved hash
|
|
||||||
.substringBefore('"')
|
|
||||||
|
|
||||||
val id = codePart.substringAfter(",\"").substringBefore('"') // required ID
|
|
||||||
|
|
||||||
val streamUrl = "$host/m3u8/$id/$streamCode/master.txt?s=1&cache=1"
|
|
||||||
|
|
||||||
val cookie = response.headers.firstOrNull {
|
|
||||||
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
|
|
||||||
}?.second?.substringBefore(";") ?: ""
|
|
||||||
|
|
||||||
val newHeaders = headers.newBuilder()
|
|
||||||
.set("cookie", cookie)
|
|
||||||
.set("accept", "*/*")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return playlistUtils.extractFromHls(
|
|
||||||
streamUrl,
|
|
||||||
masterHeaders = newHeaders,
|
|
||||||
videoHeaders = newHeaders,
|
|
||||||
videoNameGen = { "[$language] MyStream: $it" },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
class PlayerFlixExtractor(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val defaultHeaders: Headers,
|
|
||||||
private val genericExtractor: (String, String) -> List<Video>,
|
|
||||||
) {
|
|
||||||
fun videosFromUrl(url: String): List<Video> {
|
|
||||||
val doc = client.newCall(GET(url, defaultHeaders)).execute().asJsoup()
|
|
||||||
|
|
||||||
val items = doc.select("#hostList div.buttonLoadHost").mapNotNull {
|
|
||||||
val url = it.attr("onclick")
|
|
||||||
.substringAfter('"', "")
|
|
||||||
.substringBefore('"')
|
|
||||||
?: return@mapNotNull null
|
|
||||||
|
|
||||||
val language = if (it.hasClass("hostDub")) {
|
|
||||||
"Dublado"
|
|
||||||
} else {
|
|
||||||
"Legendado"
|
|
||||||
}
|
|
||||||
|
|
||||||
language to url // (Language, videoId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items.parallelCatchingFlatMapBlocking { genericExtractor(it.second, it.first) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class SuperFlixExtractor(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val defaultHeaders: Headers,
|
|
||||||
private val genericExtractor: (String, String) -> List<Video>,
|
|
||||||
private val host: String = "https://superflixapi.dev",
|
|
||||||
) {
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String): List<Video> {
|
|
||||||
val links = linksFromUrl(url)
|
|
||||||
|
|
||||||
val fixedLinks = links.parallelCatchingFlatMapBlocking {
|
|
||||||
val (language, linkUrl) = it
|
|
||||||
when {
|
|
||||||
linkUrl.contains("?vid=") -> linksFromPlayer(linkUrl, language)
|
|
||||||
else -> listOf(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixedLinks.parallelCatchingFlatMapBlocking { genericExtractor(it.second, it.first) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun linksFromPlayer(url: String, language: String): List<Pair<String, String>> {
|
|
||||||
val httpUrl = url.toHttpUrl()
|
|
||||||
val id = httpUrl.queryParameter("vid")!!
|
|
||||||
val headers = defaultHeaders.newBuilder()
|
|
||||||
.set("referer", "$host/")
|
|
||||||
.set("origin", host)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val doc = client.newCall(GET(url, headers)).await().asJsoup()
|
|
||||||
|
|
||||||
val baseUrl = "https://" + httpUrl.host
|
|
||||||
val apiUrl = "$baseUrl/ajax_sources.php"
|
|
||||||
|
|
||||||
val apiHeaders = headers.newBuilder()
|
|
||||||
.set("referer", url)
|
|
||||||
.set("origin", baseUrl)
|
|
||||||
.set("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return doc.select("ul > li[data-order-value]").mapNotNull {
|
|
||||||
val name = it.attr("data-dropdown-value")
|
|
||||||
val order = it.attr("data-order-value")
|
|
||||||
|
|
||||||
val formBody = FormBody.Builder()
|
|
||||||
.add("vid", id)
|
|
||||||
.add("alternative", name)
|
|
||||||
.add("ord", order)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val req = client.newCall(POST(apiUrl, apiHeaders, formBody)).await()
|
|
||||||
.body.string()
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
val iframeUrl = json.decodeFromString<PlayerLinkDto>(req).iframe!!
|
|
||||||
val iframeServer = iframeUrl.toHttpUrl().queryParameter("sv")!!
|
|
||||||
language to when (name) {
|
|
||||||
"1xbet" -> "https://watch.brplayer.site/watch?v=${iframeServer.trim('/')}"
|
|
||||||
else -> iframeServer
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PlayerLinkDto(val iframe: String? = null)
|
|
||||||
|
|
||||||
private fun linksFromUrl(url: String): List<Pair<String, String>> {
|
|
||||||
val doc = client.newCall(GET(url, defaultHeaders)).execute().asJsoup()
|
|
||||||
|
|
||||||
val items = doc.select("div.select_languages").mapNotNull {
|
|
||||||
val id = it.nextElementSibling()
|
|
||||||
?.selectFirst("div[data-id]")
|
|
||||||
?.attr("data-id")
|
|
||||||
?: return@mapNotNull null
|
|
||||||
|
|
||||||
val language = it.text()
|
|
||||||
.replace("Disponível", "")
|
|
||||||
.replace("disponível", "")
|
|
||||||
.replace("Apenas", "")
|
|
||||||
.replace("em", "")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
language to id // (Language, videoId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val headers = defaultHeaders.newBuilder()
|
|
||||||
.set("Origin", host)
|
|
||||||
.set("Referer", url)
|
|
||||||
.set("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return items.mapNotNull {
|
|
||||||
runCatching {
|
|
||||||
it.first to getLink(it.second, headers)!!
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLink(id: String, headers: Headers): String? {
|
|
||||||
val body = FormBody.Builder()
|
|
||||||
.add("action", "getPlayer")
|
|
||||||
.add("video_id", id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val res = client.newCall(POST("$host/api", headers, body)).execute()
|
|
||||||
.body.string()
|
|
||||||
|
|
||||||
return json.decodeFromString<ApiResponseDto>(res).data?.video_url
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ApiResponseDto(val data: DataDto? = null)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DataDto(val video_url: String? = null)
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Vizer.tv'
|
|
||||||
extClass = '.Vizer'
|
|
||||||
extVersionCode = 24
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(':lib:fireplayer-extractor'))
|
|
||||||
implementation(project(':lib:mixdrop-extractor'))
|
|
||||||
implementation(project(':lib:playlist-utils'))
|
|
||||||
implementation(project(':lib:streamtape-extractor'))
|
|
||||||
implementation(libs.jsunpacker)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,359 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.vizer
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.VizerFilters.FilterSearchParams
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.EpisodeListDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.dto.HostersDto
|
|
||||||
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.VideoListDto
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.interceptor.WebViewResolver
|
|
||||||
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.fireplayerextractor.FireplayerExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
|
||||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
class Vizer : ConfigurableAnimeSource, AnimeHttpSource() {
|
|
||||||
|
|
||||||
override val name = "Vizer.tv"
|
|
||||||
|
|
||||||
override val baseUrl = "https://novizer.com"
|
|
||||||
private val apiUrl = "$baseUrl/includes/ajax"
|
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
private val episodesClient by lazy {
|
|
||||||
client.newBuilder().rateLimitHost(baseUrl.toHttpUrl(), 1, 1.5.seconds).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val webViewResolver by lazy { WebViewResolver(headers) }
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
override fun popularAnimeRequest(page: Int): Request {
|
|
||||||
val pageType = preferences.getString(PREF_POPULAR_PAGE_KEY, PREF_POPULAR_PAGE_DEFAULT)!!
|
|
||||||
val params = FilterSearchParams(
|
|
||||||
orderBy = "vzViews",
|
|
||||||
orderWay = "desc",
|
|
||||||
type = pageType,
|
|
||||||
)
|
|
||||||
return searchAnimeRequest(page, "", params)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
|
||||||
val result = response.parseAs<SearchResultDto>()
|
|
||||||
val animes = result.items.values.map(::animeFromObject)
|
|
||||||
val hasNext = result.quantity == 35
|
|
||||||
return AnimesPage(animes, hasNext)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animeFromObject(item: SearchItemDto) = SAnime.create().apply {
|
|
||||||
val (urlslug, imgslug) = when {
|
|
||||||
item.status.isBlank() -> Pair("filme", "movies")
|
|
||||||
else -> Pair("serie", "series")
|
|
||||||
}
|
|
||||||
url = "/$urlslug/online/${item.url}"
|
|
||||||
title = item.title
|
|
||||||
status = when (item.status) {
|
|
||||||
"Retornando" -> SAnime.ONGOING
|
|
||||||
else -> SAnime.COMPLETED
|
|
||||||
}
|
|
||||||
thumbnail_url = "$baseUrl/content/$imgslug/posterPt/342/${item.id}.webp"
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
override fun latestUpdatesRequest(page: Int) = apiRequest("getHomeSliderSeries=1")
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
|
||||||
val parsedData = response.parseAs<SearchResultDto>()
|
|
||||||
val animes = parsedData.items.values.map(::animeFromObject)
|
|
||||||
return AnimesPage(animes, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
override fun getFilterList(): AnimeFilterList = VizerFilters.FILTER_LIST
|
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
|
||||||
|
|
||||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) {
|
|
||||||
val path = query.removePrefix(PREFIX_SEARCH).replace("/", "/online/")
|
|
||||||
client.newCall(GET("$baseUrl/$path"))
|
|
||||||
.awaitSuccess()
|
|
||||||
.use(::searchAnimeByPathParse)
|
|
||||||
} else {
|
|
||||||
super.getSearchAnime(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
|
||||||
val details = animeDetailsParse(response).apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
|
||||||
val params = VizerFilters.getSearchParameters(filters)
|
|
||||||
return searchAnimeRequest(page, query, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeRequest(page: Int, query: String, params: FilterSearchParams): Request {
|
|
||||||
val urlBuilder = "$apiUrl/ajaxPagination.php".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", "${page - 1}")
|
|
||||||
.addQueryParameter("categoryFilterYearMin", params.minYear)
|
|
||||||
.addQueryParameter("categoryFilterYearMax", params.maxYear)
|
|
||||||
.addQueryParameter("categoryFilterOrderBy", params.orderBy)
|
|
||||||
.addQueryParameter("categoryFilterOrderWay", params.orderWay)
|
|
||||||
.apply {
|
|
||||||
if (query.isNotBlank()) addQueryParameter("search", query)
|
|
||||||
|
|
||||||
when (params.type) {
|
|
||||||
"Movies" -> {
|
|
||||||
addQueryParameter("saga", "0")
|
|
||||||
addQueryParameter("categoriesListMovies", params.genre)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
addQueryParameter("categoriesListSeries", params.genre)
|
|
||||||
val isAnime = params.type == "anime"
|
|
||||||
addQueryParameter("anime", if (isAnime) "1" else "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GET(urlBuilder.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================== Anime Details ============================
|
|
||||||
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
setUrlWithoutDomain(doc.location())
|
|
||||||
title = doc.selectFirst("section.ai > h2")!!.text()
|
|
||||||
thumbnail_url = doc.selectFirst("meta[property=og:image]")!!.attr("content")
|
|
||||||
|
|
||||||
description = buildString {
|
|
||||||
append(doc.selectFirst("span.desc")!!.text() + "\n")
|
|
||||||
doc.selectFirst("div.year")?.also { append("\nAno: ", it.text()) }
|
|
||||||
doc.selectFirst("div.tm")?.also { append("\nDuração: ", it.text()) }
|
|
||||||
doc.selectFirst("a.rating")?.also { append("\nNota: ", it.text()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
|
||||||
private fun getSeasonEps(seasonElement: Element): List<SEpisode> {
|
|
||||||
val id = seasonElement.attr("data-season-id")
|
|
||||||
val sname = seasonElement.text()
|
|
||||||
val response = episodesClient.newCall(apiRequest("getEpisodes=$id")).execute()
|
|
||||||
val episodes = response.parseAs<EpisodeListDto>().episodes
|
|
||||||
.values
|
|
||||||
.filter { it.released === true }
|
|
||||||
.map {
|
|
||||||
SEpisode.create().apply {
|
|
||||||
name = "$sname: Ep ${it.name}".run {
|
|
||||||
if (!it.title.contains("Episode ")) {
|
|
||||||
this + " - ${it.title}"
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
episode_number = it.name.toFloatOrNull() ?: 0F
|
|
||||||
url = it.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return episodes
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val seasons = doc.select("div.seasons div.list 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("getEpisodeData=$url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
|
||||||
val body = response.body.string()
|
|
||||||
val videoObjectList = if (body.startsWith("{")) {
|
|
||||||
body.parseAs<VideoListDto>().videos.values.toList()
|
|
||||||
} else {
|
|
||||||
val doc = response.asJsoup(body)
|
|
||||||
doc.select("div.audios div[data-load-player]").mapNotNull {
|
|
||||||
try {
|
|
||||||
val movieHosters = it.attr("data-players").parseAs<HostersDto>()
|
|
||||||
val movieId = it.attr("data-load-player")
|
|
||||||
val movieLang = if (it.hasClass("legendado")) "1" else "0"
|
|
||||||
VideoDto(movieId, movieLang).apply { hosters = movieHosters }
|
|
||||||
} catch (_: Throwable) { null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return videoObjectList.parallelCatchingFlatMapBlocking(::getVideosFromObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mixdropExtractor by lazy { MixDropExtractor(client) }
|
|
||||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
|
||||||
private val fireplayerExtractor by lazy { FireplayerExtractor(client) }
|
|
||||||
|
|
||||||
private suspend fun getVideosFromObject(videoObj: VideoDto): List<Video> {
|
|
||||||
val hosters = videoObj.hosters ?: return emptyList()
|
|
||||||
|
|
||||||
val langPrefix = if (videoObj.lang == "1") "LEG" else "DUB"
|
|
||||||
|
|
||||||
return hosters.iterator().parallelCatchingFlatMap { (name, status) ->
|
|
||||||
// Always try the warezcdn
|
|
||||||
if (status != 3 && name != "warezcdn") return@parallelCatchingFlatMap emptyList()
|
|
||||||
val url = getPlayerUrl(videoObj.id, name)
|
|
||||||
if (url.isNullOrBlank()) {
|
|
||||||
return@parallelCatchingFlatMap emptyList()
|
|
||||||
}
|
|
||||||
when (name) {
|
|
||||||
"mixdrop" -> mixdropExtractor.videosFromUrl(url, langPrefix)
|
|
||||||
"streamtape" -> streamtapeExtractor.videosFromUrl(url, "StreamTape($langPrefix)")
|
|
||||||
"warezcdn" -> fireplayerExtractor.videosFromUrl(url, videoNameGen = { "WarezCDN($langPrefix) - $it" })
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Settings ==============================
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
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(PREF_POPULAR_PAGE_DEFAULT)
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_PLAYER_KEY
|
|
||||||
title = PREF_PLAYER_TITLE
|
|
||||||
entries = PREF_PLAYER_ARRAY
|
|
||||||
entryValues = PREF_PLAYER_ARRAY
|
|
||||||
setDefaultValue(PREF_PLAYER_DEFAULT)
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_LANGUAGE_KEY
|
|
||||||
title = PREF_LANGUAGE_TITLE
|
|
||||||
entries = PREF_LANGUAGE_ENTRIES
|
|
||||||
entryValues = PREF_LANGUAGE_VALUES
|
|
||||||
setDefaultValue(PREF_LANGUAGE_DEFAULT)
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
private val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
|
||||||
|
|
||||||
private fun getPlayerUrl(id: String, name: String): String? {
|
|
||||||
return webViewResolver.getUrl("$baseUrl/embed/getEmbed.php?id=$id&sv=$name", "$baseUrl/termos")
|
|
||||||
}
|
|
||||||
|
|
||||||
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("$apiUrl/publicFunctions.php", newHeaders, body = reqBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
|
||||||
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
|
|
||||||
val language = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
|
||||||
return sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ it.quality.contains(player) },
|
|
||||||
{ it.quality.contains(language) },
|
|
||||||
),
|
|
||||||
).reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val PREF_POPULAR_PAGE_KEY = "pref_popular_page"
|
|
||||||
private const val PREF_POPULAR_PAGE_DEFAULT = "movie"
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val PREF_PLAYER_KEY = "pref_player"
|
|
||||||
private const val PREF_PLAYER_DEFAULT = "MixDrop"
|
|
||||||
private const val PREF_PLAYER_TITLE = "Player/Server favorito"
|
|
||||||
private val PREF_PLAYER_ARRAY = arrayOf(
|
|
||||||
"MixDrop",
|
|
||||||
"StreamTape",
|
|
||||||
"WarezCDN",
|
|
||||||
)
|
|
||||||
|
|
||||||
private const val PREF_LANGUAGE_KEY = "pref_language"
|
|
||||||
private const val PREF_LANGUAGE_DEFAULT = "LEG"
|
|
||||||
private const val PREF_LANGUAGE_TITLE = "Língua/tipo preferido"
|
|
||||||
private val PREF_LANGUAGE_ENTRIES = arrayOf("Legendado", "Dublado")
|
|
||||||
private val PREF_LANGUAGE_VALUES = arrayOf("LEG", "DUB")
|
|
||||||
|
|
||||||
const val PREFIX_SEARCH = "path:"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.vizer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animeextension.pt.vizer.VizerFilters.VizerFiltersData.CURRENT_YEAR
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
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.asQueryPart(): String {
|
|
||||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
|
||||||
}
|
|
||||||
|
|
||||||
class TypeFilter : QueryPartFilter("Tipo", VizerFiltersData.TYPES)
|
|
||||||
class MinYearFilter : QueryPartFilter("Ano (min)", VizerFiltersData.MIN_YEARS)
|
|
||||||
class MaxYearFilter : QueryPartFilter("Ano (max)", VizerFiltersData.MAX_YEARS)
|
|
||||||
class GenreFilter : QueryPartFilter("Categoria", VizerFiltersData.GENRES)
|
|
||||||
|
|
||||||
class SortFilter : AnimeFilter.Sort(
|
|
||||||
"Ordernar por",
|
|
||||||
VizerFiltersData.ORDERS.map { it.first }.toTypedArray(),
|
|
||||||
Selection(0, false),
|
|
||||||
)
|
|
||||||
|
|
||||||
val FILTER_LIST get() = AnimeFilterList(
|
|
||||||
TypeFilter(),
|
|
||||||
MinYearFilter(),
|
|
||||||
MaxYearFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class FilterSearchParams(
|
|
||||||
val type: String = "anime",
|
|
||||||
val minYear: String = "1890",
|
|
||||||
val maxYear: String = CURRENT_YEAR.toString(),
|
|
||||||
val genre: String = "all",
|
|
||||||
val orderBy: String = "rating",
|
|
||||||
val orderWay: String = "desc",
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
|
||||||
if (filters.isEmpty()) return FilterSearchParams()
|
|
||||||
|
|
||||||
val sortFilter = filters.firstOrNull { it is SortFilter } as? SortFilter
|
|
||||||
val (orderBy, ascending) = sortFilter?.state?.run {
|
|
||||||
val order = VizerFiltersData.ORDERS[index].second
|
|
||||||
val orderWay = if (ascending) "asc" else "desc"
|
|
||||||
|
|
||||||
Pair(order, orderWay)
|
|
||||||
} ?: Pair("rating", "desc")
|
|
||||||
|
|
||||||
return FilterSearchParams(
|
|
||||||
filters.asQueryPart<TypeFilter>(),
|
|
||||||
filters.asQueryPart<MinYearFilter>(),
|
|
||||||
filters.asQueryPart<MaxYearFilter>(),
|
|
||||||
filters.asQueryPart<GenreFilter>(),
|
|
||||||
orderBy,
|
|
||||||
ascending,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object VizerFiltersData {
|
|
||||||
val TYPES = arrayOf(
|
|
||||||
Pair("Animes", "anime"),
|
|
||||||
Pair("Filmes", "Movies"),
|
|
||||||
Pair("Series", "Series"),
|
|
||||||
)
|
|
||||||
val CURRENT_YEAR by lazy {
|
|
||||||
Calendar.getInstance()[Calendar.YEAR]
|
|
||||||
}
|
|
||||||
val MAX_YEARS = (CURRENT_YEAR downTo 1890).map {
|
|
||||||
Pair(it.toString(), it.toString())
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
val MIN_YEARS = MAX_YEARS.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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.vizer.dto
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
|
||||||
import kotlinx.serialization.EncodeDefault
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
import kotlinx.serialization.json.JsonTransformingSerializer
|
|
||||||
import kotlinx.serialization.json.booleanOrNull
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
|
|
||||||
typealias FakeList<T> = Map<String, T>
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchResultDto(
|
|
||||||
val quantity: Int = 0,
|
|
||||||
@EncodeDefault
|
|
||||||
@SerialName("list")
|
|
||||||
val items: FakeList<SearchItemDto> = emptyMap(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchItemDto(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val url: String,
|
|
||||||
@EncodeDefault
|
|
||||||
val status: String = "",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class EpisodeListDto(
|
|
||||||
@SerialName("list")
|
|
||||||
val episodes: FakeList<EpisodeItemDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class EpisodeItemDto(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
@Serializable(with = BooleanSerializer::class)
|
|
||||||
val released: Boolean,
|
|
||||||
val title: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class VideoListDto(
|
|
||||||
@SerialName("list")
|
|
||||||
val videos: FakeList<VideoDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class VideoDto(
|
|
||||||
val id: String,
|
|
||||||
val lang: String,
|
|
||||||
@SerialName("players")
|
|
||||||
private val players: String? = null,
|
|
||||||
) {
|
|
||||||
var hosters = try {
|
|
||||||
players?.parseAs<HostersDto>()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class HostersDto(
|
|
||||||
val mixdrop: Int = 0,
|
|
||||||
val streamtape: Int = 0,
|
|
||||||
val warezcdn: Int = 0,
|
|
||||||
) {
|
|
||||||
operator fun iterator(): List<Pair<String, Int>> {
|
|
||||||
return listOf(
|
|
||||||
"mixdrop" to mixdrop,
|
|
||||||
"streamtape" to streamtape,
|
|
||||||
"warezcdn" to warezcdn,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object BooleanSerializer : JsonTransformingSerializer<Boolean>(Boolean.serializer()) {
|
|
||||||
override fun transformDeserialize(element: JsonElement): JsonElement {
|
|
||||||
require(element is JsonPrimitive)
|
|
||||||
return if (element.jsonPrimitive.isString && element.jsonPrimitive.content == "true") {
|
|
||||||
JsonPrimitive(true)
|
|
||||||
} else {
|
|
||||||
JsonPrimitive(element.jsonPrimitive.booleanOrNull ?: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.vizer.interceptor
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import okhttp3.Headers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class WebViewResolver(private val globalHeaders: Headers) {
|
|
||||||
private val context: Application by injectLazy()
|
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
||||||
private val tag by lazy { javaClass.simpleName }
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun getUrl(origRequestUrl: String, baseUrl: String): String? {
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
var webView: WebView? = null
|
|
||||||
var result: String? = null
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
val webview = WebView(context)
|
|
||||||
webView = webview
|
|
||||||
with(webview.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
useWideViewPort = false
|
|
||||||
loadWithOverviewMode = false
|
|
||||||
userAgentString = globalHeaders["User-Agent"]
|
|
||||||
}
|
|
||||||
webview.webViewClient = object : WebViewClient() {
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
): WebResourceResponse? {
|
|
||||||
val url = request.url.toString()
|
|
||||||
Log.d(tag, "Checking url $url")
|
|
||||||
if (VIDEO_REGEX.containsMatchIn(url)) {
|
|
||||||
result = url
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
Log.d(tag, "onPageFinished $url")
|
|
||||||
super.onPageFinished(view, url)
|
|
||||||
|
|
||||||
view?.evaluateJavascript("document.body.innerHTML += '<iframe src=\"" + origRequestUrl + "\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"\" webkitallowfullscreen=\"\" mozallowfullscreen=\"\"></iframe>'") {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
webView?.loadUrl(baseUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
|
||||||
|
|
||||||
handler.post {
|
|
||||||
webView?.stopLoading()
|
|
||||||
webView?.destroy()
|
|
||||||
webView = null
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TIMEOUT_SEC: Long = 25
|
|
||||||
private val VIDEO_REGEX by lazy { Regex("//(mixdrop|streamtape|warezcdn)|/video/") }
|
|
||||||
}
|
|
||||||
}
|
|