This commit is contained in:
imper1aldev
2023-03-19 12:25:42 -06:00
committed by GitHub
parent 04fbbb1bbb
commit 68db5b45b7
3 changed files with 311 additions and 62 deletions

View File

@ -5,8 +5,16 @@ ext {
extName = 'EnNovelas'
pkgNameSuffix = 'es.ennovelas'
extClass = '.EnNovelas'
extVersionCode = 1
extVersionCode = 2
libVersion = '13'
}
dependencies {
implementation(project(':lib-okru-extractor'))
implementation project(path: ':lib-streamsb-extractor')
implementation project(path: ':lib-fembed-extractor')
implementation project(path: ':lib-voe-extractor')
implementation project(path: ':lib-streamtape-extractor')
implementation project(path: ':lib-dood-extractor')
}
apply from: "$rootDir/common.gradle"

View File

@ -4,29 +4,40 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.es.ennovelas.extractors.StreamlareExtractor
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.fembedextractor.FembedExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
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.lang.Exception
import java.text.Normalizer
import kotlin.Exception
class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "EnNovelas"
override val baseUrl = "https://www.ennovelas.com"
override val baseUrl = "https://w.ennovelas.net"
override val lang = "es"
@ -38,42 +49,59 @@ 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/telenovelas/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")
return "$baseUrl/?cat_name=$novel&op=search&per_page=all"
}
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 {
val headers = headers.newBuilder()
.add("authority", "w.ennovelas.net")
.add("referer", response.request.url.toString())
.add("accept", "*/*")
.add("accept-language", "es-MX,es;q=0.9,en;q=0.8")
.add("sec-fetch-mode", "cors")
.add("x-requested-with", "XMLHttpRequest")
.build()
val season = getNumberFromEpsString(it.text())
client.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
}
}
} 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"
@ -87,16 +115,126 @@ 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("data-server")
val link = urlResolve(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("sbembed.com") || link.contains("sbembed1.com") || link.contains("sbplay.org") ||
link.contains("sbvideo.net") || link.contains("streamsb.net") || link.contains("sbplay.one") ||
link.contains("cloudemb.com") || link.contains("playersb.com") || link.contains("tubesb.com") ||
link.contains("sbplay1.com") || link.contains("embedsb.com") || link.contains("watchsb.com") ||
link.contains("sbplay2.com") || link.contains("japopav.tv") || link.contains("viewsb.com") ||
link.contains("sbfast") || link.contains("sbfull.com") || link.contains("javplaya.com") ||
link.contains("ssbstream.net") || link.contains("p1ayerjavseen.com") || link.contains("sbthe.com") ||
link.contains("sbchill.com") || link.contains("sblongvu.com") || link.contains("sbanh.com") ||
link.contains("sblanh.com") || link.contains("sbhight.com") || link.contains("sbbrisk.com") ||
link.contains("sbspeed.com") || link.contains("multimovies.website")
) {
try {
StreamSBExtractor(client).videosFromUrl(link, headers).let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("vidmoly")) {
try {
VidmolyExtractor(client).getVideoList(link, "").let { videoList.addAll(it) }
} catch (_: Exception) {}
}
if (link.contains("fembed") || link.contains("anime789.com") || link.contains("24hd.club") ||
link.contains("fembad.org") || link.contains("vcdn.io") || link.contains("sharinglink.club") ||
link.contains("moviemaniac.org") || link.contains("votrefiles.club") || link.contains("femoload.xyz") ||
link.contains("albavido.xyz") || link.contains("feurl.com") || link.contains("dailyplanet.pw") ||
link.contains("ncdnstm.com") || link.contains("jplayer.net") || link.contains("xstreamcdn.com") ||
link.contains("fembed-hd.com") || link.contains("gcloud.live") || link.contains("vcdnplay.com") ||
link.contains("superplayxyz.club") || link.contains("vidohd.com") || link.contains("vidsource.me") ||
link.contains("cinegrabber.com") || link.contains("votrefile.xyz") || link.contains("zidiplay.com") ||
link.contains("ndrama.xyz") || link.contains("fcdn.stream") || link.contains("mediashore.org") ||
link.contains("suzihaza.com") || link.contains("there.to") || link.contains("femax20.com") ||
link.contains("javstream.top") || link.contains("viplayer.cc") || link.contains("sexhd.co") ||
link.contains("fembed.net") || link.contains("mrdhan.com") || link.contains("votrefilms.xyz") ||
link.contains("embedsito.com") || link.contains("dutrag.com") || link.contains("youvideos.ru") ||
link.contains("streamm4u.club") || link.contains("moviepl.xyz") || link.contains("asianclub.tv") ||
link.contains("vidcloud.fun") || link.contains("fplayer.info") || link.contains("diasfem.com") ||
link.contains("javpoll.com") || link.contains("reeoov.tube") || link.contains("suzihaza.com") ||
link.contains("ezsubz.com") || link.contains("vidsrc.xyz") || link.contains("diampokusy.com") ||
link.contains("diampokusy.com") || link.contains("i18n.pw") || link.contains("vanfem.com") ||
link.contains("fembed9hd.com") || link.contains("votrefilms.xyz") || link.contains("watchjavnow.xyz")
) {
try {
FembedExtractor(client).videosFromUrl(link, redirect = !link.contains("fembed")).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("streamtape")) {
try {
StreamTapeExtractor(client).videoFromUrl(link)?.let { videoList.add(it) }
} catch (_: Exception) {}
}
if (link.contains("uqload")) {
// uqload.co/embed-rxkjujtas0po.html
try {
val headers = headers.newBuilder()
.add("Accept", "*/*")
.add("authority", "uqload.co")
.add("Accept-Language", "es-MX,es;q=0.9,en;q=0.8")
.add("Connection", "keep-alive")
.add("Range", "bytes=0-")
.add("Referer", "https://uqload.co/")
.add("Sec-Fetch-Dest", "video")
.add("Sec-Fetch-Mode", "no-cors")
.add("Sec-Fetch-Site", "same-site")
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36")
.add("sec-ch-ua", "\"Google Chrome\";v=\"111\", \"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"111\"")
.add("sec-ch-ua-mobile", "?0")
.add("sec-ch-ua-platform", "\"Windows\"")
.build()
UqloadExtractor(client).videoFromUrl(if (link.contains(".html")) link else "$link.html", headers, "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.add(it) }
} catch (_: Exception) {}
}
}
return videoList
}
private fun urlResolve(url: String) = if (url.startsWith("//")) "https:$url" else url
override fun videoListSelector() = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
@ -104,7 +242,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
@ -123,60 +261,66 @@ 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\"]")?.text() ?: ""
val description = document.selectFirst(".postDesc p:nth-child(2)")?.text() ?: ""
val genres = document.select("ul.postlist li:nth-child(1) span a").joinToString { it.text() }
anime.title = title.trim()
anime.title = title
anime.description = description.ifEmpty { title }
anime.genre = "novela"
anime.genre = genres
anime.status = SAnime.UNKNOWN
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
}
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 ->
@ -189,3 +333,45 @@ class EnNovelas : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
screen.addPreference(videoQualityPref)
}
}
class VidmolyExtractor(private val client: OkHttpClient) {
private val REGEX_PLAYLIST = Regex("file:\"(\\S+?)\"")
fun getVideoList(url: String, lang: String): List<Video> {
val body = client.newCall(GET(url)).execute()
.use { it.body.string() }
val playlistUrl = REGEX_PLAYLIST.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)
}
}
}
class UqloadExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, headers: Headers, quality: String): List<Video> {
val videoList = mutableListOf<Video>()
return try {
val document = client.newCall(GET(url)).execute().asJsoup()
document.select("script").map {
if (it.data().contains("var player = new")) {
val basicUrl =
it.data().substringAfter("sources: [\"").substringBefore("\"],")
videoList.add(Video(basicUrl, quality, basicUrl, headers = headers))
}
}
videoList
} catch (e: Exception) {
videoList
}
}
}

View File

@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.animeextension.es.ennovelas.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.POST
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.Headers
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
class StreamlareExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String): Video? {
val id = url.substringAfter("/e/").substringBefore("?poster")
val videoUrlResponse =
client.newCall(POST("https://slwatch.co/api/video/stream/get?id=$id")).execute()
.asJsoup()
json.decodeFromString<JsonObject>(
videoUrlResponse.select("body").text(),
)["result"]?.jsonObject?.forEach { quality ->
if (quality.toString().contains("file=\"")) {
val videoUrl = quality.toString().substringAfter("file=\"").substringBefore("\"").trim()
val type = if (videoUrl.contains(".m3u8")) "HSL" else "MP4"
val headers = Headers.Builder()
.add("authority", videoUrl.substringBefore("/hls").substringBefore("/mp4"))
.add("origin", "https://slwatch.co")
.add("referer", "https://slwatch.co/e/" + url.substringAfter("/e/"))
.add(
"sec-ch-ua",
"\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Google Chrome\";v=\"108\"",
)
.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", "cross-site")
.add(
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/),108.0.0.0 Safari/537.36",
)
.add("Accept-Encoding", "gzip, deflate, br")
.add("accept", "*/*")
.add(
"accept-language",
"es-MX,es-419;q=0.9,es;q=0.8,en;q=0.7,zh-TW;q=0.6,zh-CN;q=0.5,zh;q=0.4",
)
.build()
return Video(videoUrl, "Streamlare:$type", videoUrl, headers = headers)
}
}
return null
}
}