fix(src/es): Spanish extension updates (#2284)

This commit is contained in:
imper1aldev 2023-10-01 05:36:55 -06:00 committed by GitHub
parent c1829105e6
commit d1e4d8d196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 302 additions and 425 deletions

View File

@ -14,4 +14,5 @@ android {
dependencies { dependencies {
compileOnly(libs.bundles.common) compileOnly(libs.bundles.common)
} implementation(project(":lib-playlist-utils"))
}

View File

@ -1,72 +1,46 @@
package eu.kanade.tachiyomi.lib.fastreamextractor package eu.kanade.tachiyomi.lib.fastreamextractor
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import okhttp3.Headers
import kotlinx.serialization.json.jsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class FastreamExtractor(private val client: OkHttpClient) { class FastreamExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private fun fetchUrls(text: String?): List<String> { private fun fetchUrls(text: String?): List<String> {
if (text.isNullOrEmpty()) return listOf() if (text.isNullOrEmpty()) return listOf()
val linkRegex = "(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])".toRegex() val linkRegex = "(http|ftp|https):\\/\\/([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])".toRegex()
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList() return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
} }
fun videoFromUrl(url: String, server: String = "Fastream"): List<Video> { fun videoFromUrl(url: String, prefix: String = "Fastream:", headers: Headers? = null): List<Video> {
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
try { try {
val document = client.newCall(GET(url)).execute() val document = client.newCall(GET(url)).execute().asJsoup()
if (document.isSuccessful) { val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
val content = document.asJsoup() .set("Referer", "https://fastream.to/")
content.select("script").forEach { .set("Origin", "https://fastream.to")
if (it!!.data().contains("jwplayer(jwplayer(\"vplayer\").setup({")) { .build()
val basicUrl = it.data().substringAfter("file: '").substringBefore("',") document.select("script").forEach {
videoList.add(Video(basicUrl, server, basicUrl, headers = null)) if (it!!.data().contains("jwplayer(jwplayer(\"vplayer\").setup({")) {
} else { val basicUrl = it.data().substringAfter("file: '").substringBefore("',")
val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)") videoList.add(Video(basicUrl, prefix, basicUrl, headers = videoHeaders))
val qualities = listOf( } else {
Pair("Low", "360p"), val packedRegex = "eval\\(function\\(p,a,c,k,e,.*\\)\\)".toRegex()
Pair("Normal", "480p"), packedRegex.findAll(it.data()).map { packed -> packed.value }.toList().map { eval ->
Pair("HD", "720p"), val unpack = JsUnpacker.unpack(eval)
Pair("Full", "1080p"), val serverRegex = "fastream.*?\\.m3u8([^&\">]?)".toRegex()
) fetchUrls(unpack.first()).filter { serverRegex.containsMatchIn(it) }.map { url ->
packedRegex.findAll(it.data()).map { packed -> packed.value }.toList().map { eval -> PlaylistUtils(client, videoHeaders).extractFromHls(url, videoNameGen = { "$prefix$it" }).let { videoList.addAll(it) }
val fastreamRegex = "fastream.*?\\.m3u8([^&\">]?)".toRegex()
val unpack = JsUnpacker.unpack(eval)
fetchUrls(unpack.first()).map { url ->
if (fastreamRegex.containsMatchIn(url)) {
val urlQualities = url.split(",").filter { p -> !p.contains("m3u8") }
val baseUrl = urlQualities.first()
val jsonQualities = "{ \"qualityLabels\": { ${unpack.first().substringAfter("\\'qualityLabels\\':{").substringBefore("},")} }}"
val jObject = json.decodeFromString<JsonObject>(jsonQualities)
val jQualities = jObject["qualityLabels"]!!.jsonObject.map { jsonElement ->
val jQuality = jsonElement.value.toString().replace("\"", "")
qualities.find { q -> q.first.contains(jQuality) }?.second
}.toTypedArray()
var qualityIdx = 0
urlQualities.map { _url ->
if (!_url.contains("http")) {
val quality = "$server:${jQualities[qualityIdx]}"
val videoUrl = "$baseUrl$_url/master.m3u8"
qualityIdx++
videoList.add(Video(videoUrl, quality, videoUrl, headers = null))
}
}
}
}
} }
} }
} }
} }
} catch (_: Exception) { } catch (_: Exception) {}
}
return videoList return videoList
} }
} }

View File

