Initial commit
24
src/es/animefenix/build.gradle
Normal file
|
@ -0,0 +1,24 @@
|
|||
ext {
|
||||
extName = 'Animefenix'
|
||||
extClass = '.Animefenix'
|
||||
extVersionCode = 38
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation(project(':lib:streamhidevid-extractor'))
|
||||
}
|
BIN
src/es/animefenix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/es/animefenix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,414 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animefenix
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLDecoder
|
||||
|
||||
class Animefenix : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeFenix"
|
||||
|
||||
override val baseUrl = "https://www.animefenix.tv"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy { Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) }
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Amazon"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape",
|
||||
"Fastream", "Filemoon", "StreamWish", "Okru",
|
||||
"Amazon", "AmazonES", "Fireload", "FileLions",
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes?order=likes&page=$page")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val elements = document.select("article.serie-card")
|
||||
val nextPage = document.select("ul.pagination-list li a.pagination-link:contains(Siguiente)").any()
|
||||
val animeList = elements.map { element ->
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.select("figure.image a").attr("abs:href"))
|
||||
title = element.select("div.title h3 a").text()
|
||||
thumbnail_url = element.select("figure.image a img").attr("abs:src")
|
||||
description = element.select("div.serie-card__information p").text()
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, nextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/animes?order=added&page=$page")
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val yearFilter = filters.find { it is YearFilter } as YearFilter
|
||||
val stateFilter = filters.find { it is StateFilter } as StateFilter
|
||||
val typeFilter = filters.find { it is TypeFilter } as TypeFilter
|
||||
val orderByFilter = filters.find { it is OrderByFilter } as OrderByFilter
|
||||
|
||||
val genreFilter = (filters.find { it is TagFilter } as TagFilter).state.filter { it.state }
|
||||
|
||||
var filterUrl = "$baseUrl/animes?"
|
||||
if (query.isNotBlank()) {
|
||||
filterUrl += "&q=$query"
|
||||
} // search by name
|
||||
if (genreFilter.isNotEmpty()) {
|
||||
genreFilter.forEach {
|
||||
filterUrl += "&genero[]=${it.name}"
|
||||
}
|
||||
} // search by genre
|
||||
if (yearFilter.state.isNotBlank()) {
|
||||
filterUrl += "&year[]=${yearFilter.state}"
|
||||
} // search by year
|
||||
if (stateFilter.state != 0) {
|
||||
filterUrl += "&estado[]=${stateFilter.toUriPart()}"
|
||||
} // search by state
|
||||
if (typeFilter.state != 0) {
|
||||
filterUrl += "&type[]=${typeFilter.toUriPart()}"
|
||||
} // search by type
|
||||
filterUrl += "&order=${orderByFilter.toUriPart()}"
|
||||
filterUrl += "&page=$page" // add page
|
||||
|
||||
return when {
|
||||
genreFilter.isEmpty() || yearFilter.state.isNotBlank() ||
|
||||
stateFilter.state != 0 || typeFilter.state != 0 || query.isNotBlank() -> GET(filterUrl, headers)
|
||||
else -> GET("$baseUrl/animes?order=likes&page=$page ")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("ul.anime-page__episode-list.is-size-6 li").map { it ->
|
||||
val epNum = it.select("a span").text().replace("Episodio", "")
|
||||
SEpisode.create().apply {
|
||||
episode_number = epNum.toFloat()
|
||||
name = "Episodio $epNum"
|
||||
setUrlWithoutDomain(it.select("a").attr("abs:href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
val servers = document.selectFirst("script:containsData(var tabsArray)")!!.data()
|
||||
.split("tabsArray").map { it.substringAfter("src='").substringBefore("'").replace("amp;", "") }
|
||||
.filter { it.contains("https") }
|
||||
|
||||
servers.forEach { server ->
|
||||
val decodedUrl = URLDecoder.decode(server, "UTF-8")
|
||||
val realUrl = try {
|
||||
client.newCall(GET(decodedUrl)).execute().asJsoup().selectFirst("script")!!
|
||||
.data().substringAfter("src=\"").substringBefore("\"")
|
||||
} catch (e: Exception) { "" }
|
||||
|
||||
try {
|
||||
serverVideoResolver(realUrl).let { videoList.addAll(it) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
return videoList.filter { it.url.contains("https") || it.url.contains("http") }
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val embedUrl = url.lowercase()
|
||||
try {
|
||||
if (embedUrl.contains("voe")) {
|
||||
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if ((embedUrl.contains("amazon") || embedUrl.contains("amz")) && !embedUrl.contains("disable")) {
|
||||
val video = amazonExtractor(baseUrl + url.substringAfter(".."))
|
||||
if (video.isNotBlank()) {
|
||||
if (url.contains("&ext=es")) {
|
||||
videoList.add(Video(video, "AmazonES", video))
|
||||
} else {
|
||||
videoList.add(Video(video, "Amazon", video))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (embedUrl.contains("ok.ru") || embedUrl.contains("okru")) {
|
||||
OkruExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:", headers = vidHeaders).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("uqload")) {
|
||||
UqloadExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("mp4upload")) {
|
||||
Mp4uploadExtractor(client).videosFromUrl(url, headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("wishembed") || embedUrl.contains("embedwish") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish")) {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://streamwish.to")
|
||||
.add("Referer", "https://streamwish.to/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
|
||||
DoodExtractor(client).videoFromUrl(url, "DoodStream", false)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamlare")) {
|
||||
StreamlareExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("yourupload") || embedUrl.contains("upload")) {
|
||||
YourUploadExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
|
||||
BurstCloudExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("fastream")) {
|
||||
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("upstream")) {
|
||||
UpstreamExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("ahvsh") || embedUrl.contains("streamhide")) {
|
||||
StreamHideVidExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("/stream/fl.php")) {
|
||||
val video = url.substringAfter("/stream/fl.php?v=")
|
||||
if (client.newCall(GET(video)).execute().code == 200) {
|
||||
videoList.add(Video(video, "FireLoad", video))
|
||||
}
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
return SAnime.create().apply {
|
||||
title = document.select("h1.title.has-text-orange").text()
|
||||
genre = document.select("a.button.is-small.is-orange.is-outlined.is-roundedX").joinToString { it.text() }
|
||||
status = parseStatus(document.select("div.column.is-12-mobile.xis-3-tablet.xis-3-desktop.xhas-background-danger.is-narrow-tablet.is-narrow-desktop a").text())
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when {
|
||||
statusString.contains("Emisión") -> SAnime.ONGOING
|
||||
statusString.contains("Finalizado") -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun amazonExtractor(url: String): String {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val videoURl = document.selectFirst("script:containsData(sources: [)")!!.data()
|
||||
.substringAfter("[{\"file\":\"")
|
||||
.substringBefore("\",").replace("\\", "")
|
||||
|
||||
return try {
|
||||
if (client.newCall(GET(videoURl)).execute().code == 200) videoURl else ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
TagFilter("Generos", checkboxesFrom(genreList)),
|
||||
StateFilter(),
|
||||
TypeFilter(),
|
||||
OrderByFilter(),
|
||||
YearFilter(),
|
||||
)
|
||||
|
||||
private val genreList = arrayOf(
|
||||
Pair("Acción", "acción"),
|
||||
Pair("Aventura", "aventura"),
|
||||
Pair("Angeles", "angeles"),
|
||||
Pair("Artes Marciales", "artes-marciales"),
|
||||
Pair("Ciencia Ficcion", "ciencia-ficcion"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Cyberpunk", "cyberpunk"),
|
||||
Pair("Demonios", "demonios"),
|
||||
Pair("Deportes", "deportes"),
|
||||
Pair("Dragones", "dragones"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Escolares", "escolares"),
|
||||
Pair("Fantasía", "fantasía"),
|
||||
Pair("Gore", "gore"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Historico", "historico"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Infantil", "infantil"),
|
||||
Pair("Isekai", "isekai"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Juegos", "juegos"),
|
||||
Pair("Magia", "magia"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Militar", "militar"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Música", "música"),
|
||||
Pair("Ninjas", "ninjas"),
|
||||
Pair("Parodias", "parodias"),
|
||||
Pair("Policia", "policia"),
|
||||
Pair("Psicológico", "psicológico"),
|
||||
Pair("Recuerdos de la vida", "recuerdos-de-la-vida"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Samurai", "samurai"),
|
||||
Pair("Sci-Fi", "sci-fi"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shonen", "shonen"),
|
||||
Pair("Slice of life", "slice-of-life"),
|
||||
Pair("Sobrenatural", "sobrenatural"),
|
||||
Pair("Space", "space"),
|
||||
Pair("Spokon", "spokon"),
|
||||
Pair("SteamPunk", "steampunk"),
|
||||
Pair("SuperPoder", "superpoder"),
|
||||
Pair("Vampiros", "vampiros"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
)
|
||||
|
||||
private fun checkboxesFrom(tagArray: Array<Pair<String, String>>): List<TagCheckBox> = tagArray.map { TagCheckBox(it.second) }
|
||||
|
||||
class TagCheckBox(tag: String) : AnimeFilter.CheckBox(tag, false)
|
||||
|
||||
class TagFilter(name: String, checkBoxes: List<TagCheckBox>) : AnimeFilter.Group<TagCheckBox>(name, checkBoxes)
|
||||
|
||||
private class YearFilter : AnimeFilter.Text("Año")
|
||||
|
||||
private class StateFilter : UriPartFilter(
|
||||
"Estado",
|
||||
arrayOf(
|
||||
Pair("<Seleccionar>", ""),
|
||||
Pair("Emision", "1"),
|
||||
Pair("Finalizado", "2"),
|
||||
Pair("Proximamente", "3"),
|
||||
Pair("En Cuarentena", "4"),
|
||||
),
|
||||
)
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Tipo",
|
||||
arrayOf(
|
||||
Pair("<Seleccionar>", ""),
|
||||
Pair("TV", "tv"),
|
||||
Pair("Pelicula", "movie"),
|
||||
Pair("Especial", "special"),
|
||||
Pair("OVA", "ova"),
|
||||
),
|
||||
)
|
||||
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"Ordenar Por",
|
||||
arrayOf(
|
||||
Pair("Por defecto", "default"),
|
||||
Pair("Recientemente Actualizados", "updated"),
|
||||
Pair("Recientemente Agregados", "added"),
|
||||
Pair("Nombre A-Z", "title"),
|
||||
Pair("Calificación", "likes"),
|
||||
Pair("Más vistos", "visits"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animefenix.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SolidFilesExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
return try {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("\"downloadUrl\":")) {
|
||||
val data = script.data().substringAfter("\"downloadUrl\":").substringBefore(",")
|
||||
val url = data.replace("\"", "")
|
||||
val videoUrl = url
|
||||
val quality = prefix + "SolidFiles"
|
||||
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||
}
|
||||
}
|
||||
videoList
|
||||
} catch (e: Exception) {
|
||||
videoList
|
||||
}
|
||||
}
|
||||
}
|
14
src/es/animeflv/build.gradle
Normal file
|
@ -0,0 +1,14 @@
|
|||
ext {
|
||||
extName = 'AnimeFLV'
|
||||
extClass = '.AnimeFlv'
|
||||
extVersionCode = 55
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
}
|
BIN
src/es/animeflv/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/es/animeflv/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/es/animeflv/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/es/animeflv/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/es/animeflv/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,340 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeflv
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.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.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
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
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.Exception
|
||||
|
||||
class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeFLV"
|
||||
|
||||
override val baseUrl = "https://www3.animeflv.net"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "720"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "StreamWish"
|
||||
private val SERVER_LIST = arrayOf("StreamWish", "YourUpload", "Okru", "Streamtape")
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.Container ul.ListAnimes li article"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/browse?order=rating&page=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("div.Description a.Button").attr("abs:href"))
|
||||
anime.title = element.select("a h3").text()
|
||||
anime.thumbnail_url = try {
|
||||
element.select("a div.Image figure img").attr("src")
|
||||
} catch (e: Exception) {
|
||||
element.select("a div.Image figure img").attr("data-cfsrc")
|
||||
}
|
||||
anime.description = element.select("div.Description p:eq(2)").text().removeSurrounding("\"")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "ul.pagination li a[rel=\"next\"]"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("var anime_info =")) {
|
||||
val animeInfo = script.data().substringAfter("var anime_info = [").substringBefore("];")
|
||||
val arrInfo = json.decodeFromString<List<String>>("[$animeInfo]")
|
||||
|
||||
val animeUri = arrInfo[2]!!.replace("\"", "")
|
||||
val episodes = script.data().substringAfter("var episodes = [").substringBefore("];").trim()
|
||||
val arrEpisodes = episodes.split("],[")
|
||||
arrEpisodes!!.forEach { arrEp ->
|
||||
val noEpisode = arrEp!!.replace("[", "")!!.replace("]", "")!!.split(",")!![0]
|
||||
val ep = SEpisode.create()
|
||||
val url = "$baseUrl/ver/$animeUri-$noEpisode"
|
||||
ep.setUrlWithoutDomain(url)
|
||||
ep.name = "Episodio $noEpisode"
|
||||
ep.episode_number = noEpisode.toFloat()
|
||||
episodeList.add(ep)
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodeList
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "uwu"
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
/*--------------------------------Video extractors------------------------------------*/
|
||||
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
|
||||
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers.newBuilder().add("Referer", "$baseUrl/").build()) }
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val jsonString = document.selectFirst("script:containsData(var videos = {)")?.data() ?: return emptyList()
|
||||
val responseString = jsonString.substringAfter("var videos =").substringBefore(";").trim()
|
||||
return json.decodeFromString<ServerModel>(responseString).sub.parallelCatchingFlatMapBlocking {
|
||||
when (it.title) {
|
||||
"Stape" -> listOf(streamTapeExtractor.videoFromUrl(it.url ?: it.code)!!)
|
||||
"Okru" -> okruExtractor.videosFromUrl(it.url ?: it.code)
|
||||
"YourUpload" -> yourUploadExtractor.videoFromUrl(it.url ?: it.code, headers = headers)
|
||||
"SW" -> streamWishExtractor.videosFromUrl(it.url ?: it.code, videoNameGen = { "StreamWish:$it" })
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val stateFilter = filterList.find { it is StateFilter } as StateFilter
|
||||
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
|
||||
val orderByFilter = filterList.find { it is OrderByFilter } as OrderByFilter
|
||||
var uri = "$baseUrl/browse?"
|
||||
uri += if (query.isNotBlank()) "&q=$query" else ""
|
||||
uri += if (genreFilter.state != 0) "&genre[]=${genreFilter.toUriPart()}" else ""
|
||||
uri += if (stateFilter.state != 0) "&status[]=${stateFilter.toUriPart()}" else ""
|
||||
uri += if (typeFilter.state != 0) "&type[]=${typeFilter.toUriPart()}" else ""
|
||||
uri += "&order=${orderByFilter.toUriPart()}"
|
||||
uri += "&page=$page"
|
||||
return when {
|
||||
query.isNotBlank() || genreFilter.state != 0 || stateFilter.state != 0 || orderByFilter.state != 0 || typeFilter.state != 0 -> GET(uri)
|
||||
else -> GET("$baseUrl/browse?page=$page&order=rating")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
StateFilter(),
|
||||
TypeFilter(),
|
||||
OrderByFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("<Selecionar>", "all"),
|
||||
Pair("Todo", "all"),
|
||||
Pair("Acción", "accion"),
|
||||
Pair("Artes Marciales", "artes_marciales"),
|
||||
Pair("Aventuras", "aventura"),
|
||||
Pair("Carreras", "carreras"),
|
||||
Pair("Ciencia Ficción", "ciencia_ficcion"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Demencia", "demencia"),
|
||||
Pair("Demonios", "demonios"),
|
||||
Pair("Deportes", "deportes"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Escolares", "escolares"),
|
||||
Pair("Espacial", "espacial"),
|
||||
Pair("Fantasía", "fantasia"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Historico", "historico"),
|
||||
Pair("Infantil", "infantil"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Juegos", "juegos"),
|
||||
Pair("Magia", "magia"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Militar", "militar"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Música", "musica"),
|
||||
Pair("Parodia", "parodia"),
|
||||
Pair("Policía", "policia"),
|
||||
Pair("Psicológico", "psicologico"),
|
||||
Pair("Recuentos de la vida", "recuentos_de_la_vida"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Samurai", "samurai"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Sobrenatural", "sobrenatural"),
|
||||
Pair("Superpoderes", "superpoderes"),
|
||||
Pair("Suspenso", "suspenso"),
|
||||
Pair("Terror", "terror"),
|
||||
Pair("Vampiros", "vampiros"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
),
|
||||
)
|
||||
|
||||
private class StateFilter : UriPartFilter(
|
||||
"Estado",
|
||||
arrayOf(
|
||||
Pair("<Seleccionar>", ""),
|
||||
Pair("En emisión", "1"),
|
||||
Pair("Finalizado", "2"),
|
||||
Pair("Próximamente", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Tipo",
|
||||
arrayOf(
|
||||
Pair("<Seleccionar>", ""),
|
||||
Pair("TV", "tv"),
|
||||
Pair("Película", "movie"),
|
||||
Pair("Especial", "special"),
|
||||
Pair("OVA", "ova"),
|
||||
),
|
||||
)
|
||||
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"Ordenar Por",
|
||||
arrayOf(
|
||||
Pair("Por defecto", "default"),
|
||||
Pair("Recientemente Actualizados", "updated"),
|
||||
Pair("Recientemente Agregados", "added"),
|
||||
Pair("Nombre A-Z", "title"),
|
||||
Pair("Calificación", "rating"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.selectFirst("div.AnimeCover div.Image figure img")!!.attr("abs:src")
|
||||
anime.title = document.selectFirst("div.Ficha.fchlt div.Container .Title")!!.text()
|
||||
anime.description = document.selectFirst("div.Description")!!.text().removeSurrounding("\"")
|
||||
anime.genre = document.select("nav.Nvgnrs a").joinToString { it.text() }
|
||||
anime.status = parseStatus(document.select("span.fa-tv").text())
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when {
|
||||
statusString.contains("En emision") -> SAnime.ONGOING
|
||||
statusString.contains("Finalizado") -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse?order=added&page=$page")
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServerModel(
|
||||
@SerialName("SUB")
|
||||
val sub: List<Sub> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Sub(
|
||||
val server: String? = "",
|
||||
val title: String? = "",
|
||||
val ads: Long? = null,
|
||||
val url: String? = null,
|
||||
val code: String = "",
|
||||
@SerialName("allow_mobile")
|
||||
val allowMobile: Boolean? = false,
|
||||
)
|
||||
}
|
11
src/es/animeid/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'AnimeID'
|
||||
extClass = '.AnimeID'
|
||||
extVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
}
|
BIN
src/es/animeid/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/es/animeid/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
src/es/animeid/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/es/animeid/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
src/es/animeid/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 64 KiB |
|
@ -0,0 +1,365 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeid
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.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.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
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
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
class AnimeID : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeID"
|
||||
|
||||
override val baseUrl = "https://www.animeid.tv/"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "#result article.item"
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series?sort=views&pag=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(baseUrl + element.select("a").attr("href"))
|
||||
anime.title = element.select("a header").text()
|
||||
anime.thumbnail_url = element.select("a figure img").attr("src")
|
||||
anime.description = element.select("p div").text().removeSurrounding("\"")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "#paginas ul li:nth-last-child(2) a"
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val animeId = document.select("#ord").attr("data-id")
|
||||
return episodeJsonParse(response.request.url.toString(), animeId)
|
||||
}
|
||||
|
||||
private fun episodeJsonParse(url: String, animeId: String): MutableList<SEpisode> {
|
||||
val capList = mutableListOf<SEpisode>()
|
||||
var nextPage = 1
|
||||
do {
|
||||
val headers = headers.newBuilder()
|
||||
.set("Referer", url)
|
||||
.set("sec-fetch-site", "same-origin")
|
||||
.set("x-requested-with", "XMLHttpRequest")
|
||||
.set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0")
|
||||
.set("Accept-Language", "es-MX,es-419;q=0.9,es;q=0.8,en;q=0.7")
|
||||
.build()
|
||||
|
||||
val responseString = client.newCall(GET("https://www.animeid.tv/ajax/caps?id=$animeId&ord=DESC&pag=$nextPage", headers))
|
||||
.execute().asJsoup().body()!!.toString().substringAfter("<body>").substringBefore("</body>")
|
||||
val jObject = json.decodeFromString<JsonObject>(responseString)
|
||||
val listCaps = jObject["list"]!!.jsonArray
|
||||
listCaps!!.forEach { cap ->
|
||||
val capParsed = cap.jsonObject
|
||||
val epNum = capParsed["numero"]!!.jsonPrimitive.content!!.toFloat()
|
||||
val episode = SEpisode.create()
|
||||
val dateUpload = manualDateParse(capParsed["date"]!!.jsonPrimitive.content!!.toString())
|
||||
episode.episode_number = epNum
|
||||
episode.name = "Episodio $epNum"
|
||||
dateUpload!!.also { episode.date_upload = it }
|
||||
episode.setUrlWithoutDomain(baseUrl + capParsed["href"]!!.jsonPrimitive.content!!.toString())
|
||||
capList.add(episode)
|
||||
}
|
||||
if (listCaps!!.any()) nextPage += 1 else nextPage = -1
|
||||
} while (nextPage != -1)
|
||||
return capList
|
||||
}
|
||||
|
||||
private fun manualDateParse(stringDate: String): Long? {
|
||||
return try {
|
||||
val format = SimpleDateFormat("dd MMM yyyy")
|
||||
format.parse(stringDate!!.toString()).time
|
||||
} catch (e: Exception) {
|
||||
var dateParsed = stringDate.split(" ")
|
||||
val arrMonths = arrayOf("Jun", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||
val day = dateParsed[0]!!.trim().toInt()
|
||||
val month = arrMonths.indexOf(dateParsed[1].trim()) + 1
|
||||
val year = dateParsed[2]!!.trim().toInt()
|
||||
Date(year, month, day).time
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("#partes div.container li.subtab div.parte").forEach { script ->
|
||||
val jsonString = script.attr("data")
|
||||
val jsonUnescape = unescapeJava(jsonString)!!.replace("\\", "")
|
||||
val url = jsonUnescape.substringAfter("src=\"").substringBefore("\"").replace("\\\\", "\\")
|
||||
if (url.contains("streamtape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url)?.let { videoList.add(it) }
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun unescapeJava(escaped: String): String {
|
||||
var escaped = escaped
|
||||
if (escaped.indexOf("\\u") == -1) return escaped
|
||||
var processed = ""
|
||||
var position = escaped.indexOf("\\u")
|
||||
while (position != -1) {
|
||||
if (position != 0) processed += escaped.substring(0, position)
|
||||
val token = escaped.substring(position + 2, position + 6)
|
||||
escaped = escaped.substring(position + 6)
|
||||
processed += token.toInt(16).toChar()
|
||||
position = escaped.indexOf("\\u")
|
||||
}
|
||||
processed += escaped
|
||||
return processed
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.selectFirst("#anime figure img.cover")!!.attr("abs:src")
|
||||
anime.title = document.selectFirst("#anime section hgroup h1")!!.text()
|
||||
anime.description = document.selectFirst("#anime section p.sinopsis")!!.text().removeSurrounding("\"")
|
||||
anime.genre = document.select("#anime section ul.tags li a").joinToString { it.text() }
|
||||
anime.status = parseStatus(document.select("div.main div section div.status-left div.cuerpo div:nth-child(2) span").text().trim())
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val orderByFilter = filters.find { it is OrderByFilter } as OrderByFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/buscar?q=$query&pag=$page&sort=${orderByFilter.toUriPart()}")
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genero/${genreFilter.toUriPart()}?pag=$page&sort=${orderByFilter.toUriPart()}")
|
||||
orderByFilter.state != 0 -> GET("$baseUrl/series?sort=${orderByFilter.toUriPart()}&pag=$page")
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
OrderByFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("<Selecionar>", ""),
|
||||
Pair("+18", "18"),
|
||||
Pair("Acción", "accion"),
|
||||
Pair("Animación", "animacion"),
|
||||
Pair("Arte", "arte"),
|
||||
Pair("Artes Marciales", "artes-marciales"),
|
||||
Pair("Aventura", "aventura"),
|
||||
Pair("Bizarro", "bizarro"),
|
||||
Pair("Carreras", "carreras"),
|
||||
Pair("Ciencia Ficción", "ciencia-ficcion"),
|
||||
Pair("Colegialas", "colegialas"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Concert", "concert"),
|
||||
Pair("Cyberpunk", "cyberpunk"),
|
||||
Pair("Demonios", "demonios"),
|
||||
Pair("Deportes", "deportes"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Escolares", "escolares"),
|
||||
Pair("Fantasía", "fantasia"),
|
||||
Pair("Fútbol", "futbol"),
|
||||
Pair("Game", "game"),
|
||||
Pair("Gore", "gore"),
|
||||
Pair("Guerra", "guerra"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Histórico", "historico"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Idol", "idol"),
|
||||
Pair("Infantil", "infantil"),
|
||||
Pair("invier", "invier"),
|
||||
Pair("Invierno 2013", "invierno-2013"),
|
||||
Pair("Invierno 2014", "invierno-2014"),
|
||||
Pair("Invierno 2015", "invierno-2015"),
|
||||
Pair("Invierno 2016", "invierno-2016"),
|
||||
Pair("Invierno 2017", "invierno-2017"),
|
||||
Pair("Invierno 2019", "invierno-2019"),
|
||||
Pair("Invierno 2020", "invierno-2020"),
|
||||
Pair("Invierno 2021", "invierno-2021"),
|
||||
Pair("Invierno 2022", "invierno-2022"),
|
||||
Pair("Invierno-2018", "invierno-2018"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Juegos", "juegos"),
|
||||
Pair("Juegos De Mesa", "juegos-de-mesa"),
|
||||
Pair("Kids", "kids"),
|
||||
Pair("Loli", "loli"),
|
||||
Pair("Lucha", "lucha"),
|
||||
Pair("Mafia", "mafia"),
|
||||
Pair("Magia", "magia"),
|
||||
Pair("Mahou Shōjo", "mahou-shojo"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Militar", "militar"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Música", "musica"),
|
||||
Pair("Otoño 2012", "otono-2012"),
|
||||
Pair("Otoño 2013", "otono-2013"),
|
||||
Pair("Otoño 2014", "otono-2014"),
|
||||
Pair("Otoño 2015", "otono-2015"),
|
||||
Pair("Otoño 2016", "otono-2016"),
|
||||
Pair("Otoño 2018", "otono-2018"),
|
||||
Pair("Otoño 2019", "otono-2019"),
|
||||
Pair("Otoño 2020", "otono-2020"),
|
||||
Pair("Otoño 2021", "otono-2021"),
|
||||
Pair("otono-2017", "otono-2017"),
|
||||
Pair("Pantsu", "pantsu"),
|
||||
Pair("Parodia", "parodia"),
|
||||
Pair("Policía", "policia"),
|
||||
Pair("Post Apocalitico", "post-apocalitico"),
|
||||
Pair("prima", "prima"),
|
||||
Pair("Primavera 2013", "primavera-2013"),
|
||||
Pair("Primavera 2014", "primavera-2014"),
|
||||
Pair("Primavera 2015", "primavera-2015"),
|
||||
Pair("Primavera 2016", "primavera-2016"),
|
||||
Pair("Primavera 2017", "primavera-2017"),
|
||||
Pair("primavera 2018", "primavera-2018"),
|
||||
Pair("Primavera 2019", "primavera-2019"),
|
||||
Pair("Primavera 2020", "primavera-2020"),
|
||||
Pair("Primavera 2021", "primavera-2021"),
|
||||
Pair("Primavera 2022", "primavera-2022"),
|
||||
Pair("Primvera 2018", "primvera-2018"),
|
||||
Pair("Psicológico", "psicologico"),
|
||||
Pair("Recuentos De La Vida", "recuentos-de-la-vida"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Samurai", "samurai"),
|
||||
Pair("Sci-Fi", "sci-fi"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shōjo", "shojo"),
|
||||
Pair("Shōjo-ai", "shojo-ai"),
|
||||
Pair("Shōnen", "shonen"),
|
||||
Pair("Shōnen-ai", "shonen-ai"),
|
||||
Pair("Shooter", "shooter"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shoujo Ai", "shoujo-ai"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("shounen ai", "shounen-ai"),
|
||||
Pair("Slice Of Life", "slice-of-life"),
|
||||
Pair("Sobrenatural", "sobrenatural"),
|
||||
Pair("Super poder", "super-poder"),
|
||||
Pair("Supernatural", "supernatural"),
|
||||
Pair("Suspenso", "suspenso"),
|
||||
Pair("Terror", "terror"),
|
||||
Pair("Thriller", "thriller"),
|
||||
Pair("Torneo", "torneo"),
|
||||
Pair("Tragedia", "tragedia"),
|
||||
Pair("Vampiros", "vampiros"),
|
||||
Pair("ver", "ver"),
|
||||
Pair("vera", "vera"),
|
||||
Pair("Verano 2013", "verano-2013"),
|
||||
Pair("Verano 2014", "verano-2014"),
|
||||
Pair("Verano 2015", "verano-2015"),
|
||||
Pair("Verano 2016", "verano-2016"),
|
||||
Pair("Verano 2017", "verano-2017"),
|
||||
Pair("Verano 2018", "verano-2018"),
|
||||
Pair("Verano 2019", "verano-2019"),
|
||||
Pair("Verano 2020", "verano-2020"),
|
||||
Pair("Verano 2021", "verano-2021"),
|
||||
Pair("Verano 2022", "verano-2022"),
|
||||
Pair("Violencia", "violencia"),
|
||||
Pair("Vocaloid", "vocaloid"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"Ordenar Por",
|
||||
arrayOf(
|
||||
Pair("<Seleccionar>", ""),
|
||||
Pair("Recientes", "newest"),
|
||||
Pair("A-Z", "asc"),
|
||||
Pair("Más vistos", "views"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when {
|
||||
statusString.contains("En emisión") -> SAnime.ONGOING
|
||||
statusString.contains("Finalizada") -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/series?sort=newest&pag=$page")
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("StreamTape")
|
||||
entryValues = arrayOf("StreamTape")
|
||||
setDefaultValue("StreamTape")
|
||||
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)
|
||||
}
|
||||
}
|
15
src/es/animelatinohd/build.gradle
Normal file
|
@ -0,0 +1,15 @@
|
|||
ext {
|
||||
extName = 'AnimeLatinoHD'
|
||||
extClass = '.AnimeLatinoHD'
|
||||
extVersionCode = 32
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
}
|
BIN
src/es/animelatinohd/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/es/animelatinohd/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/es/animelatinohd/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
src/es/animelatinohd/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/es/animelatinohd/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,435 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animelatinohd
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.es.animelatinohd.extractors.SolidFilesExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnimeLatinoHD : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeLatinoHD"
|
||||
|
||||
override val baseUrl = "https://www.animelatinohd.com"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "FileLions"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
|
||||
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
|
||||
"FileLions", "StreamHideVid", "SolidFiles", "Od.lk", "CldUp",
|
||||
)
|
||||
|
||||
private const val PREF_LANGUAGE_KEY = "preferred_language"
|
||||
private const val PREF_LANGUAGE_DEFAULT = "[LAT]"
|
||||
private val LANGUAGE_LIST = arrayOf("[LAT]", "[SUB]")
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animes/populares")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val url = response.request.url.toString().lowercase()
|
||||
val hasNextPage = document.select("#__next > main > div > div[class*=\"Animes_paginate\"] a:last-child svg").any()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("{\"props\":{\"pageProps\":")) {
|
||||
val jObject = json.decodeFromString<JsonObject>(script.data())
|
||||
val props = jObject["props"]!!.jsonObject
|
||||
val pageProps = props["pageProps"]!!.jsonObject
|
||||
val data = pageProps["data"]!!.jsonObject
|
||||
if (url.contains("status=1")) {
|
||||
val latestData = data["data"]!!.jsonArray
|
||||
latestData.forEach { item ->
|
||||
val animeItem = item!!.jsonObject
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
} else {
|
||||
val popularToday = data["popular_today"]!!.jsonArray
|
||||
popularToday.forEach { item ->
|
||||
val animeItem = item!!.jsonObject
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/animes?page=$page&status=1")
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
val newAnime = SAnime.create()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("{\"props\":{\"pageProps\":")) {
|
||||
val jObject = json.decodeFromString<JsonObject>(script.data())
|
||||
val props = jObject["props"]!!.jsonObject
|
||||
val pageProps = props["pageProps"]!!.jsonObject
|
||||
val data = pageProps["data"]!!.jsonObject
|
||||
|
||||
newAnime.title = data["name"]!!.jsonPrimitive!!.content
|
||||
newAnime.genre = data["genres"]!!.jsonPrimitive!!.content.split(",").joinToString()
|
||||
newAnime.description = data["overview"]!!.jsonPrimitive!!.content
|
||||
newAnime.status = parseStatus(data["status"]!!.jsonPrimitive!!.content)
|
||||
newAnime.thumbnail_url = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${data["poster"]!!.jsonPrimitive!!.content}"
|
||||
newAnime.setUrlWithoutDomain(externalOrInternalImg("anime/${data["slug"]!!.jsonPrimitive!!.content}"))
|
||||
}
|
||||
}
|
||||
return newAnime
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("{\"props\":{\"pageProps\":")) {
|
||||
val jObject = json.decodeFromString<JsonObject>(script.data())
|
||||
val props = jObject["props"]!!.jsonObject
|
||||
val pageProps = props["pageProps"]!!.jsonObject
|
||||
val data = pageProps["data"]!!.jsonObject
|
||||
val arrEpisode = data["episodes"]!!.jsonArray
|
||||
arrEpisode.forEach { item ->
|
||||
val animeItem = item!!.jsonObject
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(externalOrInternalImg("ver/${data["slug"]!!.jsonPrimitive!!.content}/${animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()}"))
|
||||
episode.episode_number = animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()
|
||||
episode.name = "Episodio ${animeItem["number"]!!.jsonPrimitive!!.content!!.toFloat()}"
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodeList
|
||||
}
|
||||
|
||||
private fun parseJsonArray(json: JsonElement?): List<JsonElement> {
|
||||
val list = mutableListOf<JsonElement>()
|
||||
json!!.jsonObject!!.entries!!.forEach { list.add(it.value) }
|
||||
return list
|
||||
}
|
||||
|
||||
private fun fetchUrls(text: String?): List<String> {
|
||||
if (text.isNullOrEmpty()) return listOf()
|
||||
val linkRegex = "(https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))".toRegex()
|
||||
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("{\"props\":{\"pageProps\":")) {
|
||||
val jObject = json.decodeFromString<JsonObject>(script.data())
|
||||
val props = jObject["props"]!!.jsonObject
|
||||
val pageProps = props["pageProps"]!!.jsonObject
|
||||
val data = pageProps["data"]!!.jsonObject
|
||||
val playersElement = data["players"]
|
||||
val players = if (playersElement !is JsonArray) JsonArray(parseJsonArray(playersElement)) else playersElement!!.jsonArray
|
||||
players.forEach { player ->
|
||||
val servers = player!!.jsonArray
|
||||
servers.forEach { server ->
|
||||
val item = server!!.jsonObject
|
||||
val request = client.newCall(
|
||||
GET(
|
||||
url = "https://api.animelatinohd.com/stream/${item["id"]!!.jsonPrimitive.content}",
|
||||
headers = headers.newBuilder()
|
||||
.add("Referer", "https://www.animelatinohd.com/")
|
||||
.add("authority", "api.animelatinohd.com")
|
||||
.add("upgrade-insecure-requests", "1")
|
||||
.build(),
|
||||
),
|
||||
).execute()
|
||||
val locationsDdh = request!!.networkResponse.toString()
|
||||
fetchUrls(locationsDdh).map { url ->
|
||||
val language = if (item["languaje"]!!.jsonPrimitive!!.content == "1") "[LAT]" else "[SUB]"
|
||||
val embedUrl = url.lowercase()
|
||||
if (embedUrl.contains("filemoon")) {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "$language Filemoon:", headers = vidHeaders).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "$language FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("streamtape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url, "$language Streamtape")?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("dood")) {
|
||||
DoodExtractor(client).videoFromUrl(url, "$language DoodStream")?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("okru") || embedUrl.contains("ok.ru")) {
|
||||
OkruExtractor(client).videosFromUrl(url, language).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("solidfiles")) {
|
||||
SolidFilesExtractor(client).videosFromUrl(url, language).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("od.lk")) {
|
||||
videoList.add(Video(url, language + "Od.lk", url))
|
||||
}
|
||||
if (embedUrl.contains("cldup.com")) {
|
||||
videoList.add(Video(url, language + "CldUp", url))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(lang) },
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val stateFilter = filterList.find { it is StateFilter } as StateFilter
|
||||
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
|
||||
|
||||
val filterUrl = if (query.isBlank()) {
|
||||
"$baseUrl/animes?page=$page&genre=${genreFilter.toUriPart()}&status=${stateFilter.toUriPart()}&type=${typeFilter.toUriPart()}"
|
||||
} else {
|
||||
"$baseUrl/animes?page=$page&search=$query"
|
||||
}
|
||||
|
||||
return GET(filterUrl)
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora los filtros"),
|
||||
GenreFilter(),
|
||||
StateFilter(),
|
||||
TypeFilter(),
|
||||
)
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val hasNextPage = document.select("#__next > main > div > div[class*=\"Animes_paginate\"] a:last-child svg").any()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("{\"props\":{\"pageProps\":")) {
|
||||
val jObject = json.decodeFromString<JsonObject>(script.data())
|
||||
val props = jObject["props"]!!.jsonObject
|
||||
val pageProps = props["pageProps"]!!.jsonObject
|
||||
val data = pageProps["data"]!!.jsonObject
|
||||
val arrData = data["data"]!!.jsonArray
|
||||
arrData.forEach { item ->
|
||||
val animeItem = item!!.jsonObject
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(externalOrInternalImg("anime/${animeItem["slug"]!!.jsonPrimitive!!.content}"))
|
||||
anime.thumbnail_url = "https://image.tmdb.org/t/p/w200${animeItem["poster"]!!.jsonPrimitive!!.content}"
|
||||
anime.title = animeItem["name"]!!.jsonPrimitive!!.content
|
||||
animeList.add(anime)
|
||||
}
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("<Selecionar>", ""),
|
||||
Pair("Acción", "accion"),
|
||||
Pair("Aliens", "aliens"),
|
||||
Pair("Artes Marciales", "artes-marciales"),
|
||||
Pair("Aventura", "aventura"),
|
||||
Pair("Ciencia Ficción", "ciencia-ficcion"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Cyberpunk", "cyberpunk"),
|
||||
Pair("Demonios", "demonios"),
|
||||
Pair("Deportes", "deportes"),
|
||||
Pair("Detectives", "detectives"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Escolar", "escolar"),
|
||||
Pair("Espacio", "espacio"),
|
||||
Pair("Fantasía", "fantasia"),
|
||||
Pair("Gore", "gore"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Histórico", "historico"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Juegos", "juegos"),
|
||||
Pair("Kodomo", "kodomo"),
|
||||
Pair("Magia", "magia"),
|
||||
Pair("Maho Shoujo", "maho-shoujo"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Militar", "militar"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Musica", "musica"),
|
||||
Pair("Parodia", "parodia"),
|
||||
Pair("Policial", "policial"),
|
||||
Pair("Psicológico", "psicologico"),
|
||||
Pair("Recuentos De La Vida", "recuentos-de-la-vida"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Samurais", "samurais"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shoujo Ai", "shoujo-ai"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Shounen Ai", "shounen-ai"),
|
||||
Pair("Sobrenatural", "sobrenatural"),
|
||||
Pair("Soft Hentai", "soft-hentai"),
|
||||
Pair("Super Poderes", "super-poderes"),
|
||||
Pair("Suspenso", "suspenso"),
|
||||
Pair("Terror", "terror"),
|
||||
Pair("Vampiros", "vampiros"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
),
|
||||
)
|
||||
|
||||
private class StateFilter : UriPartFilter(
|
||||
"Estado",
|
||||
arrayOf(
|
||||
Pair("Todos", ""),
|
||||
Pair("Finalizado", "0"),
|
||||
Pair("En emisión", "1"),
|
||||
),
|
||||
)
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Tipo",
|
||||
arrayOf(
|
||||
Pair("Todos", ""),
|
||||
Pair("Animes", "tv"),
|
||||
Pair("Películas", "movie"),
|
||||
Pair("Especiales", "special"),
|
||||
Pair("OVAS", "ova"),
|
||||
Pair("ONAS", "ona"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private fun externalOrInternalImg(url: String) = if (url.contains("https")) url else "$baseUrl/$url"
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when {
|
||||
statusString.contains("1") -> SAnime.ONGOING
|
||||
statusString.contains("0") -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANGUAGE_KEY
|
||||
title = "Preferred language"
|
||||
entries = LANGUAGE_LIST
|
||||
entryValues = LANGUAGE_LIST
|
||||
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_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animelatinohd.extractors
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.pow
|
||||
|
||||
// https://github.com/cylonu87/JsUnpacker
|
||||
class JsUnpacker(packedJS: String?) {
|
||||
private var packedJS: String? = null
|
||||
|
||||
/**
|
||||
* Detects whether the javascript is P.A.C.K.E.R. coded.
|
||||
*
|
||||
* @return true if it's P.A.C.K.E.R. coded.
|
||||
*/
|
||||
fun detect(): Boolean {
|
||||
val js = packedJS!!.replace(" ", "")
|
||||
val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]")
|
||||
val m = p.matcher(js)
|
||||
return m.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack the javascript
|
||||
*
|
||||
* @return the javascript unpacked or null.
|
||||
*/
|
||||
fun unpack(): String? {
|
||||
val js = packedJS
|
||||
runCatching {
|
||||
var p =
|
||||
Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL)
|
||||
var m = p.matcher(js)
|
||||
if (m.find() && m.groupCount() == 4) {
|
||||
val payload = m.group(1).replace("\\'", "'")
|
||||
val radixStr = m.group(2)
|
||||
val countStr = m.group(3)
|
||||
val symtab = m.group(4).split("\\|".toRegex()).toTypedArray()
|
||||
val radix = radixStr.toIntOrNull() ?: 36
|
||||
val count = countStr.toIntOrNull() ?: 0
|
||||
if (symtab.size != count) {
|
||||
throw Exception("Unknown p.a.c.k.e.r. encoding")
|
||||
}
|
||||
val unbase = Unbase(radix)
|
||||
p = Pattern.compile("\\b\\w+\\b")
|
||||
m = p.matcher(payload)
|
||||
val decoded = StringBuilder(payload)
|
||||
var replaceOffset = 0
|
||||
while (m.find()) {
|
||||
val word = m.group(0)
|
||||
val x = unbase.unbase(word)
|
||||
var value: String? = null
|
||||
if (x < symtab.size && x >= 0) {
|
||||
value = symtab[x]
|
||||
}
|
||||
if (value != null && value.isNotEmpty()) {
|
||||
decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value)
|
||||
replaceOffset += value.length - word.length
|
||||
}
|
||||
}
|
||||
return decoded.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private inner class Unbase(private val radix: Int) {
|
||||
private val alphabet62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private val alphabet95 =
|
||||
" !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
||||
private var alphabet: String? = null
|
||||
private var dictionary: HashMap<String, Int>? = null
|
||||
fun unbase(str: String): Int {
|
||||
var ret = 0
|
||||
if (alphabet == null) {
|
||||
ret = str.toInt(radix)
|
||||
} else {
|
||||
val tmp = StringBuilder(str).reverse().toString()
|
||||
for (i in tmp.indices) {
|
||||
ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring(i, i + 1)]!!).toInt()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
init {
|
||||
if (radix > 36) {
|
||||
when {
|
||||
radix < 62 -> {
|
||||
alphabet = alphabet62.substring(0, radix)
|
||||
}
|
||||
radix in 63..94 -> {
|
||||
alphabet = alphabet95.substring(0, radix)
|
||||
}
|
||||
radix == 62 -> {
|
||||
alphabet = alphabet62
|
||||
}
|
||||
radix == 95 -> {
|
||||
alphabet = alphabet95
|
||||
}
|
||||
}
|
||||
dictionary = HashMap(95)
|
||||
for (i in 0 until alphabet!!.length) {
|
||||
dictionary!![alphabet!!.substring(i, i + 1)] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param packedJS javascript P.A.C.K.E.R. coded.
|
||||
*/
|
||||
init {
|
||||
this.packedJS = packedJS
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val C =
|
||||
listOf(
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6d,
|
||||
0x2e,
|
||||
0x67,
|
||||
0x6f,
|
||||
0x6f,
|
||||
0x67,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x6e,
|
||||
0x64,
|
||||
0x72,
|
||||
0x6f,
|
||||
0x69,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x67,
|
||||
0x6d,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x64,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x4d,
|
||||
0x6f,
|
||||
0x62,
|
||||
0x69,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x41,
|
||||
0x64,
|
||||
0x73,
|
||||
)
|
||||
private val Z =
|
||||
listOf(
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6d,
|
||||
0x2e,
|
||||
0x66,
|
||||
0x61,
|
||||
0x63,
|
||||
0x65,
|
||||
0x62,
|
||||
0x6f,
|
||||
0x6f,
|
||||
0x6b,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x64,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x41,
|
||||
0x64,
|
||||
)
|
||||
|
||||
fun String.load(): String? {
|
||||
return try {
|
||||
var load = this
|
||||
|
||||
for (q in C.indices) {
|
||||
if (C[q % 4] > 270) {
|
||||
load += C[q % 3]
|
||||
} else {
|
||||
load += C[q].toChar()
|
||||
}
|
||||
}
|
||||
|
||||
Class.forName(load.substring(load.length - C.size, load.length)).name
|
||||
} catch (_: Exception) {
|
||||
try {
|
||||
var f = C[2].toChar().toString()
|
||||
for (w in Z.indices) {
|
||||
f += Z[w].toChar()
|
||||
}
|
||||
return Class.forName(f.substring(0b001, f.length)).name
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animelatinohd.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SolidFilesExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
return try {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
document.select("script").forEach { script ->
|
||||
if (script.data().contains("\"downloadUrl\":")) {
|
||||
val data = script.data().substringAfter("\"downloadUrl\":").substringBefore(",")
|
||||
val url = data.replace("\"", "")
|
||||
val videoUrl = url
|
||||
val quality = prefix + "SolidFiles"
|
||||
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||
}
|
||||
}
|
||||
videoList
|
||||
} catch (e: Exception) {
|
||||
videoList
|
||||
}
|
||||
}
|
||||
}
|
23
src/es/animemovil/build.gradle
Normal file
|
@ -0,0 +1,23 @@
|
|||
ext {
|
||||
extName = 'AnimeMovil'
|
||||
extClass = '.AnimeMovil'
|
||||
extVersionCode = 14
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:playlist-utils'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
}
|
BIN
src/es/animemovil/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/es/animemovil/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/es/animemovil/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/es/animemovil/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/es/animemovil/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6 KiB |
|
@ -0,0 +1,473 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animemovil
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLEncoder
|
||||
|
||||
class AnimeMovil : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeMovil"
|
||||
|
||||
override val baseUrl = "https://animemeow.xyz"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"PlusTube", "PlusVid", "PlusIm", "PlusWish", "PlusHub", "PlusDex",
|
||||
"YourUpload", "Voe", "StreamWish", "Mp4Upload", "Doodstream",
|
||||
"Uqload", "BurstCloud", "Upstream", "StreamTape", "PlusFilm",
|
||||
"Fastream", "FileLions",
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/directorio/?p=$page", headers)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val elements = document.select(".grid-animes article")
|
||||
val nextPage = document.select(".pagination .right:not(.disabledd) .page-link").any()
|
||||
val animeList = elements.map { element ->
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")?.attr("abs:href") ?: "")
|
||||
title = element.selectFirst("a > p")?.text() ?: ""
|
||||
thumbnail_url = element.selectFirst("a .main-img img")?.attr("abs:src") ?: ""
|
||||
status = when (element.select("a .figure-title > p").text().trim()) {
|
||||
"Finalizado" -> SAnime.COMPLETED
|
||||
"En emision" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, nextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val statusFilter = filterList.find { it is StatusFilter } as StatusFilter
|
||||
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
|
||||
val languageFilter = filterList.find { it is LanguageFilter } as LanguageFilter
|
||||
|
||||
val params = HashMap<String, String>()
|
||||
if (genreFilter.state != 0) {
|
||||
params["genero"] = genreFilter.toUriPart()
|
||||
}
|
||||
if (statusFilter.state != 0) {
|
||||
params["estado"] = statusFilter.toUriPart()
|
||||
}
|
||||
if (typeFilter.state != 0) {
|
||||
params["tipo"] = typeFilter.toUriPart()
|
||||
}
|
||||
if (languageFilter.state != 0) {
|
||||
params["idioma"] = languageFilter.toUriPart()
|
||||
}
|
||||
params["p"] = "$page"
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/directorio/?p=$page&q=$query", headers)
|
||||
else -> GET("$baseUrl/directorio/?${urlEncodeUTF8(params)}", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
return SAnime.create().apply {
|
||||
title = document.selectFirst(".banner-info div.titles h1")?.text() ?: ""
|
||||
description = document.select("#sinopsis").text()
|
||||
thumbnail_url = document.selectFirst("#anime_image")?.attr("abs:src")
|
||||
genre = document.select(".generos-wrap .item").joinToString { it.text() }
|
||||
status = when (document.select(".banner-img .estado").text().trim()) {
|
||||
"Finalizado" -> SAnime.COMPLETED
|
||||
"En emision" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
val document = response.asJsoup()
|
||||
val seasons = document.select(".temporadas-lista .btn-temporada")
|
||||
if (seasons.any()) {
|
||||
val token = try {
|
||||
response.headers.first { it.first == "set-cookie" && it.second.startsWith("csrftoken") }
|
||||
.second.substringAfter("=").substringBefore(";").replace("%3D", "=")
|
||||
} catch (_: Exception) { "" }
|
||||
seasons.reversed().map {
|
||||
val sid = it.attr("data-sid")
|
||||
val t = it.attr("data-t")
|
||||
|
||||
val mediaType = "application/json".toMediaType()
|
||||
val requestBody = "{\"show\": \"$sid\",\"temporada\": \"$t\"}"
|
||||
val request = Request.Builder()
|
||||
.url("https://animemeow.xyz/api/obtener_episodios_x_temporada/")
|
||||
.post(requestBody.toRequestBody(mediaType))
|
||||
.header("authority", response.request.url.host)
|
||||
.header("origin", "https://${response.request.url.host}")
|
||||
.header("referer", response.request.url.toString())
|
||||
.header("x-csrftoken", token)
|
||||
.header("x-requested-with", "XMLHttpRequest")
|
||||
.header("cookie", "csrftoken=$token")
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().asJsoup().let {
|
||||
json.decodeFromString<EpisodesDto>(it.body().text()).episodios.forEachIndexed { idx, it ->
|
||||
val episode = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(it.url)
|
||||
name = "T$t - " + it.epNombre.replace("Ver", "").trim()
|
||||
episode_number = idx.toFloat()
|
||||
}
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
document.select("#eps li > a").reversed().forEachIndexed { idx, it ->
|
||||
val nameEp = it.selectFirst("p")?.ownText() ?: ""
|
||||
val episode = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = nameEp.replace("Ver", "").trim()
|
||||
episode_number = idx.toFloat()
|
||||
}
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
|
||||
private fun fetchUrls(text: String?): List<String> {
|
||||
if (text.isNullOrEmpty()) return listOf()
|
||||
val linkRegex = "(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])".toRegex()
|
||||
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("#fuentes button").forEach {
|
||||
try {
|
||||
val url = it.attr("data-url").substringAfter("redirect.php?id=").trim()
|
||||
if (url.contains("php?id=")) {
|
||||
val serverName = it.ownText().trim()
|
||||
val serverDocument = client.newCall(GET(url)).execute().asJsoup()
|
||||
val fileData = serverDocument.selectFirst("script:containsData(sources: [{file:)")?.data() ?: ""
|
||||
val genericFiles = fetchUrls(fileData)
|
||||
if (genericFiles.any()) {
|
||||
genericFiles.forEach { fileSrc ->
|
||||
if (fileSrc.contains(".m3u8")) {
|
||||
videoList.add(Video(fileSrc, "$serverName:HLS", fileSrc, headers = null))
|
||||
}
|
||||
if (fileSrc.contains(".mp4")) {
|
||||
videoList.add(Video(fileSrc, "$serverName:MP4", fileSrc, headers = null))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverVideoResolver(url).let { videoList.addAll(it) }
|
||||
}
|
||||
} else {
|
||||
serverVideoResolver(url).let { videoList.addAll(it) }
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val embedUrl = url.lowercase()
|
||||
try {
|
||||
if (embedUrl.contains("voe")) {
|
||||
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:").also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("uqload")) {
|
||||
UqloadExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("mp4upload")) {
|
||||
val newHeaders = headers.newBuilder().add("referer", "https://re.animepelix.net/").build()
|
||||
Mp4uploadExtractor(client).videosFromUrl(url, newHeaders).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("wish")) {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
|
||||
DoodExtractor(client).videoFromUrl(url, "DoodStream", false)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamlare")) {
|
||||
StreamlareExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("yourupload")) {
|
||||
YourUploadExtractor(client).videoFromUrl(url, headers = headers).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
|
||||
BurstCloudExtractor(client).videoFromUrl(url, headers = headers).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("fastream")) {
|
||||
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("upstream")) {
|
||||
UpstreamExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("streamtape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url)?.also(videoList::add)
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
LanguageFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("Seleccionar", ""),
|
||||
Pair("Acción", "1"),
|
||||
Pair("Escolares", "2"),
|
||||
Pair("Romance", "3"),
|
||||
Pair("Shoujo", "4"),
|
||||
Pair("Comedia", "5"),
|
||||
Pair("Drama", "6"),
|
||||
Pair("Seinen", "7"),
|
||||
Pair("Deportes", "8"),
|
||||
Pair("Shounen", "9"),
|
||||
Pair("Recuentos de la vida", "10"),
|
||||
Pair("Ecchi", "11"),
|
||||
Pair("Sobrenatural", "12"),
|
||||
Pair("Fantasía", "13"),
|
||||
Pair("Magia", "14"),
|
||||
Pair("Superpoderes", "15"),
|
||||
Pair("Demencia", "16"),
|
||||
Pair("Misterio", "17"),
|
||||
Pair("Psicológico", "18"),
|
||||
Pair("Suspenso", "19"),
|
||||
Pair("Ciencia Ficción", "20"),
|
||||
Pair("Mecha", "21"),
|
||||
Pair("Militar", "22"),
|
||||
Pair("Aventuras", "23"),
|
||||
Pair("Historico", "24"),
|
||||
Pair("Infantil", "25"),
|
||||
Pair("Artes Marciales", "26"),
|
||||
Pair("Terror", "27"),
|
||||
Pair("Harem", "28"),
|
||||
Pair("Josei", "29"),
|
||||
Pair("Parodia", "30"),
|
||||
Pair("Policía", "31"),
|
||||
Pair("Juegos", "32"),
|
||||
Pair("Carreras", "33"),
|
||||
Pair("Samurai", "34"),
|
||||
Pair("Espacial", "35"),
|
||||
Pair("Música", "36"),
|
||||
Pair("Yuri", "37"),
|
||||
Pair("Demonios", "38"),
|
||||
Pair("Vampiros", "39"),
|
||||
Pair("Yaoi", "40"),
|
||||
Pair("Humor Negro", "41"),
|
||||
Pair("Crimen", "42"),
|
||||
Pair("Hentai", "43"),
|
||||
Pair("Youtuber", "44"),
|
||||
Pair("MaiNess Random", "45"),
|
||||
Pair("Donghua", "46"),
|
||||
Pair("Horror", "47"),
|
||||
Pair("Sin Censura", "48"),
|
||||
Pair("Gore", "49"),
|
||||
Pair("Live Action", "50"),
|
||||
Pair("Isekai", "51"),
|
||||
Pair("Gourmet", "52"),
|
||||
Pair("spokon", "53"),
|
||||
Pair("Zombies", "54"),
|
||||
),
|
||||
)
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Tipos",
|
||||
arrayOf(
|
||||
Pair("Seleccionar", ""),
|
||||
Pair("TV", "1"),
|
||||
Pair("Película", "2"),
|
||||
Pair("OVA", "3"),
|
||||
Pair("Especial", "4"),
|
||||
Pair("Serie", "9"),
|
||||
Pair("Dorama", "11"),
|
||||
Pair("Corto", "14"),
|
||||
Pair("Donghua", "15"),
|
||||
Pair("ONA", "16"),
|
||||
Pair("Live Action", "17"),
|
||||
Pair("Manhwa", "18"),
|
||||
Pair("Teatral", "19"),
|
||||
),
|
||||
)
|
||||
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"Estados",
|
||||
arrayOf(
|
||||
Pair("Seleccionar", ""),
|
||||
Pair("Finalizado", "1"),
|
||||
Pair("En emision", "2"),
|
||||
Pair("Proximamente", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
private class LanguageFilter : UriPartFilter(
|
||||
"Idioma",
|
||||
arrayOf(
|
||||
Pair("Seleccionar", ""),
|
||||
Pair("Japonés", "1"),
|
||||
Pair("Latino", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpisodesDto(
|
||||
val autenticado: Boolean,
|
||||
val episodios: List<Episodio>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Episodio(
|
||||
val id: Long,
|
||||
@SerialName("ep_nombre")
|
||||
val epNombre: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
private fun urlEncodeUTF8(s: String?): String? {
|
||||
return try {
|
||||
URLEncoder.encode(s, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlEncodeUTF8(map: Map<*, *>): String? {
|
||||
val sb = StringBuilder()
|
||||
for ((key, value) in map) {
|
||||
if (sb.isNotEmpty()) {
|
||||
sb.append("&")
|
||||
}
|
||||
sb.append(String.format("%s=%s", urlEncodeUTF8(key.toString()), urlEncodeUTF8(value.toString())))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
17
src/es/animeonlineninja/build.gradle
Normal file
|
@ -0,0 +1,17 @@
|
|||
ext {
|
||||
extName = 'AnimeOnline.Ninja'
|
||||
extClass = '.AnimeOnlineNinja'
|
||||
themePkg = 'dooplay'
|
||||
baseUrl = 'https://ww3.animeonline.ninja'
|
||||
overrideVersionCode = 38
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
}
|
BIN
src/es/animeonlineninja/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/es/animeonlineninja/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/es/animeonlineninja/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/es/animeonlineninja/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/es/animeonlineninja/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
|
@ -0,0 +1,266 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeonlineninja
|
||||
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
|
||||
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
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeOnlineNinja : DooPlay(
|
||||
"es",
|
||||
"AnimeOnline.Ninja",
|
||||
"https://ww3.animeonline.ninja",
|
||||
) {
|
||||
override val client by lazy {
|
||||
if (preferences.getBoolean(PREF_VRF_INTERCEPT_KEY, PREF_VRF_INTERCEPT_DEFAULT)) {
|
||||
network.client.newBuilder()
|
||||
.addInterceptor(VrfInterceptor())
|
||||
.build()
|
||||
} else {
|
||||
network.client
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/tendencias/$page")
|
||||
|
||||
override fun popularAnimeSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AnimeOnlineNinjaFilters.getSearchParameters(filters)
|
||||
val path = when {
|
||||
params.genre.isNotBlank() -> {
|
||||
if (params.genre in listOf("tendencias", "ratings")) {
|
||||
"/" + params.genre
|
||||
} else {
|
||||
"/genero/${params.genre}"
|
||||
}
|
||||
}
|
||||
params.language.isNotBlank() -> "/genero/${params.language}"
|
||||
params.year.isNotBlank() -> "/release/${params.year}"
|
||||
params.movie.isNotBlank() -> {
|
||||
if (params.movie == "pelicula") {
|
||||
"/pelicula"
|
||||
} else {
|
||||
"/genero/${params.movie}"
|
||||
}
|
||||
}
|
||||
else -> buildString {
|
||||
append(
|
||||
when {
|
||||
query.isNotBlank() -> "/?s=$query"
|
||||
params.letter.isNotBlank() -> "/letra/${params.letter}/?"
|
||||
else -> "/tendencias/?"
|
||||
},
|
||||
)
|
||||
|
||||
append(
|
||||
if (contains("tendencias")) {
|
||||
"&get=${when (params.type){
|
||||
"serie" -> "TV"
|
||||
"pelicula" -> "movies"
|
||||
else -> "todos"
|
||||
}}"
|
||||
} else {
|
||||
"&tipo=${params.type}"
|
||||
},
|
||||
)
|
||||
|
||||
if (params.isInverted) append("&orden=asc")
|
||||
}
|
||||
}
|
||||
|
||||
return if (path.startsWith("/?s=")) {
|
||||
GET("$baseUrl/page/$page$path")
|
||||
} else if (path.startsWith("/letra") || path.startsWith("/tendencias")) {
|
||||
val before = path.substringBeforeLast("/")
|
||||
val after = path.substringAfterLast("/")
|
||||
GET(baseUrl + before + "/page/$page/" + after)
|
||||
} else {
|
||||
GET("$baseUrl$path/page/$page")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override val episodeMovieText = "Película"
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val players = document.select("ul#playeroptionsul li")
|
||||
return players.flatMap { player ->
|
||||
val name = player.selectFirst("span.title")!!.text()
|
||||
val url = getPlayerUrl(player)
|
||||
extractVideos(url, name)
|
||||
}
|
||||
}
|
||||
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val mixDropExtractor by lazy { MixDropExtractor(client) }
|
||||
private val uqloadExtractor by lazy { UqloadExtractor(client) }
|
||||
|
||||
private fun extractVideos(url: String, lang: String): List<Video> {
|
||||
return when {
|
||||
"saidochesto.top" in url || "MULTISERVER" in lang.uppercase() ->
|
||||
extractFromMulti(url)
|
||||
"filemoon" in url ->
|
||||
filemoonExtractor.videosFromUrl(url, "$lang Filemoon - ", headers)
|
||||
"dood" in url ->
|
||||
doodExtractor.videoFromUrl(url, "$lang DoodStream", false)
|
||||
?.let(::listOf)
|
||||
"streamtape" in url ->
|
||||
streamTapeExtractor.videoFromUrl(url, "$lang StreamTape")
|
||||
?.let(::listOf)
|
||||
"mixdrop" in url ->
|
||||
mixDropExtractor.videoFromUrl(url, lang)
|
||||
"uqload" in url ->
|
||||
uqloadExtractor.videosFromUrl(url)
|
||||
"wolfstream" in url -> {
|
||||
client.newCall(GET(url, headers)).execute()
|
||||
.asJsoup()
|
||||
.selectFirst("script:containsData(sources)")
|
||||
?.data()
|
||||
?.let { jsData ->
|
||||
val videoUrl = jsData.substringAfter("{file:\"").substringBefore("\"")
|
||||
listOf(Video(videoUrl, "$lang WolfStream", videoUrl, headers = headers))
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: emptyList<Video>()
|
||||
}
|
||||
|
||||
private fun extractFromMulti(url: String): List<Video> {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val prefLang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
val langSelector = when {
|
||||
prefLang.isBlank() -> "div"
|
||||
else -> "div.OD_$prefLang"
|
||||
}
|
||||
return document.select("div.ODDIV $langSelector > li").flatMap {
|
||||
val hosterUrl = it.attr("onclick").toString()
|
||||
.substringAfter("('")
|
||||
.substringBefore("')")
|
||||
val lang = when (langSelector) {
|
||||
"div" -> {
|
||||
it.parent()?.attr("class").toString()
|
||||
.substringAfter("OD_", "")
|
||||
.substringBefore(" ")
|
||||
}
|
||||
else -> prefLang
|
||||
}
|
||||
extractVideos(hosterUrl, lang)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPlayerUrl(player: Element): String {
|
||||
val type = player.attr("data-type")
|
||||
val id = player.attr("data-post")
|
||||
val num = player.attr("data-nume")
|
||||
return client.newCall(GET("$baseUrl/wp-json/dooplayer/v1/post/$id?type=$type&source=$num"))
|
||||
.execute()
|
||||
.let { response ->
|
||||
response.body.string()
|
||||
.substringAfter("\"embed_url\":\"")
|
||||
.substringBefore("\",")
|
||||
.replace("\\", "")
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun Document.getDescription(): String {
|
||||
return select("$additionalInfoSelector div.wp-content p")
|
||||
.eachText()
|
||||
.joinToString("\n")
|
||||
}
|
||||
|
||||
override val additionalInfoItems = listOf("Título", "Temporadas", "Episodios", "Duración media")
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override val latestUpdatesPath = "episodio"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.pagination > *:last-child:not(span):not(.current)"
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override val fetchGenres = false
|
||||
|
||||
override fun getFilterList() = AnimeOnlineNinjaFilters.FILTER_LIST
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
super.setupPreferenceScreen(screen) // Quality preference
|
||||
|
||||
val langPref = ListPreference(screen.context).apply {
|
||||
key = PREF_LANG_KEY
|
||||
title = PREF_LANG_TITLE
|
||||
entries = PREF_LANG_ENTRIES
|
||||
entryValues = PREF_LANG_VALUES
|
||||
setDefaultValue(PREF_LANG_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()
|
||||
}
|
||||
}
|
||||
|
||||
val vrfIterceptPref = CheckBoxPreference(screen.context).apply {
|
||||
key = PREF_VRF_INTERCEPT_KEY
|
||||
title = PREF_VRF_INTERCEPT_TITLE
|
||||
summary = PREF_VRF_INTERCEPT_SUMMARY
|
||||
setDefaultValue(PREF_VRF_INTERCEPT_DEFAULT)
|
||||
}
|
||||
|
||||
screen.addPreference(vrfIterceptPref)
|
||||
screen.addPreference(langPref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun String.toDate() = 0L
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
|
||||
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(lang) },
|
||||
{ it.quality.contains(quality) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override val prefQualityValues = arrayOf("480p", "720p", "1080p")
|
||||
override val prefQualityEntries = prefQualityValues
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANG_KEY = "preferred_lang"
|
||||
private const val PREF_LANG_TITLE = "Preferred language"
|
||||
private const val PREF_LANG_DEFAULT = "SUB"
|
||||
private val PREF_LANG_ENTRIES = arrayOf("SUB", "All", "ES", "LAT")
|
||||
private val PREF_LANG_VALUES = arrayOf("SUB", "", "ES", "LAT")
|
||||
|
||||
private const val PREF_VRF_INTERCEPT_KEY = "vrf_intercept"
|
||||
private const val PREF_VRF_INTERCEPT_TITLE = "Intercept VRF links (Requiere Reiniciar)"
|
||||
private const val PREF_VRF_INTERCEPT_SUMMARY = "Intercept VRF links and open them in the browser"
|
||||
private const val PREF_VRF_INTERCEPT_DEFAULT = false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeonlineninja
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object AnimeOnlineNinjaFilters {
|
||||
|
||||
open class UriPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return first { it is R } as R
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asUriPart(): String {
|
||||
return getFirst<R>().let {
|
||||
(it as UriPartFilter).toUriPart()
|
||||
}
|
||||
}
|
||||
|
||||
class InvertedResultsFilter : AnimeFilter.CheckBox("Invertir resultados", false)
|
||||
class TypeFilter : UriPartFilter("Tipo", AnimesOnlineNinjaData.TYPES)
|
||||
class LetterFilter : UriPartFilter("Filtrar por letra", AnimesOnlineNinjaData.LETTERS)
|
||||
|
||||
class GenreFilter : UriPartFilter("Generos", AnimesOnlineNinjaData.GENRES)
|
||||
class LanguageFilter : UriPartFilter("Idiomas", AnimesOnlineNinjaData.LANGUAGES)
|
||||
class YearFilter : UriPartFilter("Año", AnimesOnlineNinjaData.YEARS)
|
||||
class MovieFilter : UriPartFilter("Peliculas", AnimesOnlineNinjaData.MOVIES)
|
||||
|
||||
class OtherOptionsGroup : AnimeFilter.Group<UriPartFilter>(
|
||||
"Otros filtros",
|
||||
listOf(
|
||||
GenreFilter(),
|
||||
LanguageFilter(),
|
||||
YearFilter(),
|
||||
MovieFilter(),
|
||||
),
|
||||
)
|
||||
|
||||
private inline fun <reified R> AnimeFilter.Group<UriPartFilter>.getItemUri(): String {
|
||||
return state.first { it is R }.toUriPart()
|
||||
}
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
InvertedResultsFilter(),
|
||||
TypeFilter(),
|
||||
LetterFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("Estos filtros no afectan a la busqueda por texto"),
|
||||
OtherOptionsGroup(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val isInverted: Boolean = false,
|
||||
val type: String = "",
|
||||
val letter: String = "",
|
||||
val genre: String = "",
|
||||
val language: String = "",
|
||||
val year: String = "",
|
||||
val movie: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
val others = filters.getFirst<OtherOptionsGroup>()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.getFirst<InvertedResultsFilter>().state,
|
||||
filters.asUriPart<TypeFilter>(),
|
||||
filters.asUriPart<LetterFilter>(),
|
||||
others.getItemUri<GenreFilter>(),
|
||||
others.getItemUri<LanguageFilter>(),
|
||||
others.getItemUri<YearFilter>(),
|
||||
others.getItemUri<MovieFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private object AnimesOnlineNinjaData {
|
||||
val EVERY = Pair("Seleccionar", "")
|
||||
|
||||
val TYPES = arrayOf(
|
||||
Pair("Todos", "todos"),
|
||||
Pair("Series", "serie"),
|
||||
Pair("Peliculas", "pelicula"),
|
||||
)
|
||||
|
||||
val LETTERS = arrayOf(EVERY) + ('a'..'z').map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray()
|
||||
|
||||
val GENRES = arrayOf(
|
||||
EVERY,
|
||||
Pair("Sin Censura \uD83D\uDD1E", "sin-censura"),
|
||||
Pair("En emisión ⏩", "en-emision"),
|
||||
Pair("Blu-Ray / DVD \uD83D\uDCC0", "blu-ray-dvd"),
|
||||
Pair("Próximamente", "proximamente"),
|
||||
Pair("Live Action \uD83C\uDDEF\uD83C\uDDF5", "live-action"),
|
||||
Pair("Popular en la web \uD83D\uDCAB", "tendencias"),
|
||||
Pair("Mejores valorados ⭐", "ratings"),
|
||||
)
|
||||
|
||||
val LANGUAGES = arrayOf(
|
||||
EVERY,
|
||||
Pair("Audio Latino \uD83C\uDDF2\uD83C\uDDFD", "audio-latino"),
|
||||
Pair("Audio Castellano \uD83C\uDDEA\uD83C\uDDF8", "anime-castellano"),
|
||||
)
|
||||
|
||||
val YEARS = arrayOf(EVERY) + (2024 downTo 1979).map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray()
|
||||
|
||||
val MOVIES = arrayOf(
|
||||
EVERY,
|
||||
Pair("Anime ㊗️", "pelicula"),
|
||||
Pair("Live Action \uD83C\uDDEF\uD83C\uDDF5", "live-action"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeonlineninja
|
||||
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class VrfInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val respBody = response.body.string()
|
||||
if (response.headers["Content-Type"]?.contains("image") == true) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
val body = if (respBody.contains("One moment, please")) {
|
||||
val parsed = Jsoup.parse(respBody)
|
||||
val js = parsed.selectFirst("script:containsData(west=)")!!.data()
|
||||
val west = js.substringAfter("west=").substringBefore(",")
|
||||
val east = js.substringAfter("east=").substringBefore(",")
|
||||
val form = parsed.selectFirst("form#wsidchk-form")!!.attr("action")
|
||||
val eval = evalJs(west, east)
|
||||
val getLink = "https://" + request.url.host + form + "?wsidchk=$eval"
|
||||
chain.proceed(GET(getLink)).body
|
||||
} else {
|
||||
respBody.toResponseBody(response.body.contentType())
|
||||
}
|
||||
return response.newBuilder().body(body).build()
|
||||
}
|
||||
|
||||
private fun evalJs(west: String, east: String): String {
|
||||
return QuickJs.create().use { qjs ->
|
||||
val jscript = """$west + $east;"""
|
||||
qjs.evaluate(jscript).toString()
|
||||
}
|
||||
}
|
||||
}
|
10
src/es/animeyt/build.gradle
Normal file
|
@ -0,0 +1,10 @@
|
|||
ext {
|
||||
extName = 'Animeyt'
|
||||
extClass = '.Animeyt'
|
||||
extVersionCode = 10
|
||||
}
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
}
|
BIN
src/es/animeyt/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/es/animeyt/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/es/animeyt/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/es/animeyt/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
src/es/animeyt/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 54 KiB |
|
@ -0,0 +1,188 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeyt
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.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.lib.fastreamextractor.FastreamExtractor
|
||||
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
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class Animeyt : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeYT"
|
||||
|
||||
override val baseUrl = "https://ytanime.tv"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Fastream"
|
||||
private val SERVER_LIST = arrayOf("Fastream")
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.video-block div.row div.col-md-2 div.video-card"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/mas-populares?page=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("div.video-card div.video-card-body div.video-title a").attr("href"))
|
||||
anime.title = element.select("div.video-card div.video-card-body div.video-title a").text()
|
||||
anime.thumbnail_url = element.select("div.video-card div.video-card-image a:nth-child(2) img").attr("src")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item:last-child a"
|
||||
|
||||
override fun episodeListSelector() = "#caps ul.list-group li.list-group-item a"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
val epNum = getNumberFromEpsString(element.select("span.sa-series-link__number").text())
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
val epParsed = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
episode.episode_number = epParsed
|
||||
episode.name = "Episodio $epParsed"
|
||||
return episode
|
||||
}
|
||||
|
||||
private fun getNumberFromEpsString(epsStr: String): String {
|
||||
return epsStr.filter { it.isDigit() }
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("#plays iframe").forEach { container ->
|
||||
val server = container.attr("src")
|
||||
.split(".")[0]
|
||||
.replace("https://", "")
|
||||
.replace("http://", "")
|
||||
|
||||
var url = container.attr("src")
|
||||
if (server == "fastream") {
|
||||
if (url.contains("emb.html")) {
|
||||
val key = url.split("/").last()
|
||||
url = "https://fastream.to/embed-$key.html"
|
||||
}
|
||||
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/search?q=$query&page=$page")
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
return popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.selectFirst("div.sa-series-dashboard__poster div.sa-layout__line.sa-layout__line--sm div figure.sa-poster__fig img")!!.attr("src")
|
||||
anime.title = document.selectFirst("#info div.sa-layout__line div div.sa-title-series__title span")!!.html()
|
||||
anime.description = document.selectFirst("#info div.sa-layout__line p.sa-text")!!.text().removeSurrounding("\"")
|
||||
// anime.genre = document.select("nav.Nvgnrs a").joinToString { it.text() }
|
||||
anime.status = parseStatus(document.select("#info > div:nth-child(2) > button").text())
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when {
|
||||
statusString.contains("En Emisión") -> SAnime.ONGOING
|
||||
statusString.contains("Finalizado") -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ultimos-animes?page=$page")
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
15
src/es/animeytes/build.gradle
Normal file
|
@ -0,0 +1,15 @@
|
|||
ext {
|
||||
extName = 'AnimeYT.es'
|
||||
extClass = '.AnimeYTES'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animeyt.es'
|
||||
overrideVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:okru-extractor"))
|
||||
implementation(project(":lib:streamtape-extractor"))
|
||||
implementation(project(":lib:sendvid-extractor"))
|
||||
}
|
BIN
src/es/animeytes/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
src/es/animeytes/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/es/animeytes/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/es/animeytes/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
src/es/animeytes/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,29 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.animeytes
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
|
||||
class AnimeYTES : AnimeStream(
|
||||
"es",
|
||||
"AnimeYT.es",
|
||||
"https://animeyt.es",
|
||||
) {
|
||||
override val animeListUrl = "$baseUrl/tv"
|
||||
|
||||
// ============================ Video Links =============================
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
return when (name) {
|
||||
"OK" -> okruExtractor.videosFromUrl(url)
|
||||
"Stream" -> streamtapeExtractor.videosFromUrl(url)
|
||||
"Send" -> sendvidExtractor.videosFromUrl(url)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
27
src/es/asialiveaction/build.gradle
Normal file
|
@ -0,0 +1,27 @@
|
|||
ext {
|
||||
extName = 'AsiaLiveAction'
|
||||
extClass = '.AsiaLiveAction'
|
||||
extVersionCode = 30
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vudeo-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation(project(':lib:streamhidevid-extractor'))
|
||||
implementation(project(':lib:vk-extractor'))
|
||||
}
|
BIN
src/es/asialiveaction/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/es/asialiveaction/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/es/asialiveaction/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
src/es/asialiveaction/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/es/asialiveaction/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,357 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.asialiveaction
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.es.asialiveaction.extractors.VidGuardExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.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.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
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
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
|
||||
class AsiaLiveAction : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AsiaLiveAction"
|
||||
|
||||
override val baseUrl = "https://asialiveaction.com"
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "FileLions"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape",
|
||||
"Fastream", "Filemoon", "StreamWish", "VidGuard",
|
||||
"Amazon", "AmazonES", "Fireload", "FileLions",
|
||||
"vk.com",
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.TpRwCont main section ul.MovieList li.TPostMv article.TPost"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/todos/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.title = element.select("a h3.Title").text()
|
||||
anime.thumbnail_url = element.select("a div.Image figure img").attr("src").trim().replace("//", "https://")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.TpRwCont main div a.next.page-numbers"
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.selectFirst("header div.Image figure img")!!.attr("src").trim().replace("//", "https://")
|
||||
anime.title = document.selectFirst("header div.asia-post-header h1.Title")!!.text()
|
||||
anime.description = document.selectFirst("header div.asia-post-main div.Description p:nth-child(2), header div.asia-post-main div.Description p")!!.text().removeSurrounding("\"")
|
||||
anime.genre = document.select("div.asia-post-main p.Info span.tags a").joinToString { it.text() }
|
||||
val year = document.select("header div.asia-post-main p.Info span.Date a").text().toInt()
|
||||
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||
anime.status = when {
|
||||
year < currentYear -> SAnime.COMPLETED
|
||||
year == currentYear -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
return super.episodeListParse(response).reversed()
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "#ep-list div.TPTblCn span a, #ep-list div.TPTblCn .accordion"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
return if (element.attr("class").contains("accordion")) {
|
||||
val epNum = getNumberFromEpsString(element.select("label span").text())
|
||||
SEpisode.create().apply {
|
||||
name = element.select("label span").text().trim()
|
||||
episode_number = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
setUrlWithoutDomain(element.selectFirst("ul li a")?.attr("abs:href")!!)
|
||||
}
|
||||
} else {
|
||||
val epNum = getNumberFromEpsString(element.select("div.flex-grow-1 p").text())
|
||||
SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
episode_number = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
name = element.select("div.flex-grow-1 p").text().trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNumberFromEpsString(epsStr: String): String {
|
||||
return epsStr.filter { it.isDigit() }
|
||||
}
|
||||
|
||||
private fun fetchUrls(text: String?): List<String> {
|
||||
if (text.isNullOrEmpty()) return listOf()
|
||||
val linkRegex = "(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])".toRegex()
|
||||
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("script:containsData(var videos)").forEach { script ->
|
||||
fetchUrls(script.data()).map { url ->
|
||||
try {
|
||||
serverVideoResolver(url).also(videoList::addAll)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val embedUrl = url.lowercase()
|
||||
try {
|
||||
if (embedUrl.contains("voe")) {
|
||||
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if ((embedUrl.contains("amazon") || embedUrl.contains("amz")) && !embedUrl.contains("disable")) {
|
||||
val body = client.newCall(GET(url)).execute().asJsoup()
|
||||
if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
|
||||
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
|
||||
.substringAfter("shareId = \"").substringBefore("\"")
|
||||
val amazonApiJson = client.newCall(GET("https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"))
|
||||
.execute().asJsoup()
|
||||
val epId = amazonApiJson.toString().substringAfter("\"id\":\"").substringBefore("\"")
|
||||
val amazonApi =
|
||||
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
|
||||
.execute().asJsoup()
|
||||
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
|
||||
videoList.add(Video(videoUrl, "Amazon", videoUrl))
|
||||
}
|
||||
}
|
||||
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:", headers = vidHeaders).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("uqload")) {
|
||||
UqloadExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("mp4upload")) {
|
||||
Mp4uploadExtractor(client).videosFromUrl(url, headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("wishembed") ||
|
||||
embedUrl.contains("streamwish") ||
|
||||
embedUrl.contains("strwish") ||
|
||||
embedUrl.contains("wish") ||
|
||||
embedUrl.contains("sfastwish")
|
||||
) {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "StreamWish:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
|
||||
val url2 = url.replace("https://doodstream.com/e/", "https://dood.to/e/")
|
||||
DoodExtractor(client).videoFromUrl(url2, "DoodStream", false)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamlare")) {
|
||||
StreamlareExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("yourupload") || embedUrl.contains("upload")) {
|
||||
YourUploadExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
|
||||
BurstCloudExtractor(client).videoFromUrl(url, headers = headers).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("fastream")) {
|
||||
FastreamExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("upstream")) {
|
||||
UpstreamExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("ahvsh") || embedUrl.contains("streamhide") || embedUrl.contains("hide")) {
|
||||
StreamHideVidExtractor(client).videosFromUrl(url).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion") || embedUrl.contains("fviplions")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("vembed") || embedUrl.contains("guard")) {
|
||||
VidGuardExtractor(client).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("vk")) {
|
||||
VkExtractor(client, headers).videosFromUrl(url).also(videoList::addAll)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$query")
|
||||
genreFilter.state != 0 -> GET("$baseUrl/tag/${genreFilter.toUriPart()}/page/$page")
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("<Selecionar>", "all"),
|
||||
Pair("Acción", "accion"),
|
||||
Pair("Aventura", "aventura"),
|
||||
Pair("Ciencia Ficción", "ciencia-ficcion"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Deporte", "deporte"),
|
||||
Pair("Erótico", "erotico"),
|
||||
Pair("Escolar", "escolar"),
|
||||
Pair("Extraterrestres", "extraterrestres"),
|
||||
Pair("Fantasía", "fantasia"),
|
||||
Pair("Histórico", "historico"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Lucha", "lucha"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Música", "musica"),
|
||||
Pair("Psicológico", "psicologico"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Sobrenatural", "sobrenatural"),
|
||||
Pair("Yaoi / BL", "yaoi-bl"),
|
||||
Pair("Yuri / GL", "yuri-gl"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
return popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.asialiveaction.extractors
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class VidGuardExtractor(private val client: OkHttpClient) {
|
||||
private val context: Application by injectLazy()
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
class JsObject(private val latch: CountDownLatch) {
|
||||
var payload: String = ""
|
||||
|
||||
@JavascriptInterface
|
||||
fun passPayload(passedPayload: String) {
|
||||
payload = passedPayload
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun videosFromUrl(url: String): List<Video> {
|
||||
val doc = client.newCall(GET(url)).execute().asJsoup()
|
||||
val scriptUrl = doc.selectFirst("script[src*=ad/plugin]")
|
||||
?.absUrl("src")
|
||||
?: return emptyList()
|
||||
|
||||
val headers = Headers.headersOf("Referer", url)
|
||||
val script = client.newCall(GET(scriptUrl, headers)).execute()
|
||||
.body.string()
|
||||
|
||||
val sources = getSourcesFromScript(script, url)
|
||||
.takeIf { it.isNotBlank() && it != "undefined" }
|
||||
?: return emptyList()
|
||||
|
||||
return sources.substringAfter("stream:[").substringBefore("}]")
|
||||
.split('{')
|
||||
.drop(1)
|
||||
.mapNotNull { line ->
|
||||
val resolution = line.substringAfter("Label\":\"").substringBefore('"')
|
||||
val videoUrl = line.substringAfter("URL\":\"").substringBefore('"')
|
||||
.takeIf(String::isNotBlank)
|
||||
?.let(::fixUrl)
|
||||
?: return@mapNotNull null
|
||||
Video(videoUrl, "VidGuard:$resolution", videoUrl, headers)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSourcesFromScript(script: String, url: String): String {
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
var webView: WebView? = null
|
||||
|
||||
val jsinterface = JsObject(latch)
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
with(webview.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
}
|
||||
|
||||
webview.addJavascriptInterface(jsinterface, "android")
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
view?.clearCache(true)
|
||||
view?.clearFormData()
|
||||
view?.evaluateJavascript(script) {}
|
||||
view?.evaluateJavascript("window.android.passPayload(JSON.stringify(window.svg))") {}
|
||||
}
|
||||
}
|
||||
|
||||
webview.loadDataWithBaseURL(url, "<html></html>", "text/html", "UTF-8", null)
|
||||
}
|
||||
|
||||
latch.await(5, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
return jsinterface.payload
|
||||
}
|
||||
|
||||
private fun fixUrl(url: String): String {
|
||||
val httpUrl = url.toHttpUrl()
|
||||
val originalSign = httpUrl.queryParameter("sig")!!
|
||||
val newSign = originalSign.chunked(2).joinToString("") {
|
||||
Char(it.toInt(16) xor 2).toString()
|
||||
}
|
||||
.let { String(Base64.decode(it, Base64.DEFAULT)) }
|
||||
.substring(5)
|
||||
.chunked(2)
|
||||
.reversed()
|
||||
.joinToString("")
|
||||
.substring(5)
|
||||
|
||||
return httpUrl.newBuilder()
|
||||
.removeAllQueryParameters("sig")
|
||||
.addQueryParameter("sig", newSign)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
}
|
7
src/es/beatzanime/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'BeatZ Anime'
|
||||
extClass = '.BeatZAnime'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/es/beatzanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/es/beatzanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/es/beatzanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/es/beatzanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
src/es/beatzanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,281 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.beatzanime
|
||||
|
||||
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.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class BeatZAnime : ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "BeatZ Anime"
|
||||
|
||||
override val baseUrl = "https://www.beatz-anime.net"
|
||||
|
||||
private val indexHost = "dd.beatz-anime.net"
|
||||
private val indexHttpUrl = "https://$indexHost".toHttpUrl()
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val url = if (page > 1) {
|
||||
"$baseUrl/emision/pagina=$page"
|
||||
} else {
|
||||
"$baseUrl/emision/"
|
||||
}
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = ".row > div:has(a.titulo-largo)"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||
with(element.selectFirst("a.titulo-largo")!!) {
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
title = text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active + li:not(.disabled)"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = if (page > 1) {
|
||||
"$baseUrl/index.php?pagina=$page"
|
||||
} else {
|
||||
"$baseUrl/"
|
||||
}
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val source = filters.filterIsInstance<SourceFilter>().first().getValue()
|
||||
val status = filters.filterIsInstance<StatusFilter>().first().getValue()
|
||||
val type = filters.filterIsInstance<TypeFilter>().first().getValue()
|
||||
|
||||
val url = "$baseUrl/lista-animes/index.php"
|
||||
|
||||
val formBody = FormBody.Builder().apply {
|
||||
add("buscar", query)
|
||||
add("fuente", source)
|
||||
add("estado", status)
|
||||
add("tipo-anime", type)
|
||||
}.build()
|
||||
|
||||
val formHeaders = headersBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Origin", baseUrl)
|
||||
add("Referer", url)
|
||||
}.build()
|
||||
|
||||
return POST(url, formHeaders, formBody)
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = ".row > div:has(span.titulo)"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||
with(element.selectFirst("a:has(span)")!!) {
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
title = text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? = null
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
SourceFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
)
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
title = document.selectFirst("h1")!!.text()
|
||||
thumbnail_url = document.selectFirst(".row > div > img")?.imgAttr()
|
||||
genre = document.selectFirst("p.post-text span:has(b:contains(Generos))")?.ownText()
|
||||
status = document.selectFirst("div:has(>h5:contains(Estado)) a").parseStatus()
|
||||
description = buildString {
|
||||
document.selectFirst("p.post-text")?.textNodes()?.let {
|
||||
append(it.joinToString("\n\n") { it.text() })
|
||||
}
|
||||
append("\n\n")
|
||||
document.selectFirst("p.post-text span:has(b:contains(Sinónimos))")?.let {
|
||||
append("Sinónimos: ")
|
||||
append(it.ownText())
|
||||
}
|
||||
}.trim()
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"finalizado" -> SAnime.COMPLETED
|
||||
"en emisión", "en emsión" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
|
||||
val indexUrlRaw = document.selectFirst("a[href*=$indexHost]")!!.attr("abs:href").toHttpUrl()
|
||||
val indexUrl = if (indexUrlRaw.encodedPath.contains("api/raw/")) {
|
||||
val path = indexUrlRaw.queryParameter("path")!!.substringAfter("/")
|
||||
.substringBefore("/")
|
||||
"https://$indexHost/$path/"
|
||||
} else {
|
||||
indexUrlRaw.toString()
|
||||
}
|
||||
|
||||
fun traverseFolder(basePath: String, relativePath: String, recursionDepth: Int = 0) {
|
||||
if (recursionDepth == 2) return
|
||||
|
||||
val apiHeaders = headersBuilder().apply {
|
||||
add("Accept", "application/json, text/plain, */*")
|
||||
add("Host", indexHost)
|
||||
add(
|
||||
"Referer",
|
||||
indexHttpUrl.newBuilder()
|
||||
.addPathSegments(basePath)
|
||||
.build()
|
||||
.toString(),
|
||||
)
|
||||
}.build()
|
||||
|
||||
val apiUrl = indexHttpUrl.newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addPathSegment("")
|
||||
addQueryParameter("path", basePath)
|
||||
}.build()
|
||||
|
||||
val data = client.newCall(
|
||||
GET(apiUrl, apiHeaders),
|
||||
).execute().parseAs<IndexResponseDto>()
|
||||
|
||||
data.folder.value.forEach { item ->
|
||||
if (item.folder != null) {
|
||||
traverseFolder("$basePath/${item.name}", item.name, recursionDepth + 1)
|
||||
} else if (item.file != null) {
|
||||
val fileExt = item.name.substringAfterLast(".")
|
||||
if (!SUPPORTED_FORMATS.any { it.equals(fileExt, true) }) return@forEach
|
||||
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = item.name
|
||||
url = "$basePath/${item.name}"
|
||||
scanlator = buildList {
|
||||
if (relativePath != "") add(relativePath)
|
||||
add(item.size.formatBytes())
|
||||
}.joinToString(" • ")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFolder("/${indexUrl.toHttpUrl().pathSegments.first()}", "")
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class IndexResponseDto(
|
||||
val folder: FolderDto,
|
||||
) {
|
||||
@Serializable
|
||||
class FolderDto(
|
||||
val value: List<ItemDto>,
|
||||
) {
|
||||
@Serializable
|
||||
class ItemDto(
|
||||
val name: String,
|
||||
val size: Long,
|
||||
val folder: JsonObject? = null,
|
||||
val file: JsonObject? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.formatBytes(): String = when {
|
||||
this >= 1_000_000_000 -> "%.2f GB".format(this / 1_000_000_000.0)
|
||||
this >= 1_000_000 -> "%.2f MB".format(this / 1_000_000.0)
|
||||
this >= 1_000 -> "%.2f KB".format(this / 1_000.0)
|
||||
this > 1 -> "$this bytes"
|
||||
this == 1L -> "$this byte"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val url = indexHttpUrl.newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addPathSegment("raw")
|
||||
addPathSegment("")
|
||||
addQueryParameter("path", episode.url)
|
||||
}.build().toString()
|
||||
|
||||
val path = episode.url.substringAfter("/").substringBeforeLast("/") + "/"
|
||||
|
||||
val videoHeaders = headersBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
add("Referer", indexHttpUrl.newBuilder().addPathSegments(path).build().toString())
|
||||
}.build()
|
||||
|
||||
return listOf(Video(url, "Video", url, videoHeaders))
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SUPPORTED_FORMATS = listOf("mp4", "mkv")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.beatzanime
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : AnimeFilter.Select<String>(
|
||||
name,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
) {
|
||||
fun getValue(): String {
|
||||
return vals[state].second
|
||||
}
|
||||
}
|
||||
|
||||
class SourceFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("Todos", ""),
|
||||
Pair("BDRip", "BDRip"),
|
||||
Pair("WebRip", "WebRip"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : UriPartFilter(
|
||||
"Estado",
|
||||
arrayOf(
|
||||
Pair("Todos", ""),
|
||||
Pair("En Emision", "En Emision"),
|
||||
Pair("Finalizado", "Finalizado"),
|
||||
Pair("En Proceso", "En Proceso"),
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : UriPartFilter(
|
||||
"Tipo",
|
||||
arrayOf(
|
||||
Pair("Todos", ""),
|
||||
Pair("Serie", "Serie"),
|
||||
Pair("Pelicula", "Pelicula"),
|
||||
),
|
||||
)
|
26
src/es/cuevana/build.gradle
Normal file
|
@ -0,0 +1,26 @@
|
|||
ext {
|
||||
extName = 'Cuevana'
|
||||
extClass = '.CuevanaFactory'
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vudeo-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/es/cuevana/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/es/cuevana/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/es/cuevana/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/es/cuevana/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/es/cuevana/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,398 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cuevana
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.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.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
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
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class CuevanaCh(override val name: String, override val baseUrl: String) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANGUAGE_KEY = "preferred_language"
|
||||
private const val PREF_LANGUAGE_DEFAULT = "[LAT]"
|
||||
private val LANGUAGE_LIST = arrayOf("[LAT]", "[SUB]", "[CAST]")
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
|
||||
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
|
||||
"Tomatomatela",
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = ".MovieList .TPostMv .TPost"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/peliculas?page=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
anime.title = element.select("a .Title").text()
|
||||
anime.thumbnail_url = element.select("a .Image figure.Objf img").attr("abs:data-src")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "nav.navigation > div.nav-links > a.next.page-numbers"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
val document = response.asJsoup()
|
||||
if (response.request.url.toString().contains("/serie/")) {
|
||||
document.select("[id*=season-]").reversed().mapIndexed { idxSeason, season ->
|
||||
val noSeason = try {
|
||||
season.attr("id").substringAfter("season-").toInt()
|
||||
} catch (e: Exception) {
|
||||
idxSeason
|
||||
}
|
||||
season.select(".TPostMv article.TPost").reversed().mapIndexed { idxCap, cap ->
|
||||
val epNum = try {
|
||||
cap.select("a div.Image span.Year").text().substringAfter("x").toFloat()
|
||||
} catch (e: Exception) {
|
||||
idxCap.toFloat()
|
||||
}
|
||||
val episode = SEpisode.create()
|
||||
val date = cap.select("a > p").text()
|
||||
val epDate = try {
|
||||
SimpleDateFormat("yyyy-MM-dd").parse(date)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
episode.episode_number = epNum
|
||||
episode.name = "T$noSeason - Episodio $epNum"
|
||||
if (epDate != null) episode.date_upload = epDate.time
|
||||
episode.setUrlWithoutDomain(cap.select("a").attr("href"))
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create().apply {
|
||||
episode_number = 1f
|
||||
name = "PELÍCULA"
|
||||
}
|
||||
episode.setUrlWithoutDomain(response.request.url.toString())
|
||||
episodes.add(episode)
|
||||
}
|
||||
return episodes.reversed()
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
document.select("ul.anime_muti_link li").map {
|
||||
val langPrefix = try {
|
||||
val languageTag = it.selectFirst(".cdtr span")!!.text()
|
||||
if (languageTag.lowercase().contains("latino")) {
|
||||
"[LAT]"
|
||||
} else if (languageTag.lowercase().contains("castellano")) {
|
||||
"[CAST]"
|
||||
} else if (languageTag.lowercase().contains("subtitulado")) {
|
||||
"[SUB]"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
val url = it.attr("abs:data-video")
|
||||
try {
|
||||
serverVideoResolver(url, langPrefix).also(videoList::addAll)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val embedUrl = url.lowercase()
|
||||
try {
|
||||
if (embedUrl.contains("voe")) {
|
||||
VoeExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
|
||||
}
|
||||
if ((embedUrl.contains("amazon") || embedUrl.contains("amz")) && !embedUrl.contains("disable")) {
|
||||
val body = client.newCall(GET(url)).execute().asJsoup()
|
||||
if (body.select("script:containsData(var shareId)").toString().isNotBlank()) {
|
||||
val shareId = body.selectFirst("script:containsData(var shareId)")!!.data()
|
||||
.substringAfter("shareId = \"").substringBefore("\"")
|
||||
val amazonApiJson = client.newCall(GET("https://www.amazon.com/drive/v1/shares/$shareId?resourceVersion=V2&ContentType=JSON&asset=ALL"))
|
||||
.execute().asJsoup()
|
||||
val epId = amazonApiJson.toString().substringAfter("\"id\":\"").substringBefore("\"")
|
||||
val amazonApi =
|
||||
client.newCall(GET("https://www.amazon.com/drive/v1/nodes/$epId/children?resourceVersion=V2&ContentType=JSON&limit=200&sort=%5B%22kind+DESC%22%2C+%22modifiedDate+DESC%22%5D&asset=ALL&tempLink=true&shareId=$shareId"))
|
||||
.execute().asJsoup()
|
||||
val videoUrl = amazonApi.toString().substringAfter("\"FOLDER\":").substringAfter("tempLink\":\"").substringBefore("\"")
|
||||
videoList.add(Video(videoUrl, "$prefix Amazon", videoUrl))
|
||||
}
|
||||
}
|
||||
if (embedUrl.contains("ok.ru") || embedUrl.contains("okru")) {
|
||||
OkruExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "$prefix Filemoon:", headers = vidHeaders).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("uqload")) {
|
||||
UqloadExtractor(client).videosFromUrl(url, prefix = prefix).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("mp4upload")) {
|
||||
Mp4uploadExtractor(client).videosFromUrl(url, headers, prefix = prefix).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("strwish") || embedUrl.contains("wish")) {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://streamwish.to")
|
||||
.add("Referer", "https://streamwish.to/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" }).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
|
||||
val url2 = url.replace("https://doodstream.com/e/", "https://dood.to/e/")
|
||||
DoodExtractor(client).videoFromUrl(url2, "$prefix DoodStream", false)?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamlare")) {
|
||||
StreamlareExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("yourupload") || embedUrl.contains("upload")) {
|
||||
YourUploadExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
|
||||
BurstCloudExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("fastream")) {
|
||||
FastreamExtractor(client, headers).videosFromUrl(url, prefix = "$prefix Fastream:").also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("upstream")) {
|
||||
UpstreamExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
|
||||
}
|
||||
if (embedUrl.contains("streamtape") || embedUrl.contains("stp") || embedUrl.contains("stape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url, quality = "$prefix StreamTape")?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("tomatomatela")) {
|
||||
runCatching {
|
||||
val mainUrl = url.substringBefore("/embed.html#").substringAfter("https://")
|
||||
val headers = headers.newBuilder()
|
||||
.set("authority", mainUrl)
|
||||
.set("accept", "application/json, text/javascript, */*; q=0.01")
|
||||
.set("accept-language", "es-MX,es-419;q=0.9,es;q=0.8,en;q=0.7")
|
||||
.set("sec-ch-ua", "\"Chromium\";v=\"106\", \"Google Chrome\";v=\"106\", \"Not;A=Brand\";v=\"99\"")
|
||||
.set("sec-ch-ua-mobile", "?0")
|
||||
.set("sec-ch-ua-platform", "Windows")
|
||||
.set("sec-fetch-dest", "empty")
|
||||
.set("sec-fetch-mode", "cors")
|
||||
.set("sec-fetch-site", "same-origin")
|
||||
.set("x-requested-with", "XMLHttpRequest")
|
||||
.build()
|
||||
val token = url.substringAfter("/embed.html#")
|
||||
val urlRequest = "https://$mainUrl/details.php?v=$token"
|
||||
val response = client.newCall(GET(urlRequest, headers = headers)).execute().asJsoup()
|
||||
val bodyText = response.select("body").text()
|
||||
val json = json.decodeFromString<JsonObject>(bodyText)
|
||||
val status = json["status"]!!.jsonPrimitive!!.content
|
||||
val file = json["file"]!!.jsonPrimitive!!.content
|
||||
if (status == "200") { videoList.add(Video(file, "$prefix Tomatomatela", file, headers = null)) }
|
||||
}
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "$prefix FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/search.html?keyword=$query&page=$page", headers)
|
||||
genreFilter.state != 0 -> GET("$baseUrl/category/${genreFilter.toUriPart()}?page=$page")
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
return popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.selectFirst(".TPost header .Title")!!.text()
|
||||
anime.thumbnail_url = document.selectFirst(".backdrop article div.Image figure img")!!.attr("abs:data-src")
|
||||
anime.description = document.selectFirst(".backdrop article.TPost div.Description")!!.text().trim()
|
||||
anime.genre = document.select("ul.InfoList li:nth-child(1) > a").joinToString { it.text() }
|
||||
anime.status = SAnime.UNKNOWN
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Tipos",
|
||||
arrayOf(
|
||||
Pair("<selecionar>", ""),
|
||||
Pair("Acción", "accion"),
|
||||
Pair("Animación", "animacion"),
|
||||
Pair("Aventura", "aventura"),
|
||||
Pair("Bélico Guerra", "belico-guerra"),
|
||||
Pair("Biográfia", "biografia"),
|
||||
Pair("Ciencia Ficción", "ciencia-ficcion"),
|
||||
Pair("Comedia", "comedia"),
|
||||
Pair("Crimen", "crimen"),
|
||||
Pair("Documentales", "documentales"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Familiar", "familiar"),
|
||||
Pair("Fantasía", "fantasia"),
|
||||
Pair("Misterio", "misterio"),
|
||||
Pair("Musical", "musical"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Terror", "terror"),
|
||||
Pair("Thriller", "thriller"),
|
||||
),
|
||||
)
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(lang) },
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANGUAGE_KEY
|
||||
title = "Preferred language"
|
||||
entries = LANGUAGE_LIST
|
||||
entryValues = LANGUAGE_LIST
|
||||
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_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cuevana
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.es.cuevana.models.AnimeEpisodesList
|
||||
import eu.kanade.tachiyomi.animeextension.es.cuevana.models.PopularAnimeList
|
||||
import eu.kanade.tachiyomi.animeextension.es.cuevana.models.Videos
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelMapBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
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
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class CuevanaEu(override val name: String, override val baseUrl: String) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANGUAGE_KEY = "preferred_language"
|
||||
private const val PREF_LANGUAGE_DEFAULT = "[LAT]"
|
||||
private val LANGUAGE_LIST = arrayOf("[LAT]", "[ENG]", "[CAST]", "[JAP]")
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"Tomatomatela", "YourUpload", "Doodstream", "Okru",
|
||||
"Voe", "StreamTape", "StreamWish", "Filemoon",
|
||||
"FileLions",
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String = ".MovieList .TPostMv .TPost"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/peliculas/estrenos/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val hasNextPage = document.select("nav.navigation > div.nav-links > a.next.page-numbers").any()
|
||||
val script = document.selectFirst("script:containsData({\"props\":{\"pageProps\":{)")!!.data()
|
||||
|
||||
val responseJson = json.decodeFromString<PopularAnimeList>(script)
|
||||
responseJson.props?.pageProps?.movies?.map { animeItem ->
|
||||
val anime = SAnime.create()
|
||||
val preSlug = animeItem.url?.slug ?: ""
|
||||
val type = if (preSlug.startsWith("series")) "ver-serie" else "ver-pelicula"
|
||||
|
||||
anime.title = animeItem.titles?.name ?: ""
|
||||
anime.thumbnail_url = animeItem.images?.poster?.replace("/original/", "/w200/") ?: ""
|
||||
anime.description = animeItem.overview
|
||||
anime.setUrlWithoutDomain("/$type/${animeItem.slug?.name}")
|
||||
animeList.add(anime)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "uwu"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
val document = response.asJsoup()
|
||||
if (response.request.url.toString().contains("/ver-serie/")) {
|
||||
val script = document.selectFirst("script:containsData({\"props\":{\"pageProps\":{)")!!.data()
|
||||
val responseJson = json.decodeFromString<AnimeEpisodesList>(script)
|
||||
responseJson.props?.pageProps?.thisSerie?.seasons?.map {
|
||||
it.episodes.map { ep ->
|
||||
val episode = SEpisode.create()
|
||||
val epDate = try {
|
||||
ep.releaseDate?.substringBefore("T")?.let { date -> SimpleDateFormat("yyyy-MM-dd").parse(date) }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (epDate != null) episode.date_upload = epDate.time
|
||||
episode.name = "T${ep.slug?.season} - Episodio ${ep.slug?.episode}"
|
||||
episode.episode_number = ep.number?.toFloat()!!
|
||||
episode.setUrlWithoutDomain("/episodio/${ep.slug?.name}-temporada-${ep.slug?.season}-episodio-${ep.slug?.episode}")
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create().apply {
|
||||
episode_number = 1f
|
||||
name = "PELÍCULA"
|
||||
}
|
||||
episode.setUrlWithoutDomain(response.request.url.toString())
|
||||
episodes.add(episode)
|
||||
}
|
||||
return episodes.reversed()
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val videoList = mutableListOf<Video>()
|
||||
val script = document.selectFirst("script:containsData({\"props\":{\"pageProps\":{)")!!.data()
|
||||
val responseJson = json.decodeFromString<AnimeEpisodesList>(script)
|
||||
if (response.request.url.toString().contains("/episodio/")) {
|
||||
serverIterator(responseJson.props?.pageProps?.episode?.videos).let {
|
||||
videoList.addAll(it)
|
||||
}
|
||||
} else {
|
||||
serverIterator(responseJson.props?.pageProps?.thisMovie?.videos).let {
|
||||
videoList.addAll(it)
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun serverIterator(videos: Videos?): MutableList<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
videos?.latino?.parallelMapBlocking {
|
||||
try {
|
||||
val body = client.newCall(GET(it.result!!)).execute().asJsoup()
|
||||
val url = body.selectFirst("script:containsData(var message)")?.data()?.substringAfter("var url = '")?.substringBefore("'") ?: ""
|
||||
loadExtractor(url, "[LAT]").let { videoList.addAll(it) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
videos?.spanish?.map {
|
||||
try {
|
||||
val body = client.newCall(GET(it.result!!)).execute().asJsoup()
|
||||
val url = body.selectFirst("script:containsData(var message)")?.data()?.substringAfter("var url = '")?.substringBefore("'") ?: ""
|
||||
loadExtractor(url, "[CAST]").let { videoList.addAll(it) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
videos?.english?.map {
|
||||
try {
|
||||
val body = client.newCall(GET(it.result!!)).execute().asJsoup()
|
||||
val url = body.selectFirst("script:containsData(var message)")?.data()?.substringAfter("var url = '")?.substringBefore("'") ?: ""
|
||||
loadExtractor(url, "[ENG]").let { videoList.addAll(it) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
videos?.japanese?.map {
|
||||
val body = client.newCall(GET(it.result!!)).execute().asJsoup()
|
||||
val url = body.selectFirst("script:containsData(var message)")?.data()?.substringAfter("var url = '")?.substringBefore("'") ?: ""
|
||||
loadExtractor(url, "[JAP]").let { videoList.addAll(it) }
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun loadExtractor(url: String, prefix: String = ""): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val embedUrl = url.lowercase()
|
||||
if (embedUrl.contains("tomatomatela")) {
|
||||
try {
|
||||
val mainUrl = url.substringBefore("/embed.html#").substringAfter("https://")
|
||||
val headers = headers.newBuilder()
|
||||
.set("authority", mainUrl)
|
||||
.set("accept", "application/json, text/javascript, */*; q=0.01")
|
||||
.set("accept-language", "es-MX,es-419;q=0.9,es;q=0.8,en;q=0.7")
|
||||
.set("sec-ch-ua", "\"Chromium\";v=\"106\", \"Google Chrome\";v=\"106\", \"Not;A=Brand\";v=\"99\"")
|
||||
.set("sec-ch-ua-mobile", "?0")
|
||||
.set("sec-ch-ua-platform", "Windows")
|
||||
.set("sec-fetch-dest", "empty")
|
||||
.set("sec-fetch-mode", "cors")
|
||||
.set("sec-fetch-site", "same-origin")
|
||||
.set("x-requested-with", "XMLHttpRequest")
|
||||
.build()
|
||||
val token = url.substringAfter("/embed.html#")
|
||||
val urlRequest = "https://$mainUrl/details.php?v=$token"
|
||||
val response = client.newCall(GET(urlRequest, headers = headers)).execute().asJsoup()
|
||||
val bodyText = response.select("body").text()
|
||||
val json = json.decodeFromString<JsonObject>(bodyText)
|
||||
val status = json["status"]!!.jsonPrimitive!!.content
|
||||
val file = json["file"]!!.jsonPrimitive!!.content
|
||||
if (status == "200") { videoList.add(Video(file, "$prefix Tomatomatela", file, headers = null)) }
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
if (embedUrl.contains("yourupload")) {
|
||||
val videos = YourUploadExtractor(client).videoFromUrl(url, headers = headers)
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
|
||||
DoodExtractor(client).videoFromUrl(url, "$prefix DoodStream", false)
|
||||
?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("okru") || embedUrl.contains("ok.ru")) {
|
||||
OkruExtractor(client).videosFromUrl(url, prefix, true).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("voe")) {
|
||||
VoeExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("streamtape")) {
|
||||
StreamTapeExtractor(client).videoFromUrl(url, "$prefix StreamTape")?.let { videoList.add(it) }
|
||||
}
|
||||
if (embedUrl.contains("wishembed") || embedUrl.contains("streamwish") || embedUrl.contains("wish")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url) { "$prefix StreamWish:$it" }
|
||||
.also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
|
||||
FilemoonExtractor(client).videosFromUrl(url, "$prefix Filemoon:").also(videoList::addAll)
|
||||
}
|
||||
if (embedUrl.contains("filelions") || embedUrl.contains("lion")) {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "$prefix FileLions:$it" }).also(videoList::addAll)
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(lang) },
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/search?q=$query", headers)
|
||||
genreFilter.state != 0 -> GET("$baseUrl/${genreFilter.toUriPart()}/page/$page")
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun animeDetailsParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
val newAnime = SAnime.create()
|
||||
val script = document.selectFirst("script:containsData({\"props\":{\"pageProps\":{)")!!.data()
|
||||
val responseJson = json.decodeFromString<AnimeEpisodesList>(script)
|
||||
if (response.request.url.toString().contains("/ver-serie/")) {
|
||||
val data = responseJson.props?.pageProps?.thisSerie
|
||||
newAnime.status = SAnime.UNKNOWN
|
||||
newAnime.title = data?.titles?.name ?: ""
|
||||
newAnime.description = data?.overview ?: ""
|
||||
newAnime.thumbnail_url = data?.images?.poster?.replace("/original/", "/w500/")
|
||||
newAnime.genre = data?.genres?.joinToString { it.name ?: "" }
|
||||
newAnime.artist = data?.cast?.acting?.firstOrNull()?.name ?: ""
|
||||
newAnime.setUrlWithoutDomain(response.request.url.toString())
|
||||
} else {
|
||||
val data = responseJson.props?.pageProps?.thisMovie
|
||||
newAnime.status = SAnime.UNKNOWN
|
||||
newAnime.title = data?.titles?.name ?: ""
|
||||
newAnime.description = data?.overview ?: ""
|
||||
newAnime.thumbnail_url = data?.images?.poster?.replace("/original/", "/w500/")
|
||||
newAnime.genre = data?.genres?.joinToString { it.name ?: "" }
|
||||
newAnime.artist = data?.cast?.acting?.firstOrNull()?.name ?: ""
|
||||
newAnime.setUrlWithoutDomain(response.request.url.toString())
|
||||
}
|
||||
|
||||
return newAnime
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Tipos",
|
||||
arrayOf(
|
||||
Pair("<selecionar>", ""),
|
||||
Pair("Series", "series/estrenos"),
|
||||
Pair("Acción", "genero/accion"),
|
||||
Pair("Aventura", "genero/aventura"),
|
||||
Pair("Animación", "genero/animacion"),
|
||||
Pair("Ciencia Ficción", "genero/ciencia-ficcion"),
|
||||
Pair("Comedia", "genero/comedia"),
|
||||
Pair("Crimen", "genero/crimen"),
|
||||
Pair("Documentales", "genero/documental"),
|
||||
Pair("Drama", "genero/drama"),
|
||||
Pair("Familia", "genero/familia"),
|
||||
Pair("Fantasía", "genero/fantasia"),
|
||||
Pair("Misterio", "genero/misterio"),
|
||||
Pair("Romance", "genero/romance"),
|
||||
Pair("Suspenso", "genero/suspense"),
|
||||
Pair("Terror", "genero/terror"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANGUAGE_KEY
|
||||
title = "Preferred language"
|
||||
entries = LANGUAGE_LIST
|
||||
entryValues = LANGUAGE_LIST
|
||||
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_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cuevana
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||
|
||||
class CuevanaFactory : AnimeSourceFactory {
|
||||
override fun createSources(): List<AnimeSource> = listOf(
|
||||
CuevanaCh("Cuevana3Ch", "https://ww1.cuevana3.ch"),
|
||||
CuevanaEu("Cuevana3Eu", "https://www.cuevana3.eu"),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cuevana.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AnimeEpisodesList(
|
||||
@SerialName("props") var props: Props? = Props(),
|
||||
@SerialName("page") var page: String? = null,
|
||||
@SerialName("query") var query: Query? = Query(),
|
||||
@SerialName("buildId") var buildId: String? = null,
|
||||
@SerialName("isFallback") var isFallback: Boolean? = null,
|
||||
@SerialName("gsp") var gsp: Boolean? = null,
|
||||
@SerialName("locale") var locale: String? = null,
|
||||
@SerialName("locales") var locales: ArrayList<String> = arrayListOf(),
|
||||
@SerialName("defaultLocale") var defaultLocale: String? = null,
|
||||
@SerialName("scriptLoader") var scriptLoader: ArrayList<String> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Episodes(
|
||||
@SerialName("title") var title: String? = null,
|
||||
@SerialName("TMDbId") var TMDbId: String? = null,
|
||||
@SerialName("number") var number: Int? = null,
|
||||
@SerialName("releaseDate") var releaseDate: String? = null,
|
||||
@SerialName("image") var image: String? = null,
|
||||
@SerialName("url") var url: Url? = Url(),
|
||||
@SerialName("slug") var slug: Slug? = Slug(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Seasons(
|
||||
@SerialName("number") var number: Int? = null,
|
||||
@SerialName("episodes") var episodes: ArrayList<Episodes> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Original(
|
||||
@SerialName("name") var name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ThisSerie(
|
||||
@SerialName("TMDbId") var TMDbId: String? = null,
|
||||
@SerialName("seasons") var seasons: ArrayList<Seasons> = arrayListOf(),
|
||||
@SerialName("titles") var titles: Titles? = Titles(),
|
||||
@SerialName("images") var images: Images? = Images(),
|
||||
@SerialName("overview") var overview: String? = null,
|
||||
@SerialName("genres") var genres: ArrayList<Genres> = arrayListOf(),
|
||||
@SerialName("cast") var cast: Cast? = Cast(),
|
||||
@SerialName("rate") var rate: Rate? = Rate(),
|
||||
@SerialName("url") var url: Url? = Url(),
|
||||
@SerialName("slug") var slug: Slug? = Slug(),
|
||||
@SerialName("releaseDate") var releaseDate: String? = null,
|
||||
)
|
|
@ -0,0 +1,176 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.cuevana.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularAnimeList(
|
||||
@SerialName("props") var props: Props? = Props(),
|
||||
@SerialName("page") var page: String? = null,
|
||||
@SerialName("query") var query: Query? = Query(),
|
||||
@SerialName("buildId") var buildId: String? = null,
|
||||
@SerialName("isFallback") var isFallback: Boolean? = null,
|
||||
@SerialName("gsp") var gsp: Boolean? = null,
|
||||
@SerialName("locale") var locale: String? = null,
|
||||
@SerialName("locales") var locales: ArrayList<String> = arrayListOf(),
|
||||
@SerialName("defaultLocale") var defaultLocale: String? = null,
|
||||
@SerialName("scriptLoader") var scriptLoader: ArrayList<String> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Titles(
|
||||
@SerialName("name") var name: String? = null,
|
||||
@SerialName("original") var original: Original? = Original(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Images(
|
||||
@SerialName("poster") var poster: String? = null,
|
||||
@SerialName("backdrop") var backdrop: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Rate(
|
||||
@SerialName("average") var average: Double? = null,
|
||||
@SerialName("votes") var votes: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Genres(
|
||||
@SerialName("id") var id: String? = null,
|
||||
@SerialName("slug") var slug: String? = null,
|
||||
@SerialName("name") var name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Acting(
|
||||
@SerialName("id") var id: String? = null,
|
||||
@SerialName("name") var name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Cast(
|
||||
@SerialName("acting") var acting: ArrayList<Acting> = arrayListOf(),
|
||||
@SerialName("directing") var directing: ArrayList<Directing> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Url(
|
||||
@SerialName("slug") var slug: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Slug(
|
||||
@SerialName("name") var name: String? = null,
|
||||
@SerialName("season") var season: String? = null,
|
||||
@SerialName("episode") var episode: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Movies(
|
||||
@SerialName("titles") var titles: Titles? = Titles(),
|
||||
@SerialName("images") var images: Images? = Images(),
|
||||
@SerialName("rate") var rate: Rate? = Rate(),
|
||||
@SerialName("overview") var overview: String? = null,
|
||||
@SerialName("TMDbId") var TMDbId: String? = null,
|
||||
@SerialName("genres") var genres: ArrayList<Genres> = arrayListOf(),
|
||||
@SerialName("cast") var cast: Cast? = Cast(),
|
||||
@SerialName("runtime") var runtime: Int? = null,
|
||||
@SerialName("releaseDate") var releaseDate: String? = null,
|
||||
@SerialName("url") var url: Url? = Url(),
|
||||
@SerialName("slug") var slug: Slug? = Slug(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageProps(
|
||||
@SerialName("thisSerie") var thisSerie: ThisSerie? = ThisSerie(),
|
||||
@SerialName("thisMovie") var thisMovie: ThisMovie? = ThisMovie(),
|
||||
@SerialName("movies") var movies: ArrayList<Movies> = arrayListOf(),
|
||||
@SerialName("pages") var pages: Int? = null,
|
||||
@SerialName("season") var season: Season? = Season(),
|
||||
@SerialName("episode") var episode: Episode? = Episode(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Props(
|
||||
@SerialName("pageProps") var pageProps: PageProps? = PageProps(),
|
||||
@SerialName("__N_SSG") var _NSSG: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Query(
|
||||
@SerialName("page") var page: String? = null,
|
||||
@SerialName("serie") var serie: String? = null,
|
||||
@SerialName("movie") var movie: String? = null,
|
||||
@SerialName("episode") var episode: String? = null,
|
||||
@SerialName("q") var q: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Directing(
|
||||
@SerialName("name") var name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Server(
|
||||
@SerialName("cyberlocker") var cyberlocker: String? = null,
|
||||
@SerialName("result") var result: String? = null,
|
||||
@SerialName("quality") var quality: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Videos(
|
||||
@SerialName("latino") var latino: ArrayList<Server> = arrayListOf(),
|
||||
@SerialName("spanish") var spanish: ArrayList<Server> = arrayListOf(),
|
||||
@SerialName("english") var english: ArrayList<Server> = arrayListOf(),
|
||||
@SerialName("japanese") var japanese: ArrayList<Server> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Downloads(
|
||||
@SerialName("cyberlocker") var cyberlocker: String? = null,
|
||||
@SerialName("result") var result: String? = null,
|
||||
@SerialName("quality") var quality: String? = null,
|
||||
@SerialName("language") var language: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ThisMovie(
|
||||
@SerialName("TMDbId") var TMDbId: String? = null,
|
||||
@SerialName("titles") var titles: Titles? = Titles(),
|
||||
@SerialName("images") var images: Images? = Images(),
|
||||
@SerialName("overview") var overview: String? = null,
|
||||
@SerialName("runtime") var runtime: Int? = null,
|
||||
@SerialName("genres") var genres: ArrayList<Genres> = arrayListOf(),
|
||||
@SerialName("cast") var cast: Cast? = Cast(),
|
||||
@SerialName("rate") var rate: Rate? = Rate(),
|
||||
@SerialName("url") var url: Url? = Url(),
|
||||
@SerialName("slug") var slug: Slug? = Slug(),
|
||||
@SerialName("releaseDate") var releaseDate: String? = null,
|
||||
@SerialName("videos") var videos: Videos? = Videos(),
|
||||
@SerialName("downloads") var downloads: ArrayList<Downloads> = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Season(
|
||||
@SerialName("number") var number: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NextEpisode(
|
||||
@SerialName("title") var title: String? = null,
|
||||
@SerialName("slug") var slug: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Episode(
|
||||
@SerialName("TMDbId") var TMDbId: String? = null,
|
||||
@SerialName("title") var title: String? = null,
|
||||
@SerialName("number") var number: Int? = null,
|
||||
@SerialName("image") var image: String? = null,
|
||||
@SerialName("url") var url: Url? = Url(),
|
||||
@SerialName("slug") var slug: Slug? = Slug(),
|
||||
@SerialName("nextEpisode") var nextEpisode: NextEpisode? = NextEpisode(),
|
||||
@SerialName("previousEpisode") var previousEpisode: String? = null,
|
||||
@SerialName("videos") var videos: Videos? = Videos(),
|
||||
@SerialName("downloads") var downloads: ArrayList<Downloads> = arrayListOf(),
|
||||
)
|
26
src/es/doramasflix/build.gradle
Normal file
|
@ -0,0 +1,26 @@
|
|||
ext {
|
||||
extName = 'Doramasflix'
|
||||
extClass = '.Doramasflix'
|
||||
extVersionCode = 21
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:vudeo-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:burstcloud-extractor'))
|
||||
implementation(project(':lib:fastream-extractor'))
|
||||
implementation(project(':lib:upstream-extractor'))
|
||||
implementation(project(':lib:streamhidevid-extractor'))
|
||||
}
|
BIN
src/es/doramasflix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/es/doramasflix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/es/doramasflix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/es/doramasflix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/es/doramasflix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,193 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.doramasflix
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// -----------------------Season models------------------------//
|
||||
@Serializable
|
||||
data class SeasonModel(
|
||||
val data: DataSeason = DataSeason(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DataSeason(
|
||||
val listSeasons: List<ListSeason> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ListSeason(
|
||||
val slug: String,
|
||||
@SerialName("season_number")
|
||||
val seasonNumber: Long,
|
||||
@SerialName("poster_path")
|
||||
val posterPath: String?,
|
||||
@SerialName("air_date")
|
||||
val airDate: String?,
|
||||
@SerialName("serie_name")
|
||||
val serieName: String?,
|
||||
val poster: String?,
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
// -----------------------Episode Model------------------------//
|
||||
@Serializable
|
||||
data class EpisodeModel(
|
||||
val data: DataEpisode = DataEpisode(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DataEpisode(
|
||||
val listEpisodes: List<ListEpisode> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ListEpisode(
|
||||
@SerialName("_id")
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val slug: String,
|
||||
@SerialName("serie_name")
|
||||
val serieName: String?,
|
||||
@SerialName("serie_name_es")
|
||||
val serieNameEs: String?,
|
||||
@SerialName("air_date")
|
||||
val airDate: String?,
|
||||
@SerialName("season_number")
|
||||
val seasonNumber: Long?,
|
||||
@SerialName("episode_number")
|
||||
val episodeNumber: Long?,
|
||||
val poster: String?,
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
// -----------------------Pagination Model------------------------//
|
||||
|
||||
@Serializable
|
||||
data class PaginationModel(
|
||||
val data: DataPagination = DataPagination(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DataPagination(
|
||||
val paginationDorama: PaginationDorama? = null,
|
||||
val paginationMovie: PaginationDorama? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PaginationDorama(
|
||||
val count: Long,
|
||||
val pageInfo: PageInfo,
|
||||
val items: List<Item> = emptyList(),
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageInfo(
|
||||
val currentPage: Long,
|
||||
val hasNextPage: Boolean,
|
||||
val hasPreviousPage: Boolean,
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Item(
|
||||
@SerialName("_id")
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerialName("name_es")
|
||||
val nameEs: String?,
|
||||
val slug: String,
|
||||
val names: String?,
|
||||
val overview: String?,
|
||||
@SerialName("poster_path")
|
||||
val posterPath: String?,
|
||||
val poster: String?,
|
||||
val genres: List<Genre> = emptyList(),
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Genre(
|
||||
val name: String?,
|
||||
val slug: String?,
|
||||
@SerialName("__typename")
|
||||
val typename: String?,
|
||||
)
|
||||
|
||||
// -----------------------Search Model------------------------//
|
||||
@Serializable
|
||||
data class SearchModel(
|
||||
val data: Data = Data(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
val searchDorama: List<SearchDorama> = emptyList(),
|
||||
val searchMovie: List<SearchDorama> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchDorama(
|
||||
@SerialName("_id")
|
||||
val id: String,
|
||||
val slug: String,
|
||||
val name: String,
|
||||
@SerialName("name_es")
|
||||
val nameEs: String?,
|
||||
@SerialName("poster_path")
|
||||
val posterPath: String?,
|
||||
val poster: String?,
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
)
|
||||
|
||||
// -------------------------------------------------------
|
||||
|
||||
@Serializable
|
||||
data class VideoToken(
|
||||
val link: String?,
|
||||
val server: String?,
|
||||
val app: String?,
|
||||
val iat: Long?,
|
||||
val exp: Long?,
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
@Serializable
|
||||
data class TokenModel(
|
||||
val props: PropsToken = PropsToken(),
|
||||
val page: String? = null,
|
||||
val query: QueryToken = QueryToken(),
|
||||
val buildId: String? = null,
|
||||
val isFallback: Boolean? = false,
|
||||
val isExperimentalCompile: Boolean? = false,
|
||||
val gssp: Boolean? = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PropsToken(
|
||||
val pageProps: PagePropsToken = PagePropsToken(),
|
||||
@SerialName("__N_SSP")
|
||||
val nSsp: Boolean? = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PagePropsToken(
|
||||
val token: String? = null,
|
||||
val name: String? = null,
|
||||
val app: String? = null,
|
||||
val server: String? = null,
|
||||
val iosapp: String? = null,
|
||||
val externalLink: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QueryToken(
|
||||
val token: String? = null,
|
||||
)
|
|
@ -0,0 +1,592 @@
|
|||
package eu.kanade.tachiyomi.animeextension.es.doramasflix
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamhidevidextractor.StreamHideVidExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
class Doramasflix : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Doramasflix"
|
||||
|
||||
override val baseUrl = "https://doramasflix.in"
|
||||
|
||||
private val apiUrl = "https://sv1.fluxcedene.net/api/gql"
|
||||
|
||||
// The token is made through a type of milliseconds encryption in combination
|
||||
// with other calculated strings, the milliseconds indicate the expiration date
|
||||
// of the token, so it was calculated to expire in 100 years.
|
||||
private val accessPlatform = "RxARncfg1S_MdpSrCvreoLu_SikCGMzE1NzQzODc3NjE2MQ=="
|
||||
|
||||
private val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
override val lang = "es"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANGUAGE_KEY = "preferred_language"
|
||||
private const val PREF_LANGUAGE_DEFAULT = "[LAT]"
|
||||
private val LANGUAGE_LIST = arrayOf(
|
||||
"[ENG]", "[CAST]", "[LAT]", "[SUB]", "[POR]",
|
||||
"[COR]", "[JAP]", "[MAN]", "[TAI]", "[FIL]",
|
||||
"[IND]", "[VIET]",
|
||||
)
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_DEFAULT = "Voe"
|
||||
private val SERVER_LIST = arrayOf(
|
||||
"YourUpload", "BurstCloud", "Voe", "Mp4Upload", "Doodstream",
|
||||
"Upload", "BurstCloud", "Upstream", "StreamTape", "Amazon",
|
||||
"Fastream", "Filemoon", "StreamWish", "Okru", "Streamlare",
|
||||
"Uqload",
|
||||
)
|
||||
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
}
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val popularRequestHeaders = Headers.headersOf(
|
||||
"authority", "sv1.fluxcedene.net",
|
||||
"accept", "application/json, text/plain, */*",
|
||||
"content-type", "application/json;charset=UTF-8",
|
||||
"origin", "https://doramasflix.in",
|
||||
"referer", "https://doramasflix.in/",
|
||||
"platform", "doramasflix",
|
||||
"authorization", "Bear",
|
||||
"x-access-jwt-token", "",
|
||||
"x-access-platform", accessPlatform,
|
||||
)
|
||||
|
||||
private fun externalOrInternalImg(url: String, isThumb: Boolean = false): String {
|
||||
return if (url.contains("https")) {
|
||||
url
|
||||
} else if (isThumb) {
|
||||
"https://image.tmdb.org/t/p/w220_and_h330_face$url"
|
||||
} else {
|
||||
"https://image.tmdb.org/t/p/w500$url"
|
||||
}
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
val anime = SAnime.create()
|
||||
|
||||
document.select("script").map { el ->
|
||||
if (el.data().contains("{\"props\":{\"pageProps\":{")) {
|
||||
val apolloState = json.decodeFromString<JsonObject>(el.data())!!.jsonObject["props"]!!.jsonObject["pageProps"]!!.jsonObject["apolloState"]!!.jsonObject
|
||||
val dorama = apolloState!!.entries!!.firstOrNull { (key, _) -> Regex("\\b(?:Movie|Dorama):[a-zA-Z0-9]+").matches(key) }!!.value!!.jsonObject
|
||||
|
||||
val genres = try { apolloState.entries.filter { x -> x.key.contains("genres") }.joinToString { it.value.jsonObject["name"]!!.jsonPrimitive.content } } catch (_: Exception) { "" }
|
||||
val network = try { apolloState.entries.firstOrNull { x -> x.key.contains("networks") }?.value?.jsonObject?.get("name")!!.jsonPrimitive.content } catch (_: Exception) { "" }
|
||||
val artist = try { dorama["cast"]?.jsonObject?.get("json")?.jsonArray?.firstOrNull()?.jsonObject?.get("name")?.jsonPrimitive?.content } catch (_: Exception) { "" }
|
||||
val type = try { dorama["__typename"]!!.jsonPrimitive.content.lowercase() } catch (_: Exception) { "" }
|
||||
val poster = try { dorama["poster_path"]!!.jsonPrimitive.content } catch (_: Exception) { "" }
|
||||
val urlImg = try { poster.ifEmpty { dorama["poster"]!!.jsonPrimitive.content } } catch (_: Exception) { "" }
|
||||
|
||||
val id = dorama["_id"]!!.jsonPrimitive.content
|
||||
anime.title = "${dorama["name"]?.jsonPrimitive?.content} (${dorama["name_es"]?.jsonPrimitive?.content})"
|
||||
anime.description = dorama["overview"]?.jsonPrimitive?.content?.trim() ?: ""
|
||||
if (genres.isNotEmpty()) anime.genre = genres
|
||||
if (network.isNotEmpty()) anime.author = network
|
||||
if (artist != null) anime.artist = artist
|
||||
if (type.isNotEmpty()) anime.status = if (type == "movie") SAnime.COMPLETED else SAnime.UNKNOWN
|
||||
if (urlImg.isNotEmpty()) anime.thumbnail_url = externalOrInternalImg(urlImg)
|
||||
anime.setUrlWithoutDomain(urlSolverByType(dorama["__typename"]!!.jsonPrimitive!!.content, dorama["slug"]!!.jsonPrimitive!!.content, id))
|
||||
}
|
||||
}
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val id = anime.url.substringAfter("?id=")
|
||||
return if (anime.url.contains("peliculas-online")) {
|
||||
GET(baseUrl + anime.url)
|
||||
} else {
|
||||
val body = (
|
||||
"{\"operationName\":\"listSeasons\",\"variables\":{\"serie_id\":\"$id\"},\"query\":\"query listSeasons(\$serie_id: MongoID!) " +
|
||||
"{\\n listSeasons(sort: NUMBER_ASC, filter: {serie_id: \$serie_id}) {\\n slug\\n season_number\\n poster_path\\n air_date\\n " +
|
||||
"serie_name\\n poster\\n backdrop\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
POST("$apiUrl?id=$id", popularRequestHeaders, body)
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
return if (response.request.url.toString().contains("peliculas-online")) {
|
||||
listOf(
|
||||
SEpisode.create().apply {
|
||||
episode_number = 1F
|
||||
name = "Película"
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
val id = response.request.url.toString().substringAfter("?id=")
|
||||
val responseString = response.body.string()
|
||||
val data = json.decodeFromString<SeasonModel>(responseString).data
|
||||
|
||||
data.listSeasons.parallelCatchingFlatMapBlocking {
|
||||
val season = it.seasonNumber
|
||||
val body = (
|
||||
"{\"operationName\":\"listEpisodes\",\"variables\":{\"serie_id\":\"$id\",\"season_number\":$season},\"query\":\"query " +
|
||||
"listEpisodes(\$season_number: Float!, \$serie_id: MongoID!) {\\n listEpisodes(sort: NUMBER_ASC, filter: {type_serie: \\\"dorama\\\", " +
|
||||
"serie_id: \$serie_id, season_number: \$season_number}) {\\n _id\\n name\\n slug\\n serie_name\\n serie_name_es\\n " +
|
||||
"serie_id\\n still_path\\n air_date\\n season_number\\n episode_number\\n languages\\n poster\\n backdrop\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
|
||||
val episodes = client.newCall(POST(apiUrl, popularRequestHeaders, body)).execute().let { resp ->
|
||||
json.decodeFromString<EpisodeModel>(resp.body.string())
|
||||
}
|
||||
parseEpisodeListJson(episodes)
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
private fun parseEpisodeListJson(episodes: EpisodeModel): List<SEpisode> {
|
||||
var isUpcoming = false
|
||||
val currentDate = Date().time
|
||||
return episodes.data.listEpisodes.mapIndexed { idx, episodeObject ->
|
||||
val dateEp = episodeObject.airDate
|
||||
val nameEp = if (episodeObject.name.isNullOrEmpty()) "- Capítulo ${episodeObject.episodeNumber}" else "- ${episodeObject.name}"
|
||||
if (dateEp != null && dateEp.toDate() > currentDate && !isUpcoming) isUpcoming = true
|
||||
|
||||
SEpisode.create().apply {
|
||||
name = "T${episodeObject.seasonNumber} - E${episodeObject.episodeNumber} $nameEp"
|
||||
episode_number = episodeObject.episodeNumber?.toFloat() ?: idx.toFloat()
|
||||
date_upload = dateEp?.toDate() ?: 0L
|
||||
scanlator = if (isUpcoming) "Próximamente..." else null
|
||||
setUrlWithoutDomain(urlSolverByType("episode", episodeObject.slug))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return when {
|
||||
responseString.contains("paginationMovie") -> parsePopularJson(responseString, "movie")
|
||||
else -> parsePopularJson(responseString, "dorama")
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val body = (
|
||||
"{\"operationName\":\"listDoramas\",\"variables\":{\"page\":$page,\"sort\":\"CREATEDAT_DESC\",\"perPage\":32,\"filter\":{\"isTVShow\":false}}," +
|
||||
"\"query\":\"query listDoramas(\$page: Int, \$perPage: Int, \$sort: SortFindManyDoramaInput, \$filter: FilterFindManyDoramaInput) {\\n " +
|
||||
"paginationDorama(page: \$page, perPage: \$perPage, sort: \$sort, filter: \$filter) {\\n count\\n pageInfo {\\n currentPage\\n " +
|
||||
"hasNextPage\\n hasPreviousPage\\n __typename\\n }\\n items {\\n _id\\n name\\n name_es\\n slug\\n " +
|
||||
"cast\\n names\\n overview\\n languages\\n created_by\\n popularity\\n poster_path\\n vote_average\\n " +
|
||||
"backdrop_path\\n first_air_date\\n episode_run_time\\n isTVShow\\n poster\\n backdrop\\n genres {\\n " +
|
||||
"name\\n slug\\n __typename\\n }\\n networks {\\n name\\n slug\\n __typename\\n }\\n " +
|
||||
"__typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
return POST(apiUrl, popularRequestHeaders, body)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return when {
|
||||
responseString.contains("paginationMovie") -> parsePopularJson(responseString, "movie")
|
||||
else -> parsePopularJson(responseString, "dorama")
|
||||
}
|
||||
}
|
||||
|
||||
private val languages = arrayOf(
|
||||
Pair("36", "[ENG]"),
|
||||
Pair("37", "[CAST]"),
|
||||
Pair("38", "[LAT]"),
|
||||
Pair("192", "[SUB]"),
|
||||
Pair("1327", "[POR]"),
|
||||
Pair("13109", "[COR]"),
|
||||
Pair("13110", "[JAP]"),
|
||||
Pair("13111", "[MAN]"),
|
||||
Pair("13112", "[TAI]"),
|
||||
Pair("13113", "[FIL]"),
|
||||
Pair("13114", "[IND]"),
|
||||
Pair("343422", "[VIET]"),
|
||||
)
|
||||
|
||||
private fun String.getLang(): String {
|
||||
return languages.firstOrNull { it.first == this }?.second ?: ""
|
||||
}
|
||||
|
||||
private fun parsePopularJson(jsonLine: String?, type: String): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val data = json.decodeFromString<PaginationModel>(jsonData).data
|
||||
|
||||
val pagination = when (type) {
|
||||
"dorama" -> data.paginationDorama
|
||||
"movie" -> data.paginationMovie
|
||||
else -> throw IllegalArgumentException("Tipo de dato no válido: $type")
|
||||
}
|
||||
|
||||
val hasNextPage = pagination?.pageInfo?.hasNextPage ?: false
|
||||
val animeList = pagination?.items?.map { animeObject ->
|
||||
val urlImg = when {
|
||||
!animeObject.posterPath.isNullOrEmpty() -> animeObject.posterPath.toString()
|
||||
!animeObject.poster.isNullOrEmpty() -> animeObject.poster.toString()
|
||||
else -> ""
|
||||
}
|
||||
|
||||
SAnime.create().apply {
|
||||
title = "${animeObject.name} (${animeObject.nameEs})"
|
||||
description = animeObject.overview
|
||||
genre = animeObject.genres.joinToString { it.name ?: "" }
|
||||
thumbnail_url = externalOrInternalImg(urlImg, true)
|
||||
setUrlWithoutDomain(urlSolverByType(animeObject.typename, animeObject.slug, animeObject.id))
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList ?: emptyList(), hasNextPage)
|
||||
}
|
||||
|
||||
private fun urlSolverByType(type: String, slug: String, id: String? = ""): String {
|
||||
return when (type.lowercase()) {
|
||||
"dorama" -> "$baseUrl/doramas-online/$slug?id=$id"
|
||||
"episode" -> "$baseUrl/episodios/$slug"
|
||||
"movie" -> "$baseUrl/peliculas-online/$slug?id=$id"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val body = (
|
||||
"{\"operationName\":\"listDoramas\",\"variables\":{\"page\":$page,\"sort\":\"POPULARITY_DESC\",\"perPage\":32,\"filter\":{\"isTVShow\":false}}," +
|
||||
"\"query\":\"query listDoramas(\$page: Int, \$perPage: Int, \$sort: SortFindManyDoramaInput, \$filter: FilterFindManyDoramaInput) {\\n " +
|
||||
"paginationDorama(page: \$page, perPage: \$perPage, sort: \$sort, filter: \$filter) {\\n count\\n pageInfo {\\n currentPage\\n " +
|
||||
"hasNextPage\\n hasPreviousPage\\n __typename\\n }\\n items {\\n _id\\n name\\n name_es\\n slug\\n " +
|
||||
"cast\\n names\\n overview\\n languages\\n created_by\\n popularity\\n poster_path\\n vote_average\\n " +
|
||||
"backdrop_path\\n first_air_date\\n episode_run_time\\n isTVShow\\n poster\\n backdrop\\n genres {\\n " +
|
||||
"name\\n slug\\n __typename\\n }\\n networks {\\n name\\n slug\\n __typename\\n }\\n " +
|
||||
"__typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
return POST(apiUrl, popularRequestHeaders, body)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val responseString = response.body.string()
|
||||
return when {
|
||||
responseString.contains("searchDorama") -> parseSearchAnimeJson(responseString)
|
||||
responseString.contains("paginationMovie") -> parsePopularJson(responseString, "movie")
|
||||
else -> parsePopularJson(responseString, "dorama")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSearchAnimeJson(jsonLine: String?): AnimesPage {
|
||||
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
|
||||
val jsonObject = json.decodeFromString<SearchModel>(jsonData).data
|
||||
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
jsonObject.searchDorama.map { castToSAnime(it) }.also(animeList::addAll)
|
||||
jsonObject.searchMovie.map { castToSAnime(it) }.also(animeList::addAll)
|
||||
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
private fun castToSAnime(animeObject: SearchDorama): SAnime {
|
||||
val urlImg = when {
|
||||
!animeObject.posterPath.isNullOrEmpty() -> animeObject.posterPath.toString()
|
||||
!animeObject.poster.isNullOrEmpty() -> animeObject.poster.toString()
|
||||
else -> ""
|
||||
}
|
||||
return SAnime.create().apply {
|
||||
title = "${animeObject.name} (${animeObject.nameEs})"
|
||||
thumbnail_url = externalOrInternalImg(urlImg, true)
|
||||
setUrlWithoutDomain(urlSolverByType(animeObject.typename, animeObject.slug, animeObject.id))
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> searchQueryRequest(query)
|
||||
"peliculas" in genreFilter.toUriPart() -> popularMovieRequest(page)
|
||||
"variedades" in genreFilter.toUriPart() -> popularVarietiesRequest(page)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchQueryRequest(query: String): Request {
|
||||
val fxQuery = query.replace("+", " ")
|
||||
val body = (
|
||||
"{\"operationName\":\"searchAll\",\"variables\":{\"input\":\"$fxQuery\"},\"query\":\"query searchAll(\$input: String!) {\\n " +
|
||||
"searchDorama(input: \$input, limit: 32) {\\n _id\\n slug\\n name\\n name_es\\n poster_path\\n poster\\n " +
|
||||
"__typename\\n }\\n searchMovie(input: \$input, limit: 32) {\\n _id\\n name\\n name_es\\n slug\\n poster_path\\n " +
|
||||
"poster\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
return POST(apiUrl, popularRequestHeaders, body)
|
||||
}
|
||||
|
||||
private fun popularMovieRequest(page: Int): Request {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val body = (
|
||||
"{\"operationName\":\"listMovies\",\"variables\":{\"perPage\":32,\"sort\":\"CREATEDAT_DESC\",\"filter\":{},\"page\":$page},\"query\":\"query " +
|
||||
"listMovies(\$page: Int, \$perPage: Int, \$sort: SortFindManyMovieInput, \$filter: FilterFindManyMovieInput) {\\n paginationMovie(page: \$page" +
|
||||
", perPage: \$perPage, sort: \$sort, filter: \$filter) {\\n count\\n pageInfo {\\n currentPage\\n hasNextPage\\n hasPreviousPage\\n" +
|
||||
" __typename\\n }\\n items {\\n _id\\n name\\n name_es\\n slug\\n cast\\n names\\n overview\\n " +
|
||||
"languages\\n popularity\\n poster_path\\n vote_average\\n backdrop_path\\n release_date\\n runtime\\n poster\\n " +
|
||||
"backdrop\\n genres {\\n name\\n __typename\\n }\\n networks {\\n name\\n __typename\\n }\\n " +
|
||||
"__typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
|
||||
return POST(apiUrl, popularRequestHeaders, body)
|
||||
}
|
||||
|
||||
private fun popularVarietiesRequest(page: Int): Request {
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val body = (
|
||||
"{\"operationName\":\"listDoramas\",\"variables\":{\"page\":$page,\"sort\":\"CREATEDAT_DESC\",\"perPage\":32,\"filter\":{\"isTVShow\":true}},\"query\":\"query " +
|
||||
"listDoramas(\$page: Int, \$perPage: Int, \$sort: SortFindManyDoramaInput, \$filter: FilterFindManyDoramaInput) {\\n paginationDorama(page: \$page, perPage: \$perPage, " +
|
||||
"sort: \$sort, filter: \$filter) {\\n count\\n pageInfo {\\n currentPage\\n hasNextPage\\n hasPreviousPage\\n __typename\\n }\\n " +
|
||||
"items {\\n _id\\n name\\n name_es\\n slug\\n cast\\n names\\n overview\\n languages\\n created_by\\n " +
|
||||
"popularity\\n poster_path\\n vote_average\\n backdrop_path\\n first_air_date\\n episode_run_time\\n isTVShow\\n poster\\n " +
|
||||
"backdrop\\n genres {\\n name\\n slug\\n __typename\\n }\\n networks {\\n name\\n slug\\n " +
|
||||
"__typename\\n }\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
).toRequestBody(mediaType)
|
||||
|
||||
return POST(apiUrl, popularRequestHeaders, body)
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Géneros",
|
||||
arrayOf(
|
||||
Pair("Doramas", "doramas"),
|
||||
Pair("Películas", "peliculas"),
|
||||
Pair("Variedades", "variedades"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(trim())?.time }.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val jsonData = document.selectFirst("script:containsData({\"props\":{\"pageProps\":{)")!!.data()
|
||||
val apolloState = json.decodeFromString<JsonObject>(jsonData).jsonObject["props"]!!.jsonObject["pageProps"]!!.jsonObject["apolloState"]!!.jsonObject
|
||||
val episodeItem = apolloState.entries.firstOrNull { x -> x.key.contains("Episode:") }
|
||||
|
||||
val episode = episodeItem?.value?.jsonObject
|
||||
?: apolloState.entries.firstOrNull { (key, _) -> Regex("\\b(?:Movie|Dorama):[a-zA-Z0-9]+").matches(key) }?.value?.jsonObject
|
||||
|
||||
var linksOnline = episode?.get("links_online")?.jsonObject?.get("json")?.jsonArray
|
||||
val bMovies = apolloState.entries.any { x -> x.key.contains("ROOT_QUERY.getMovieLinks(") }
|
||||
|
||||
if (bMovies && linksOnline == null) {
|
||||
linksOnline = apolloState.entries.firstOrNull { x -> x.key.contains("ROOT_QUERY.getMovieLinks(") }
|
||||
?.value?.jsonObject?.get("links_online")?.jsonObject?.get("json")?.jsonArray
|
||||
}
|
||||
|
||||
return linksOnline?.parallelCatchingFlatMapBlocking {
|
||||
val link = it.jsonObject["link"]!!.jsonPrimitive.content
|
||||
val lang = it.jsonObject["lang"]?.jsonPrimitive?.content?.getLang() ?: ""
|
||||
serverVideoResolver(link, lang)
|
||||
} ?: apolloState.entries.filter { x -> x.key.contains("ROOT_QUERY.listProblems(") }
|
||||
.mapNotNull { entry ->
|
||||
val server = entry.value.jsonObject["server"]?.jsonObject?.get("json")?.jsonObject
|
||||
val link = server?.get("link")?.jsonPrimitive?.content
|
||||
val lang = server?.get("lang")?.jsonPrimitive?.content?.getLang() ?: ""
|
||||
link?.let { it to lang }
|
||||
}.distinctBy { it.first }
|
||||
.parallelCatchingFlatMapBlocking { (link, lang) ->
|
||||
val finalLink = getRealLink(link)
|
||||
serverVideoResolver(finalLink, lang)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRealLink(link: String): String {
|
||||
if (!link.contains("fkplayer.xyz")) return link
|
||||
|
||||
val token = client.newCall(GET(link)).execute()
|
||||
.asJsoup().selectFirst("script:containsData({\"props\":{\"pageProps\":{)")?.data()
|
||||
?.parseTo<TokenModel>()
|
||||
|
||||
val mediaType = "application/json".toMediaType()
|
||||
val requestBody = "{\"token\":\"${token?.props?.pageProps?.token ?: token?.query?.token}\"}".toRequestBody(mediaType)
|
||||
|
||||
val headersVideo = headers.newBuilder()
|
||||
.add("origin", "https://${link.toHttpUrl().host}")
|
||||
.add("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val json = client.newCall(POST("https://fkplayer.xyz/api/decoding", headersVideo, requestBody))
|
||||
.execute().body.string().parseTo<VideoToken>()
|
||||
|
||||
return String(Base64.decode(json.link, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
|
||||
val embedUrl = url.lowercase()
|
||||
return when {
|
||||
"voe" in embedUrl -> VoeExtractor(client).videosFromUrl(url, " $prefix")
|
||||
"ok.ru" in embedUrl || "okru" in embedUrl -> OkruExtractor(client).videosFromUrl(url, prefix = "$prefix ")
|
||||
"filemoon" in embedUrl || "moonplayer" in embedUrl -> {
|
||||
val vidHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix = "$prefix Filemoon:", headers = vidHeaders)
|
||||
}
|
||||
"uqload" in embedUrl -> UqloadExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
"mp4upload" in embedUrl -> Mp4uploadExtractor(client).videosFromUrl(url, prefix = "$prefix ", headers = headers)
|
||||
"doodstream" in embedUrl || "dood." in embedUrl ->
|
||||
listOf(DoodExtractor(client).videoFromUrl(url.replace("https://doodstream.com/e/", "https://dood.to/e/"), "$prefix DoodStream", false)!!)
|
||||
"streamlare" in embedUrl -> StreamlareExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
"yourupload" in embedUrl || "upload" in embedUrl -> YourUploadExtractor(client).videoFromUrl(url, headers = headers, prefix = "$prefix ")
|
||||
"wishembed" in embedUrl || "streamwish" in embedUrl || "strwish" in embedUrl || "wish" in embedUrl -> {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Origin", "https://streamwish.to")
|
||||
.add("Referer", "https://streamwish.to/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
|
||||
}
|
||||
"burstcloud" in embedUrl || "burst" in embedUrl -> BurstCloudExtractor(client).videoFromUrl(url, headers = headers, prefix = "$prefix ")
|
||||
"fastream" in embedUrl -> FastreamExtractor(client, headers).videosFromUrl(url, prefix = "$prefix Fastream:")
|
||||
"upstream" in embedUrl -> UpstreamExtractor(client).videosFromUrl(url, prefix = "$prefix ")
|
||||
"streamtape" in embedUrl || "stp" in embedUrl || "stape" in embedUrl -> listOf(StreamTapeExtractor(client).videoFromUrl(url, quality = "$prefix StreamTape")!!)
|
||||
"ahvsh" in embedUrl || "streamhide" in embedUrl -> StreamHideVidExtractor(client).videosFromUrl(url, "$prefix ")
|
||||
"filelions" in embedUrl || "lion" in embedUrl -> StreamWishExtractor(client, headers).videosFromUrl(url, videoNameGen = { "$prefix FileLions:$it" })
|
||||
"vudeo" in embedUrl || "vudea" in embedUrl -> VudeoExtractor(client).videosFromUrl(url, "$prefix ")
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(lang) },
|
||||
{ it.quality.contains(server, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_LANGUAGE_KEY
|
||||
title = "Preferred language"
|
||||
entries = LANGUAGE_LIST
|
||||
entryValues = LANGUAGE_LIST
|
||||
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_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = QUALITY_LIST
|
||||
entryValues = QUALITY_LIST
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = SERVER_LIST
|
||||
entryValues = SERVER_LIST
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
private inline fun <reified T> String.parseTo(): T {
|
||||
return json.decodeFromString<T>(this)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseTo(): T {
|
||||
return json.decodeFromString<T>(this.body.string())
|
||||
}
|
||||
}
|
13
src/es/doramasyt/build.gradle
Normal file
|
@ -0,0 +1,13 @@
|
|||
ext {
|
||||
extName = 'Doramasyt'
|
||||
extClass = '.Doramasyt'
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
}
|
BIN
src/es/doramasyt/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/es/doramasyt/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/es/doramasyt/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |