Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,14 @@
ext {
extName = 'MundoDonghua'
extClass = '.MundoDonghua'
extVersionCode = 22
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:playlist-utils'))
implementation(project(':lib:dailymotion-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -0,0 +1,308 @@
package eu.kanade.tachiyomi.animeextension.es.mundodonghua
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.mundodonghua.extractors.JsUnpacker
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.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
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
class MundoDonghua : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "MundoDonghua"
override val baseUrl = "https://www.mundodonghua.com"
override val lang = "es"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector() = "div > div.row > div.item > a"
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/lista-donghuas/$page")
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h5")!!.text().removeSurrounding("\"")
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
override fun popularAnimeNextPageSelector() = "ul.pagination li:last-child a"
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element) =
popularAnimeFromElement(element).apply {
url = url.replace("/ver/", "/donghua/").substringBeforeLast("/")
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lista-episodios/$page")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = baseUrl + document.selectFirst("div.col-md-4.col-xs-12.mb-10 div.row.sm-row > div.side-banner > div.banner-side-serie")!!
.attr("style").substringAfter("background-image: url(").substringBefore(")")
anime.title = document.selectFirst("div.col-md-4.col-xs-12.mb-10 div.row.sm-row div div.sf.fc-dark.ls-title-serie")!!.html()
anime.description = document.selectFirst("section div.row div.col-md-8 div.sm-row p.text-justify")!!.text().removeSurrounding("\"")
anime.genre = document.select("div.col-md-8.col-xs-12 div.sm-row a.generos span.label").joinToString { it.text() }
anime.status = parseStatus(document.select("div.col-md-4.col-xs-12.mb-10 div.row.sm-row div:nth-child(2) div:nth-child(2) p span.badge").text())
return anime
}
override fun episodeListSelector() = "div.sm-row.mt-10 div.donghua-list-scroll ul.donghua-list a"
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epNum = element.attr("href").split("/").last().toFloat()
episode.setUrlWithoutDomain(element.attr("href"))
episode.episode_number = epNum
episode.name = "Episodio $epNum"
return episode
}
private fun getAndUnpack(string: String): Sequence<String> {
return JsUnpacker.unpack(string)
}
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").forEach { script ->
if (script.data().contains("eval(function(p,a,c,k,e")) {
val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)")
packedRegex.findAll(script.data()).map {
it.value
}.toList().map {
val unpack = getAndUnpack(it).first()
if (unpack.contains("amagi_tab")) {
fetchUrls(unpack).map { url ->
try {
VoeExtractor(client).videosFromUrl(url).also(videoList::addAll)
} catch (_: Exception) {}
}
}
if (unpack.contains("fmoon_tab")) {
fetchUrls(unpack).map { url ->
try {
val newHeaders = headers.newBuilder()
.add("authority", url.toHttpUrl().host)
.add("referer", "$baseUrl/")
.add("Origin", "https://${url.toHttpUrl().host}")
.build()
FilemoonExtractor(client).videosFromUrl(url, prefix = "Filemoon:", headers = newHeaders).also(videoList::addAll)
} catch (_: Exception) {}
}
}
if (unpack.contains("protea_tab")) {
try {
val slug = unpack.substringAfter("\"slug\":\"").substringBefore("\"")
val newHeaders = headers.newBuilder()
.add("referer", "${response.request.url}")
.add("authority", baseUrl.substringAfter("//"))
.add("accept", "*/*")
.build()
val slugPlayer = client.newCall(GET("$baseUrl/api_donghua.php?slug=$slug", headers = newHeaders)).execute().asJsoup().body().toString().substringAfter("\"url\":\"").substringBefore("\"")
val videoHeaders = headers.newBuilder()
.add("authority", "www.mdplayer.xyz")
.add("referer", "$baseUrl/")
.build()
val videoId = client.newCall(GET("https://www.mdplayer.xyz/nemonicplayer/dmplayer.php?key=$slugPlayer", headers = videoHeaders))
.execute().asJsoup().body().toString().substringAfter("video-id=\"").substringBefore("\"")
DailymotionExtractor(client, headers).videosFromUrl("https://www.dailymotion.com/embed/video/$videoId", prefix = "Dailymotion:").let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (unpack.contains("asura_tab")) {
fetchUrls(unpack).map { url ->
try {
if (url.contains("redirector")) {
val newHeaders = headers.newBuilder()
.add("authority", "www.mdnemonicplayer.xyz")
.add("accept", "*/*")
.add("origin", baseUrl)
.add("referer", "$baseUrl/")
.build()
PlaylistUtils(client, newHeaders).extractFromHls(url, videoNameGen = { "Asura:$it" }).let { videoList.addAll(it) }
}
} 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("preferred_quality", "VoeCDN")
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality == quality) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
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/busquedas/$query")
genreFilter.state != 0 -> GET("$baseUrl/genero/${genreFilter.toUriPart()}")
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>", ""),
Pair("Acción", "Acción"),
Pair("Artes Marciales", "Artes Marciales"),
Pair("Aventura", "Aventura"),
Pair("Ciencia Ficción", "Ciencia Ficción"),
Pair("Comedia", "Comedia"),
Pair("Comida", "Comida"),
Pair("Cultivación", "Cultivación"),
Pair("Demonios", "Demonios"),
Pair("Deportes", "Deportes"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Escolar", "Escolar"),
Pair("Fantasía", "Fantasía"),
Pair("Harem", "Harem"),
Pair("Harem Inverso", "Harem Inverso"),
Pair("Historico", "Historico"),
Pair("Idols", "Idols"),
Pair("Juegos", "Juegos"),
Pair("Lucha", "Lucha"),
Pair("Magia", "Magia"),
Pair("Mechas", "Mechas"),
Pair("Militar", "Militar"),
Pair("Misterio", "Misterio"),
Pair("Música", "Música"),
Pair("Por Definir", "Por Definir"),
Pair("Psicológico", "Psicológico"),
Pair("Reencarnación", "Reencarnación"),
Pair("Romance", "Romance"),
Pair("Seinen", "Seinen"),
Pair("Shojo", "Shojo"),
Pair("Shonen", "Shonen"),
Pair("Sobrenatural", "Sobrenatural"),
Pair("Sucesos de la Vida", "Sucesos de la Vida"),
Pair("Superpoderes", "Superpoderes"),
Pair("Suspenso", "Suspenso"),
Pair("Terror", "Terror"),
Pair("Vampiros", "Vampiros"),
Pair("Viaje a Otro Mundo", "Viaje a Otro Mundo"),
Pair("Videojuegos", "Videojuegos"),
Pair("Zombis", "Zombis"),
),
)
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()
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 setupPreferenceScreen(screen: PreferenceScreen) {
val qualities = arrayOf(
"VoeCDN",
"Dailymotion:1080p",
"Dailymotion:720p",
"Dailymotion:480p",
"Filemoon:1080p",
"Filemoon:720p",
"Filemoon:480p",
"Asura:1080p",
"Asura:720p",
"Asura:480p",
)
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = qualities
entryValues = qualities
setDefaultValue("VoeCDN")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}

View file

@ -0,0 +1,193 @@
package eu.kanade.tachiyomi.animeextension.es.mundodonghua.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

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.animeextension.es.mundodonghua.extractors
import eu.kanade.tachiyomi.animesource.model.Video
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.Headers
import org.jsoup.Connection
import org.jsoup.Jsoup
import uy.kohesive.injekt.injectLazy
class ProteaExtractor() {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, qualityPrefix: String = "Protea", headers: Headers): List<Video> {
val videoList = mutableListOf<Video>()
runCatching {
val document = Jsoup.connect(url).headers(headers.toMap()).ignoreContentType(true).method(Connection.Method.POST).execute()
if (document!!.body()!!.isNotEmpty()) {
val responseString = document.body().removePrefix("[").removeSuffix("]")
val jObject = json.decodeFromString<JsonObject>(responseString)
val sources = jObject["source"]!!.jsonArray
sources!!.forEach { source ->
var item = source!!.jsonObject
var quality = "$qualityPrefix:${ item["label"]!!.jsonPrimitive.content }"
var urlVideo = item["file"]!!.jsonPrimitive!!.content.removePrefix("//")
var newHeaders = Headers.Builder()
.set("authority", "www.nemonicplayer.xyz")
.set("accept", "*/*")
.set("accept-language", "es-MX,es-419;q=0.9,es;q=0.8,en;q=0.7")
.set("dnt", "1")
.set("referer", "https://www.mundodonghua.com/")
.set("sec-ch-ua", "\"Chromium\";v=\"104\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"104\"")
.set("sec-ch-ua-mobile", "?0")
.set("sec-ch-ua-platform", "\"Windows\"")
.set("sec-fetch-mode", "no-cors")
.set("sec-fetch-dest", "video")
.set("sec-fetch-site", "cross-site")
.set("sec-gpc", "1")
.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36")
.build()
videoList.add(Video("https://$urlVideo", quality, "https://$urlVideo", headers = newHeaders))
}
}
}
return videoList
}
}