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 {
compileOnly(libs.bundles.common)
}
implementation(project(":lib-playlist-utils"))
}

View File

@ -1,72 +1,46 @@
package eu.kanade.tachiyomi.lib.fastreamextractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
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.jsonObject
import okhttp3.Headers
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> {
fun videoFromUrl(url: String, prefix: String = "Fastream:", headers: Headers? = null): 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()
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))
}
}
}
}
val document = client.newCall(GET(url)).execute().asJsoup()
val videoHeaders = (headers?.newBuilder() ?: Headers.Builder())
.set("Referer", "https://fastream.to/")
.set("Origin", "https://fastream.to")
.build()
document.select("script").forEach {
if (it!!.data().contains("jwplayer(jwplayer(\"vplayer\").setup({")) {
val basicUrl = it.data().substringAfter("file: '").substringBefore("',")
videoList.add(Video(basicUrl, prefix, basicUrl, headers = videoHeaders))
} else {
val packedRegex = "eval\\(function\\(p,a,c,k,e,.*\\)\\)".toRegex()
packedRegex.findAll(it.data()).map { packed -> packed.value }.toList().map { eval ->
val unpack = JsUnpacker.unpack(eval)
val serverRegex = "fastream.*?\\.m3u8([^&\">]?)".toRegex()
fetchUrls(unpack.first()).filter { serverRegex.containsMatchIn(it) }.map { url ->
PlaylistUtils(client, videoHeaders).extractFromHls(url, videoNameGen = { "$prefix$it" }).let { videoList.addAll(it) }
}
}
}
}
} catch (_: Exception) {
}
} catch (_: Exception) {}
return videoList
}
}

View File