@ -5,14 +5,19 @@ ext {
extName = 'Animefenix' extName = 'Animefenix'
pkgNameSuffix = 'es.animefenix' pkgNameSuffix = 'es.animefenix'
extClass = '.Animefenix' extClass = '.Animefenix'
extVersionCode = 27 extVersionCode = 28
libVersion = '13' libVersion = '13'
} }
dependencies { dependencies {
implementation(project(':lib-mp4upload-extractor')) implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-streamtape-extractor')) implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-yourupload-extractor'))
implementation(project(':lib-okru-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'))
} }

View File

@ -11,9 +11,14 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor 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.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -90,40 +95,34 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.data().substringAfter("src=\"").substringBefore("\"") .data().substringAfter("src=\"").substringBefore("\"")
} catch (e: Exception) { "" } } catch (e: Exception) { "" }
when { try {
realUrl.contains("ok.ru") -> { when {
val okruVideos = OkruExtractor(client).videosFromUrl(realUrl) realUrl.contains("ok.ru") || realUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(realUrl).let { videoList.addAll(it) }
videoList.addAll(okruVideos) realUrl.contains("filemoon") || realUrl.contains("moonplayer") -> FilemoonExtractor(client).videosFromUrl(realUrl, headers = headers).let { videoList.addAll(it) }
} realUrl.contains("burstcloud") || realUrl.contains("burst") -> BurstCloudExtractor(client).videoFromUrl(realUrl, headers = headers).let { videoList.addAll(it) }
realUrl.contains("/stream/amz.php?") -> { realUrl.contains("streamwish") || realUrl.contains("embedwish") -> StreamWishExtractor(client, headers).videosFromUrl(realUrl).let { videoList.addAll(it) }
val video = amazonExtractor(baseUrl + realUrl.substringAfter("..")) realUrl.contains("streamtape") -> StreamTapeExtractor(client).videoFromUrl(realUrl, "StreamTape")?.let { videoList.add(it) }
if (video.isNotBlank()) { realUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(realUrl, headers).let { videoList.addAll(it) }
if (realUrl.contains("&ext=es")) { realUrl.contains("yourupload") -> YourUploadExtractor(client).videoFromUrl(realUrl, headers = headers).let { videoList.addAll(it) }
videoList.add(Video(video, "Amazon ES", video)) realUrl.contains("voe") -> VoeExtractor(client).videoFromUrl(realUrl)?.let { videoList.add(it) }
} else { realUrl.contains("/stream/amz.php?") -> {
videoList.add(Video(video, "Amazon", video)) val video = amazonExtractor(baseUrl + realUrl.substringAfter(".."))
if (video.isNotBlank()) {
if (realUrl.contains("&ext=es")) {
videoList.add(Video(video, "Amazon ES", video))
} else {
videoList.add(Video(video, "Amazon", video))
}
} }
} }
} realUrl.contains("/stream/fl.php") -> {
realUrl.contains("/stream/fl.php") -> { val video = realUrl.substringAfter("/stream/fl.php?v=")
val video = realUrl.substringAfter("/stream/fl.php?v=")
try {
if (client.newCall(GET(video)).execute().code == 200) { if (client.newCall(GET(video)).execute().code == 200) {
videoList.add(Video(video, "FireLoad", video)) videoList.add(Video(video, "FireLoad", video))
} }
} catch (e: Exception) {}
}
realUrl.contains("streamtape") -> {
val video = StreamTapeExtractor(client).videoFromUrl(realUrl, "StreamTape")
if (video != null) {
videoList.add(video)
} }
} }
realUrl.contains("mp4upload") -> { } catch (_: Exception) { }
val videos = Mp4uploadExtractor(client).videosFromUrl(realUrl, headers)
videoList.addAll(videos)
}
}
} }
return videoList.filter { it.url.contains("https") || it.url.contains("http") } return videoList.filter { it.url.contains("https") || it.url.contains("http") }
} }
@ -159,6 +158,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val yearFilter = filters.find { it is YearFilter } as YearFilter val yearFilter = filters.find { it is YearFilter } as YearFilter
val stateFilter = filters.find { it is StateFilter } as StateFilter val stateFilter = filters.find { it is StateFilter } as StateFilter
val typeFilter = filters.find { it is TypeFilter } as TypeFilter 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 } val genreFilter = (filters.find { it is TagFilter } as TagFilter).state.filter { it.state }
@ -180,6 +180,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (typeFilter.state != 0) { if (typeFilter.state != 0) {
filterUrl += "&type[]=${typeFilter.toUriPart()}" filterUrl += "&type[]=${typeFilter.toUriPart()}"
} // search by type } // search by type
filterUrl += "&order=${orderByFilter.toUriPart()}"
filterUrl += "&page=$page" // add page filterUrl += "&page=$page" // add page
return when { return when {
@ -188,6 +189,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
else -> GET("$baseUrl/animes?order=likes&page=$page ") else -> GET("$baseUrl/animes?order=likes&page=$page ")
} }
} }
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
@ -236,6 +238,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
TagFilter("Generos", checkboxesFrom(genreList)), TagFilter("Generos", checkboxesFrom(genreList)),
StateFilter(), StateFilter(),
TypeFilter(), TypeFilter(),
OrderByFilter(),
YearFilter(), YearFilter(),
) )
@ -292,9 +295,11 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun checkboxesFrom(tagArray: Array<Pair<String, String>>): List<TagCheckBox> = tagArray.map { TagCheckBox(it.second) } private fun checkboxesFrom(tagArray: Array<Pair<String, String>>): List<TagCheckBox> = tagArray.map { TagCheckBox(it.second) }
class TagCheckBox(tag: String) : AnimeFilter.CheckBox(tag, false) class TagCheckBox(tag: String) : AnimeFilter.CheckBox(tag, false)
class TagFilter(name: String, checkBoxes: List<TagCheckBox>) : AnimeFilter.Group<TagCheckBox>(name, checkBoxes) class TagFilter(name: String, checkBoxes: List<TagCheckBox>) : AnimeFilter.Group<TagCheckBox>(name, checkBoxes)
private class YearFilter : AnimeFilter.Text("Año", "2022") private class YearFilter : AnimeFilter.Text("Año")
private class StateFilter : UriPartFilter( private class StateFilter : UriPartFilter(
"Estado", "Estado",
arrayOf( arrayOf(
@ -305,6 +310,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Pair("En Cuarentena", "4"), Pair("En Cuarentena", "4"),
), ),
) )
private class TypeFilter : UriPartFilter( private class TypeFilter : UriPartFilter(
"Tipo", "Tipo",
arrayOf( arrayOf(
@ -316,23 +322,33 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
), ),
) )
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>>) : private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf(
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru
"Amazon", "AmazonES", "StreamTape", "Fireload", "Mp4upload", "YourUpload", "StreamWish"
)
val videoQualityPref = ListPreference(screen.context).apply { val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" key = "preferred_quality"
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf( entries = qualities
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru entryValues = qualities
"Amazon", "AmazonES", "StreamTape", "Fireload", "Mp4upload",
)
entryValues = arrayOf(
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru
"Amazon", "AmazonES", "StreamTape", "Fireload", "Mp4upload",
)
setDefaultValue("Amazon") setDefaultValue("Amazon")
summary = "%s" summary = "%s"

View File

@ -5,7 +5,7 @@ ext {
extName = 'AnimeFLV' extName = 'AnimeFLV'
pkgNameSuffix = 'es.animeflv' pkgNameSuffix = 'es.animeflv'
extClass = '.AnimeFlv' extClass = '.AnimeFlv'
extVersionCode = 49 extVersionCode = 50
libVersion = '13' libVersion = '13'
} }
@ -14,6 +14,7 @@ dependencies {
implementation(project(':lib-streamtape-extractor')) implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-okru-extractor')) implementation(project(':lib-okru-extractor'))
implementation(project(':lib-dood-extractor')) implementation(project(':lib-dood-extractor'))
implementation(project(':lib-streamwish-extractor'))
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -14,10 +14,10 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor 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.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -113,19 +113,22 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
jObject["SUB"]!!.jsonArray!!.forEach { servers -> jObject["SUB"]!!.jsonArray!!.forEach { servers ->
val json = servers!!.jsonObject val json = servers!!.jsonObject
val quality = json!!["title"]!!.jsonPrimitive!!.content val quality = json!!["title"]!!.jsonPrimitive!!.content
var url = json!!["code"]!!.jsonPrimitive!!.content val url = json!!["code"]!!.jsonPrimitive!!.content
val extractedVideos = runCatching { val extractedVideos = runCatching {
when (quality) { when (quality) {
"Stape" -> { "Stape" -> {
val stapeUrl = json!!["url"]!!.jsonPrimitive!!.content val stapeUrl = json!!["url"]!!.jsonPrimitive!!.content
StreamTapeExtractor(client).videoFromUrl(stapeUrl) StreamTapeExtractor(client).videoFromUrl(stapeUrl)?.let(::listOf)
?.let(::listOf)
} }
"Doodstream" -> "Doodstream" -> DoodExtractor(client).videoFromUrl(url, "DoodStream", false)?.let(::listOf)
DoodExtractor(client).videoFromUrl(url, "DoodStream", false)
?.let(::listOf)
"Okru" -> OkruExtractor(client).videosFromUrl(url) "Okru" -> OkruExtractor(client).videosFromUrl(url)
"YourUpload" -> YourUploadExtractor(client).videoFromUrl(url, headers = headers) "YourUpload" -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
"SW" -> {
val docHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, "StreamWish")
}
else -> null else -> null
} }
}.getOrNull() ?: emptyList<Video>() }.getOrNull() ?: emptyList<Video>()

View File

@ -5,8 +5,11 @@ ext {
extName = 'Animeyt' extName = 'Animeyt'
pkgNameSuffix = 'es.animeyt' pkgNameSuffix = 'es.animeyt'
extClass = '.Animeyt' extClass = '.Animeyt'
extVersionCode = 5 extVersionCode = 6
libVersion = '13' libVersion = '13'
} }
dependencies {
implementation(project(':lib-fastream-extractor'))
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,13 +4,13 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.animeyt.extractors.FastreamExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@ -1,76 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.animeyt.extractors
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video
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.jsonObject
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class FastreamExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
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()
}
fun videoFromUrl(url: String, server: String = "Fastream"): List<Video> {
val videoList = mutableListOf<Video>()
try {
val document = client.newCall(GET(url)).execute()
if (document.isSuccessful) {
val content = document.asJsoup()
content.select("script").forEach {
if (it!!.data().contains("jwplayer(jwplayer(\"vplayer\").setup({")) {
val basicUrl = it.data().substringAfter("file: '").substringBefore("',")
videoList.add(Video(basicUrl, server, basicUrl, headers = null))
} else {
val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)")
val qualities = listOf(
Pair("Low", "360p"),
Pair("Normal", "480p"),
Pair("HD", "720p"),
Pair("Full", "1080p"),
)
packedRegex.findAll(it.data()).map { packed -> packed.value }.toList().map { eval ->
val fastreamRegex = "fastream.*?\\.m3u8([^&\">]?)".toRegex()
val unpack = JsUnpacker.unpack(eval)
fetchUrls(unpack.first()).map { url ->
if (fastreamRegex.containsMatchIn(url)) {
val urlQualities = url.split(",").filter { p -> !p.contains("m3u8") }
val baseUrl = urlQualities.first()
val jsonQualities = "{ \"qualityLabels\": { ${unpack.first().substringAfter("\\'qualityLabels\\':{").substringBefore("},")} }}"
val jObject = json.decodeFromString<JsonObject>(jsonQualities)
val jQualities = jObject["qualityLabels"]!!.jsonObject.map { jsonElement ->
val jQuality = jsonElement.value.toString().replace("\"", "")
qualities.find { q -> q.first.contains(jQuality) }?.second
}.toTypedArray()
Log.i("bruh jQuality join", jQualities.joinToString())
var qualityIdx = 0
urlQualities.map { _url ->
if (!_url.contains("http")) {
Log.i("bruh _url", "$_url $qualityIdx")
val quality = "$server:${jQualities[qualityIdx]}"
Log.i("bruh quality", quality)
val videoUrl = "$baseUrl$_url/master.m3u8"
Log.i("bruh videoUrl", videoUrl)
qualityIdx++
videoList.add(Video(videoUrl, quality, videoUrl, headers = null))
}
}
}
}
}
}
}
}
} catch (_: Exception) {
}
return videoList
}
}