@ -5,14 +5,19 @@ ext {
extName = 'Animefenix'
pkgNameSuffix = 'es.animefenix'
extClass = '.Animefenix'
extVersionCode = 27
extVersionCode = 28
libVersion = '13'
}
dependencies {
implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-yourupload-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.Video
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.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 okhttp3.OkHttpClient
@ -90,40 +95,34 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.data().substringAfter("src=\"").substringBefore("\"")
} catch (e: Exception) { "" }
when {
realUrl.contains("ok.ru") -> {
val okruVideos = OkruExtractor(client).videosFromUrl(realUrl)
videoList.addAll(okruVideos)
}
realUrl.contains("/stream/amz.php?") -> {
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))
try {
when {
realUrl.contains("ok.ru") || realUrl.contains("okru") -> OkruExtractor(client).videosFromUrl(realUrl).let { videoList.addAll(it) }
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("streamwish") || realUrl.contains("embedwish") -> StreamWishExtractor(client, headers).videosFromUrl(realUrl).let { videoList.addAll(it) }
realUrl.contains("streamtape") -> StreamTapeExtractor(client).videoFromUrl(realUrl, "StreamTape")?.let { videoList.add(it) }
realUrl.contains("mp4upload") -> Mp4uploadExtractor(client).videosFromUrl(realUrl, headers).let { videoList.addAll(it) }
realUrl.contains("yourupload") -> YourUploadExtractor(client).videoFromUrl(realUrl, headers = headers).let { videoList.addAll(it) }
realUrl.contains("voe") -> VoeExtractor(client).videoFromUrl(realUrl)?.let { videoList.add(it) }
realUrl.contains("/stream/amz.php?") -> {
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") -> {
val video = realUrl.substringAfter("/stream/fl.php?v=")
try {
realUrl.contains("/stream/fl.php") -> {
val video = realUrl.substringAfter("/stream/fl.php?v=")
if (client.newCall(GET(video)).execute().code == 200) {
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") -> {
val videos = Mp4uploadExtractor(client).videosFromUrl(realUrl, headers)
videoList.addAll(videos)
}
}
} catch (_: Exception) { }
}
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 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 }
@ -180,6 +180,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (typeFilter.state != 0) {
filterUrl += "&type[]=${typeFilter.toUriPart()}"
} // search by type
filterUrl += "&order=${orderByFilter.toUriPart()}"
filterUrl += "&page=$page" // add page
return when {
@ -188,6 +189,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
else -> GET("$baseUrl/animes?order=likes&page=$page ")
}
}
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
@ -236,6 +238,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
TagFilter("Generos", checkboxesFrom(genreList)),
StateFilter(),
TypeFilter(),
OrderByFilter(),
YearFilter(),
)
@ -292,9 +295,11 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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", "2022")
private class YearFilter : AnimeFilter.Text("Año")
private class StateFilter : UriPartFilter(
"Estado",
arrayOf(
@ -305,6 +310,7 @@ class Animefenix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Pair("En Cuarentena", "4"),
),
)
private class TypeFilter : UriPartFilter(
"Tipo",
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>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
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 {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf(
"Okru:1080p", "Okru:720p", "Okru:480p", "Okru:360p", "Okru:240p", "Okru:144p", // Okru
"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",
)
entries = qualities
entryValues = qualities
setDefaultValue("Amazon")
summary = "%s"

View File

@ -5,7 +5,7 @@ ext {
extName = 'AnimeFLV'
pkgNameSuffix = 'es.animeflv'
extClass = '.AnimeFlv'
extVersionCode = 49
extVersionCode = 50
libVersion = '13'
}
@ -14,6 +14,7 @@ dependencies {
implementation(project(':lib-streamtape-extractor'))
implementation(project(':lib-okru-extractor'))
implementation(project(':lib-dood-extractor'))
implementation(project(':lib-streamwish-extractor'))
}
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.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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
@ -113,19 +113,22 @@ class AnimeFlv : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
jObject["SUB"]!!.jsonArray!!.forEach { servers ->
val json = servers!!.jsonObject
val quality = json!!["title"]!!.jsonPrimitive!!.content
var url = json!!["code"]!!.jsonPrimitive!!.content
val url = json!!["code"]!!.jsonPrimitive!!.content
val extractedVideos = runCatching {
when (quality) {
"Stape" -> {
val stapeUrl = json!!["url"]!!.jsonPrimitive!!.content
StreamTapeExtractor(client).videoFromUrl(stapeUrl)
?.let(::listOf)
StreamTapeExtractor(client).videoFromUrl(stapeUrl)?.let(::listOf)
}
"Doodstream" ->
DoodExtractor(client).videoFromUrl(url, "DoodStream", false)
?.let(::listOf)
"Doodstream" -> DoodExtractor(client).videoFromUrl(url, "DoodStream", false)?.let(::listOf)
"Okru" -> OkruExtractor(client).videosFromUrl(url)
"YourUpload" -> YourUploadExtractor(client).videoFromUrl(url, headers = headers)
"SW" -> {
val docHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, "StreamWish")
}
else -> null
}
}.getOrNull() ?: emptyList<Video>()

View File

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

View File

@ -4,13 +4,13 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.animeyt.extractors.FastreamExtractor
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 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'
pkgNameSuffix = 'es.ennovelas'
extClass = '.EnNovelas'
extVersionCode = 4
extVersionCode = 5
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"

View File

@ -6,27 +6,39 @@ 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.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.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.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.Normalizer
import java.util.concurrent.TimeUnit
import kotlin.Exception
class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "EnNovelas"
override val baseUrl = "https://www.zonevipz.com/"
override val baseUrl = "https://f.ennovelas.net"
override val lang = "es"
@ -38,43 +50,72 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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 {
val anime = SAnime.create()
anime.setUrlWithoutDomain(changeUrlFormat(element.select("a").attr("href")))
anime.title = element.select("a p").text()
anime.thumbnail_url = element.select("a div.thumb").attr("style")
.substringAfter("background-image:url(").substringBefore(")")
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 = ""
return anime
}
private fun changeUrlFormat(link: String): String {
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 popularAnimeNextPageSelector(): String = ".pagination .current ~ a"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
document.select("#col3 div.videobox").forEach { element ->
val ep = SEpisode.create()
val noEpisode = getNumberFromEpsString(
element.selectFirst("a:nth-child(2)")!!.text().substringAfter("Cap")
.substringBefore("FIN").substringBefore("fin"),
)
ep.setUrlWithoutDomain(element.selectFirst("a.video200")!!.attr("href"))
ep.name = "Cap" + element.selectFirst("a:nth-child(2)")!!.text().substringAfter("Cap")
ep.episode_number = noEpisode.toFloat()
episodeList.add(ep)
val seasonIds = document.select(".listSeasons li[data-season]")
var noEp = 1F
if (seasonIds.any()) {
seasonIds.reversed().map {
try {
val headers = headers.newBuilder()
.add("authority", response.request.url.toString().substringAfter("https://").substringBefore("/wp-content"))
.add("referer", response.request.url.toString())
.add("accept", "*/*")
.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"
@ -88,11 +129,69 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
document.select("script").forEach { script ->
if (script.data().contains("window.hola_player({")) {
val url = script.data().substringAfter("sources: [{src: \"").substringBefore("\",")
val quality = script.data().substringAfter("res: ").substringBefore(",")
videoList.add(Video(url, "${quality}p", url, headers = null))
val form = document.selectFirst("#btnServers form")
val urlRequest = form?.attr("action") ?: ""
val watch = form?.selectFirst("input")?.attr("value")
val domainRegex = Regex("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)")
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
@ -105,7 +204,7 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "720p")
val quality = preferences.getString("preferred_quality", "Voex")
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
@ -124,60 +223,74 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return when {
query.isNotBlank() -> GET("$baseUrl/?name=$query&op=categories_all&page=$page")
query.isNotBlank() -> GET("$baseUrl/search/$query/page/$page/")
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
return popularAnimeFromElement(element)
override fun searchAnimeParse(response: Response): AnimesPage {
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 searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("not used")
override fun searchAnimeSelector(): String = throw Exception("not used")
override fun animeDetailsParse(document: Document): SAnime {
val descriptionElement = document.selectFirst("#inwg")!!.text()
.substringAfter("Notifications:")
.substringBefore("Capitulo")
.substringBefore("Capítulo")
val anime = SAnime.create()
val title = document.selectFirst("#inwg h3 span.first-word")!!.text()
val idx = idxDescription(descriptionElement, title)
val description = descriptionElement.substring(0, idx).trim()
val title = document.selectFirst("[itemprop=\"name\"] a")?.text() ?: ""
anime.title = title.trim()
anime.description = description.ifEmpty { title }
anime.genre = "novela"
anime.status = SAnime.UNKNOWN
anime.title = title
anime.description = document.selectFirst(".postDesc .post-entry div")?.text() ?: title
anime.genre = document.select("ul.postlist li:nth-child(1) span a").joinToString { it.text() }
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
}
private fun String.removeNonSpacingMarks() = Normalizer.normalize(this, Normalizer.Form.NFD).replace("\\p{Mn}+".toRegex(), "")
private fun idxDescription(text: String, title: String): Int {
val descriptionLowercase = text.lowercase().removeNonSpacingMarks()
val titleLowercase = title.lowercase().removeNonSpacingMarks()
val idx = descriptionLowercase.lastIndexOf(titleLowercase)
return if (idx == -1) descriptionLowercase.length - 1 else idx
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("Continuous") -> SAnime.ONGOING
statusString.contains("Finished") -> 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 latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
override fun latestUpdatesSelector() = popularAnimeSelector()
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 {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p")
entryValues = arrayOf("1080p", "720p", "480p")
setDefaultValue("720p")
entries = qualities
entryValues = qualities
setDefaultValue("Voex")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -190,3 +303,23 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
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)
}
}
}