View File

@ -1,193 +0,0 @@
package eu.kanade.tachiyomi.animeextension.es.animeyt.extractors
import kotlin.math.pow
object JsUnpacker {
/**
* Regex to detect packed functions.
*/
private val PACKED_REGEX = Regex("eval[(]function[(]p,a,c,k,e,[r|d]?", setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
/**
* Regex to get and group the packed javascript.
* Needed to get information and unpack the code.
*/
private val PACKED_EXTRACT_REGEX = Regex("[}][(]'(.*)', *(\\d+), *(\\d+), *'(.*?)'[.]split[(]'[|]'[)]", setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
/**
* Matches function names and variables to de-obfuscate the code.
*/
private val UNPACK_REPLACE_REGEX = Regex("\\b\\w+\\b", setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
/**
* Check if script is packed.
*
* @param scriptBlock the String to check if it is packed.
*
* @return whether the [scriptBlock] contains packed code or not.
*/
fun detect(scriptBlock: String): Boolean {
return scriptBlock.contains(PACKED_REGEX)
}
/**
* Check if scripts are packed.
*
* @param scriptBlock (multiple) String(s) to check if it is packed.
*
* @return the packed scripts passed in [scriptBlock].
*/
fun detect(vararg scriptBlock: String): List<String> {
return scriptBlock.mapNotNull {
if (it.contains(PACKED_REGEX)) {
it
} else {
null
}
}
}
/**
* Check if scripts are packed.
*
* @param scriptBlocks multiple Strings to check if it is packed.
*
* @return the packed scripts passed in [scriptBlocks].
*/
fun detect(scriptBlocks: Collection<String>): List<String> {
return detect(*scriptBlocks.toTypedArray())
}
/**
* Unpack the passed [scriptBlock].
* It matches all found occurrences and returns them as separate Strings in a list.
*
* @param scriptBlock the String to unpack.
*
* @return unpacked code in a list or an empty list if non is packed.
*/
fun unpack(scriptBlock: String): Sequence<String> {
return if (!detect(scriptBlock)) {
emptySequence()
} else {
unpacking(scriptBlock)
}
}
/**
* Unpack the passed [scriptBlock].
* It matches all found occurrences and combines them into a single String.
*
* @param scriptBlock the String to unpack.
*
* @return unpacked code in a list combined by a whitespace to a single String.
*/
fun unpackAndCombine(scriptBlock: String): String? {
val unpacked = unpack(scriptBlock)
return if (unpacked.toList().isEmpty()) {
null
} else {
unpacked.joinToString(" ")
}
}
/**
* Unpack the passed [scriptBlock].
* It matches all found occurrences and returns them as separate Strings in a list.
*
* @param scriptBlock (multiple) String(s) to unpack.
*
* @return unpacked code in a flat list or an empty list if non is packed.
*/
fun unpack(vararg scriptBlock: String): List<String> {
val packedScripts = detect(*scriptBlock)
return packedScripts.flatMap {
unpacking(it)
}
}
/**
* Unpack the passed [scriptBlocks].
* It matches all found occurrences and returns them as separate Strings in a list.
*
* @param scriptBlocks multiple Strings to unpack.
*
* @return unpacked code in a flat list or an empty list if non is packed.
*/
fun unpack(scriptBlocks: Collection<String>): List<String> {
return unpack(*scriptBlocks.toTypedArray())
}
/**
* Unpacking functionality.
* Match all found occurrences, get the information group and unbase it.
* If found symtabs are more or less than the count provided in code, the occurrence will be ignored
* because it cannot be unpacked correctly.
*
* @param scriptBlock the String to unpack.
*
* @return a list of all unpacked code from all found packed and unpackable occurrences found.
*/
private fun unpacking(scriptBlock: String): Sequence<String> {
val unpacked = PACKED_EXTRACT_REGEX.findAll(scriptBlock).mapNotNull { result ->
val payload = result.groups[1]?.value
val symtab = result.groups[4]?.value?.split('|')
val radix = result.groups[2]?.value?.toIntOrNull() ?: 10
val count = result.groups[3]?.value?.toIntOrNull()
val unbaser = Unbaser(radix)
if (symtab == null || count == null || symtab.size != count) {
null
} else {
payload?.replace(UNPACK_REPLACE_REGEX) { match ->
val word = match.value
val unbased = symtab[unbaser.unbase(word)]
unbased.ifEmpty {
word
}
}
}
}
return unpacked
}
internal data class Unbaser(
private val base: Int,
) {
private val selector: Int = when {
base > 62 -> 95
base > 54 -> 62
base > 52 -> 54
else -> 52
}
fun unbase(value: String): Int {
return if (base in 2..36) {
value.toIntOrNull(base) ?: 0
} else {
val dict = ALPHABET[selector]?.toCharArray()?.mapIndexed { index, c ->
c to index
}?.toMap()
var returnVal = 0
val valArray = value.toCharArray().reversed()
for (i in valArray.indices) {
val cipher = valArray[i]
returnVal += (base.toFloat().pow(i) * (dict?.get(cipher) ?: 0)).toInt()
}
returnVal
}
}
companion object {
private val ALPHABET = mapOf<Int, String>(
52 to "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP",
54 to "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR",
62 to "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
95 to " !\"#\$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
)
}
}
}

View File

@ -5,8 +5,18 @@ ext {
extName = 'EnNovelas' extName = 'EnNovelas'
pkgNameSuffix = 'es.ennovelas' pkgNameSuffix = 'es.ennovelas'
extClass = '.EnNovelas' extClass = '.EnNovelas'
extVersionCode = 4 extVersionCode = 5
libVersion = '13' libVersion = '13'
} }
dependencies {
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-okru-extractor'))
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-voe-extractor'))
implementation(project(':lib-streamlare-extractor'))
implementation(project(':lib-uqload-extractor'))
implementation(project(':lib-vudeo-extractor'))
}
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -6,27 +6,39 @@ import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
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.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.Normalizer import java.util.concurrent.TimeUnit
import kotlin.Exception import kotlin.Exception
class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "EnNovelas" override val name = "EnNovelas"
override val baseUrl = "https://www.zonevipz.com/" override val baseUrl = "https://f.ennovelas.net"
override val lang = "es" override val lang = "es"
@ -38,43 +50,72 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun popularAnimeSelector(): String = "#container section.search-videos div.section-content div.row div div.col-xs-6 div.video-post" override fun popularAnimeSelector(): String = ".block-post"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/?op=categories_all&per_page=60&page=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/novelas/page/$page")
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create() val anime = SAnime.create()
anime.setUrlWithoutDomain(changeUrlFormat(element.select("a").attr("href"))) anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("a p").text() anime.title = element.select("a .title").text()
anime.thumbnail_url = element.select("a div.thumb").attr("style") anime.thumbnail_url = element.select("a img").attr("data-img")
.substringAfter("background-image:url(").substringBefore(")")
anime.description = "" anime.description = ""
return anime return anime
} }
private fun changeUrlFormat(link: String): String { override fun popularAnimeNextPageSelector(): String = ".pagination .current ~ a"
val novel = link.substringAfter("/category/").replace("+", "%20")
// "$baseUrl/?cat_name=$novel&op=search&per_page=all"
return "$baseUrl/?cat_name=$novel&op=search&per_page=100000"
}
override fun popularAnimeNextPageSelector(): String = "#container section div.section-content div.paging a:last-of-type"
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
document.select("#col3 div.videobox").forEach { element -> val seasonIds = document.select(".listSeasons li[data-season]")
val ep = SEpisode.create() var noEp = 1F
val noEpisode = getNumberFromEpsString( if (seasonIds.any()) {
element.selectFirst("a:nth-child(2)")!!.text().substringAfter("Cap") seasonIds.reversed().map {
.substringBefore("FIN").substringBefore("fin"), try {
) val headers = headers.newBuilder()
ep.setUrlWithoutDomain(element.selectFirst("a.video200")!!.attr("href")) .add("authority", response.request.url.toString().substringAfter("https://").substringBefore("/wp-content"))
ep.name = "Cap" + element.selectFirst("a:nth-child(2)")!!.text().substringAfter("Cap") .add("referer", response.request.url.toString())
ep.episode_number = noEpisode.toFloat() .add("accept", "*/*")
episodeList.add(ep) .add("accept-language", "es-MX,es;q=0.9,en;q=0.8")
.add("sec-ch-ua", "\"Google Chrome\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"")
.add("sec-ch-ua-mobile", "?0")
.add("sec-ch-ua-platform", "\"Windows\"")
.add("sec-fetch-dest", "empty")
.add("sec-fetch-mode", "cors")
.add("sec-fetch-site", "same-origin")
.add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36")
.add("x-requested-with", "XMLHttpRequest")
.build()
val season = getNumberFromEpsString(it.text())
val tmpClient = client.newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(35, TimeUnit.SECONDS)
.readTimeout(35, TimeUnit.SECONDS)
.build()
tmpClient.newCall(GET("$baseUrl/wp-content/themes/vo2022/temp/ajax/seasons.php?seriesID=${it.attr("data-season")}", headers = headers))
.execute().asJsoup().select(".block-post").forEach { element ->
val ep = SEpisode.create()
val noEpisode = getNumberFromEpsString(element.selectFirst("a .episodeNum span:nth-child(2)")!!.text()).ifEmpty { noEp }
ep.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
ep.name = "T$season - E$noEpisode - Cap" + element.selectFirst("a .title")!!.text().substringAfter("Cap")
ep.episode_number = noEp
episodeList.add(ep)
noEp += 1
}
} catch (_: Exception) { }
}
} else {
document.select(".block-post").forEach { element ->
val ep = SEpisode.create()
val noEpisode = getNumberFromEpsString(element.selectFirst("a .episodeNum span:nth-child(2)")!!.text())
ep.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
ep.name = "Cap" + element.selectFirst("a .title")!!.text().substringAfter("Cap")
ep.episode_number = noEpisode.toFloat()
episodeList.add(ep)
}
} }
return episodeList.sortedByDescending { x -> x.episode_number } return episodeList.reversed()
} }
override fun episodeListSelector() = "uwu" override fun episodeListSelector() = "uwu"
@ -88,11 +129,69 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
document.select("script").forEach { script -> val form = document.selectFirst("#btnServers form")
if (script.data().contains("window.hola_player({")) { val urlRequest = form?.attr("action") ?: ""
val url = script.data().substringAfter("sources: [{src: \"").substringBefore("\",") val watch = form?.selectFirst("input")?.attr("value")
val quality = script.data().substringAfter("res: ").substringBefore(",") val domainRegex = Regex("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)")
videoList.add(Video(url, "${quality}p", url, headers = null)) val domainUrl = domainRegex.findAll(urlRequest).firstOrNull()?.value ?: ""
val mediaType = "application/x-www-form-urlencoded".toMediaType()
val body = "watch=$watch&submit=".toRequestBody(mediaType)
val headers = headers.newBuilder()
.add("authority", domainUrl.substringAfter("//"))
.add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
.add("accept-language", "es-MX,es;q=0.9,en;q=0.8")
.add("content-type", "application/x-www-form-urlencoded")
.add("origin", baseUrl)
.add("referer", "$baseUrl/")
.add("sec-ch-ua-mobile", "?0")
.add("upgrade-insecure-requests", "1")
.build()
client.newCall(POST(urlRequest, headers, body)).execute().asJsoup().select(".serversList li").map {
val frameString = it.attr("abs:data-server")
val link = frameString.substringAfter("src='").substringBefore("'")
.replace("https://api.mycdn.moe/sblink.php?id=", "https://streamsb.net/e/")
.replace("https://api.mycdn.moe/uqlink.php?id=", "https://uqload.co/embed-")
if (link.contains("ok.ru")) {
try {
OkruExtractor(client).videosFromUrl(link).let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("vidmoly")) {
try {
VidmolyExtractor(client).getVideoList(link, "").let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("voe")) {
try {
VoeExtractor(client).videoFromUrl(link, "Voex")?.let { videoList.add(it) }
} catch (_: Exception) {}
}
if (link.contains("vudeo")) {
try {
VudeoExtractor(client).videosFromUrl(link).let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("streamtape")) {
try {
StreamTapeExtractor(client).videoFromUrl(link)?.let { videoList.add(it) }
} catch (_: Exception) {}
}
if (link.contains("uqload")) {
try {
UqloadExtractor(client).videosFromUrl(if (link.contains(".html")) link else "$link.html", "Uqload").let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("dood")) {
try {
DoodExtractor(client).videoFromUrl(link)?.let { videoList.add(it) }
} catch (_: Exception) {}
}
if (link.contains("streamlare")) {
try {
StreamlareExtractor(client).videosFromUrl(link)?.let { videoList.addAll(it) }
} catch (_: Exception) {}
} }
} }
return videoList return videoList
@ -105,7 +204,7 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoFromElement(element: Element) = throw Exception("not used") override fun videoFromElement(element: Element) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "720p") val quality = preferences.getString("preferred_quality", "Voex")
if (quality != null) { if (quality != null) {
val newList = mutableListOf<Video>() val newList = mutableListOf<Video>()
var preferred = 0 var preferred = 0
@ -124,60 +223,74 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return when { return when {
query.isNotBlank() -> GET("$baseUrl/?name=$query&op=categories_all&page=$page") query.isNotBlank() -> GET("$baseUrl/search/$query/page/$page/")
else -> popularAnimeRequest(page) else -> popularAnimeRequest(page)
} }
} }
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeParse(response: Response): AnimesPage {
return popularAnimeFromElement(element) val document = response.asJsoup()
val animeList = mutableListOf<SAnime>()
val hasNextPage = document.select(".pagination .current ~ a").any()
document.select(".block-post").map { element ->
if (element.selectFirst("a")?.attr("href")?.contains("/series/") == true) {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("a .title").text()
anime.thumbnail_url = element.select("a img").attr("data-img")
anime.description = ""
animeList.add(anime)
}
}
return AnimesPage(animeList, hasNextPage)
} }
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("not used")
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() override fun searchAnimeNextPageSelector(): String = throw Exception("not used")
override fun searchAnimeSelector(): String = throw Exception("not used")
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val descriptionElement = document.selectFirst("#inwg")!!.text()
.substringAfter("Notifications:")
.substringBefore("Capitulo")
.substringBefore("Capítulo")
val anime = SAnime.create() val anime = SAnime.create()
val title = document.selectFirst("#inwg h3 span.first-word")!!.text() val title = document.selectFirst("[itemprop=\"name\"] a")?.text() ?: ""
val idx = idxDescription(descriptionElement, title)
val description = descriptionElement.substring(0, idx).trim()
anime.title = title.trim() anime.title = title
anime.description = description.ifEmpty { title } anime.description = document.selectFirst(".postDesc .post-entry div")?.text() ?: title
anime.genre = "novela" anime.genre = document.select("ul.postlist li:nth-child(1) span a").joinToString { it.text() }
anime.status = SAnime.UNKNOWN anime.status = parseStatus(document.select("ul.postlist li:nth-child(8) .getMeta span a").text().trim())
anime.artist = document.selectFirst(".postInfo > .getMeta > span:nth-child(2) > a")?.text() ?: ""
anime.author = document.selectFirst("ul.postlist li:nth-child(3) span a")?.text() ?: ""
return anime return anime
} }
private fun String.removeNonSpacingMarks() = Normalizer.normalize(this, Normalizer.Form.NFD).replace("\\p{Mn}+".toRegex(), "") private fun parseStatus(statusString: String): Int {
return when {
private fun idxDescription(text: String, title: String): Int { statusString.contains("Continuous") -> SAnime.ONGOING
val descriptionLowercase = text.lowercase().removeNonSpacingMarks() statusString.contains("Finished") -> SAnime.COMPLETED
val titleLowercase = title.lowercase().removeNonSpacingMarks() else -> SAnime.UNKNOWN
val idx = descriptionLowercase.lastIndexOf(titleLowercase) }
return if (idx == -1) descriptionLowercase.length - 1 else idx
} }
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector() override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element) override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse?order=added&page=$page") override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
override fun latestUpdatesSelector() = popularAnimeSelector() override fun latestUpdatesSelector() = popularAnimeSelector()
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf(
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru
"Fembed:1080p", "Fembed:720p", "Fembed:480p", "Fembed:360p", "Fembed:240p", "Fembed:144p", // Fembed
"StreamSB:1080p", "StreamSB:720p", "StreamSB:480p", "StreamSB:360p", "StreamSB:240p", "StreamSB:144p", // StreamSB
"Streamlare:1080p", "Streamlare:720p", "Streamlare:480p", "Streamlare:360p", "Streamlare:240p", // Streamlare
"StreamTape", "Voex", "DoodStream", "YourUpload", "MixDrop", "Vidmoly", "Uqload",
)
val videoQualityPref = ListPreference(screen.context).apply { val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" key = "preferred_quality"
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p") entries = qualities
entryValues = arrayOf("1080p", "720p", "480p") entryValues = qualities
setDefaultValue("720p") setDefaultValue("Voex")
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -190,3 +303,23 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
screen.addPreference(videoQualityPref) screen.addPreference(videoQualityPref)
} }
} }
class VidmolyExtractor(private val client: OkHttpClient) {
fun getVideoList(url: String, lang: String): List<Video> {
val body = client.newCall(GET(url)).execute()
.use { it.body.string() }
val playlistUrl = Regex("file:\"(\\S+?)\"").find(body)!!.groupValues.get(1)
val headers = Headers.headersOf("Referer", "https://vidmoly.to")
val playlistData = client.newCall(GET(playlistUrl, headers)).execute()
.use { it.body.string() }
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, "$lang - $quality", videoUrl, headers)
}
}
}