Added Providers
This commit is contained in:
parent
46cd183528
commit
dd683a98d2
54 changed files with 2704 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
**/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
.vscode
|
12
AkwamProvider/build.gradle.kts
Normal file
12
AkwamProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" , "Anime" , "Cartoon" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=akwam.to&sz=24"
|
||||||
|
}
|
2
AkwamProvider/src/main/AndroidManifest.xml
Normal file
2
AkwamProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.akwam"/>
|
11
AkwamProvider/src/main/kotlin/com/akwam/AkwamPlugin.kt
Normal file
11
AkwamProvider/src/main/kotlin/com/akwam/AkwamPlugin.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.akwam
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class AkwamPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(Akwam())
|
||||||
|
}
|
||||||
|
}
|
221
AkwamProvider/src/main/kotlin/com/akwam/AkwamProvider.kt
Normal file
221
AkwamProvider/src/main/kotlin/com/akwam/AkwamProvider.kt
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package com.akwam
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class Akwam : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://akwam.to"
|
||||||
|
override var name = "Akwam"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie, TvType.Anime, TvType.Cartoon)
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = select("a.box").attr("href") ?: return null
|
||||||
|
if (url.contains("/games/") || url.contains("/programs/")) return null
|
||||||
|
val poster = select("picture > img")
|
||||||
|
val title = poster.attr("alt")
|
||||||
|
val posterUrl = poster.attr("data-src")
|
||||||
|
val year = select(".badge-secondary").text().toIntOrNull()
|
||||||
|
|
||||||
|
// If you need to differentiate use the url.
|
||||||
|
return MovieSearchResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
this@Akwam.name,
|
||||||
|
TvType.TvSeries,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/movies?page=" to "Movies",
|
||||||
|
"$mainUrl/series?page=" to "Series",
|
||||||
|
"$mainUrl/shows?page=" to "Shows"
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div.col-lg-auto.col-md-4.col-6.mb-12").mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val url = "$mainUrl/search?q=$query"
|
||||||
|
val doc = app.get(url).document
|
||||||
|
return doc.select("div.col-lg-auto").mapNotNull {
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toEpisode(): Episode {
|
||||||
|
val a = select("a.text-white")
|
||||||
|
val url = a.attr("href")
|
||||||
|
val title = a.text()
|
||||||
|
val thumbUrl = select("picture > img").attr("src")
|
||||||
|
val date = select("p.entry-date").text()
|
||||||
|
return newEpisode(url) {
|
||||||
|
name = title
|
||||||
|
episode = title.getIntFromText()
|
||||||
|
posterUrl = thumbUrl
|
||||||
|
addDate(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val isMovie = url.contains("/movie/")
|
||||||
|
val title = doc.select("h1.entry-title").text()
|
||||||
|
val posterUrl = doc.select("picture > img").attr("src")
|
||||||
|
|
||||||
|
val year =
|
||||||
|
doc.select("div.font-size-16.text-white.mt-2").firstOrNull {
|
||||||
|
it.text().contains("السنة")
|
||||||
|
}?.text()?.getIntFromText()
|
||||||
|
|
||||||
|
// A bit iffy to parse twice like this, but it'll do.
|
||||||
|
val duration =
|
||||||
|
doc.select("div.font-size-16.text-white.mt-2").firstOrNull {
|
||||||
|
it.text().contains("مدة الفيلم")
|
||||||
|
}?.text()?.getIntFromText()
|
||||||
|
|
||||||
|
val synopsis = doc.select("div.widget-body p:first-child").text()
|
||||||
|
|
||||||
|
val rating = doc.select("span.mx-2").text().split("/").lastOrNull()?.toRatingInt()
|
||||||
|
|
||||||
|
val tags = doc.select("div.font-size-16.d-flex.align-items-center.mt-3 > a").map {
|
||||||
|
it.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
val actors = doc.select("div.widget-body > div > div.entry-box > a").mapNotNull {
|
||||||
|
val name = it?.selectFirst("div > .entry-title")?.text() ?: return@mapNotNull null
|
||||||
|
val image = it.selectFirst("div > img")?.attr("src") ?: return@mapNotNull null
|
||||||
|
Actor(name, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
val recommendations =
|
||||||
|
doc.select("div > div.widget-body > div.row > div > div.entry-box").mapNotNull {
|
||||||
|
val recTitle = it?.selectFirst("div.entry-body > .entry-title > .text-white")
|
||||||
|
?: return@mapNotNull null
|
||||||
|
val href = recTitle.attr("href") ?: return@mapNotNull null
|
||||||
|
val name = recTitle.text() ?: return@mapNotNull null
|
||||||
|
val poster = it.selectFirst(".entry-image > a > picture > img")?.attr("data-src")
|
||||||
|
?: return@mapNotNull null
|
||||||
|
MovieSearchResponse(name, href, this.name, TvType.Movie, fixUrl(poster))
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isMovie) {
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.rating = rating
|
||||||
|
this.tags = tags
|
||||||
|
this.duration = duration
|
||||||
|
this.recommendations = recommendations
|
||||||
|
addActors(actors)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = doc.select("div.bg-primary2.p-4.col-lg-4.col-md-6.col-12").map {
|
||||||
|
it.toEpisode()
|
||||||
|
}.let {
|
||||||
|
val isReversed = (it.lastOrNull()?.episode ?: 1) < (it.firstOrNull()?.episode ?: 0)
|
||||||
|
if (isReversed)
|
||||||
|
it.reversed()
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
|
||||||
|
newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
|
||||||
|
this.duration = duration
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.tags = tags.filterNotNull()
|
||||||
|
this.rating = rating
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.recommendations = recommendations
|
||||||
|
addActors(actors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// // Maybe possible to not use the url shortener but cba investigating that.
|
||||||
|
// private suspend fun skipUrlShortener(url: String): AppResponse {
|
||||||
|
// return app.get(app.get(url).document.select("a.download-link").attr("href"))
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun getQualityFromId(id: Int?): Qualities {
|
||||||
|
return when (id) {
|
||||||
|
2 -> Qualities.P360 // Extrapolated
|
||||||
|
3 -> Qualities.P480
|
||||||
|
4 -> Qualities.P720
|
||||||
|
5 -> Qualities.P1080
|
||||||
|
else -> Qualities.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val doc = app.get(data).document
|
||||||
|
|
||||||
|
val links = doc.select("div.tab-content.quality").map { element ->
|
||||||
|
val quality = getQualityFromId(element.attr("id").getIntFromText())
|
||||||
|
element.select(".col-lg-6 > a:contains(تحميل)").map { linkElement ->
|
||||||
|
if (linkElement.attr("href").contains("/download/")) {
|
||||||
|
Pair(
|
||||||
|
linkElement.attr("href"),
|
||||||
|
quality,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val url = "$mainUrl/download${
|
||||||
|
linkElement.attr("href").split("/link")[1]
|
||||||
|
}${data.split("/movie|/episode|/show/episode".toRegex())[1]}"
|
||||||
|
Pair(
|
||||||
|
url,
|
||||||
|
quality,
|
||||||
|
)
|
||||||
|
// just in case if they add the shorts urls again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flatten()
|
||||||
|
|
||||||
|
links.map {
|
||||||
|
val linkDoc = app.get(it.first).document
|
||||||
|
val button = linkDoc.select("div.btn-loader > a")
|
||||||
|
val url = button.attr("href")
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
url,
|
||||||
|
this.mainUrl,
|
||||||
|
it.second.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
AnimeBlkomProvider/build.gradle.kts
Normal file
12
AnimeBlkomProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "Anime" , "AnimeMovie" , "OVA" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=animeblkom.net&sz=24"
|
||||||
|
}
|
2
AnimeBlkomProvider/src/main/AndroidManifest.xml
Normal file
2
AnimeBlkomProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.animeblkom"/>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.animeblkom
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class AnimeBlkomPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(AnimeBlkom())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.animeblkom
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class AnimeBlkom : MainAPI() {
|
||||||
|
override var mainUrl = "https://animeblkom.net"
|
||||||
|
override var name = "AnimeBlkom"
|
||||||
|
override var lang = "ar"
|
||||||
|
override val hasMainPage = true
|
||||||
|
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Anime,
|
||||||
|
TvType.AnimeMovie,
|
||||||
|
TvType.OVA,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse {
|
||||||
|
val url = select("div.poster a").attr("href")
|
||||||
|
val name = select("div.name a").text()
|
||||||
|
val poster = mainUrl + select("div.poster img").attr("data-original")
|
||||||
|
val year = select("div[title=\"سنة الانتاج\"]").text().toIntOrNull()
|
||||||
|
val episodesNumber = select("div[title=\"عدد الحلقات\"]").text().toIntOrNull()
|
||||||
|
val tvType = select("div[title=\"النوع\"]").text().let { if(it.contains("فيلم|خاصة".toRegex())) TvType.AnimeMovie else if(it.contains("أوفا|أونا".toRegex())) TvType.OVA else TvType.Anime }
|
||||||
|
return newAnimeSearchResponse(
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
tvType,
|
||||||
|
) {
|
||||||
|
addDubStatus(false, episodesNumber)
|
||||||
|
this.year = year
|
||||||
|
this.posterUrl = poster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/anime-list?sort_by=rate&page=" to "Most rated",
|
||||||
|
"$mainUrl/anime-list?sort_by=created_at&page=" to "Recently added",
|
||||||
|
"$mainUrl/anime-list?states=finished&page=" to "Completed"
|
||||||
|
)
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div.content-inner")
|
||||||
|
.mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val q = query.replace(" ","+")
|
||||||
|
return app.get("$mainUrl/search?query=$q").document.select("div.content.ratable").map {
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
|
||||||
|
val title = doc.select("span h1").text().replace("\\(.*".toRegex(),"")
|
||||||
|
val poster = mainUrl + doc.select("div.poster img").attr("data-original")
|
||||||
|
val description = doc.select(".story p").text()
|
||||||
|
val genre = doc.select("p.genres a").map {
|
||||||
|
it.text()
|
||||||
|
}
|
||||||
|
val year = doc.select(".info-table div:contains(تاريخ الانتاج) span.info").text().split("-")[0].toIntOrNull()
|
||||||
|
val status = doc.select(".info-table div:contains(حالة الأنمي) span.info").text().let { if(it.contains("مستمر")) ShowStatus.Ongoing else ShowStatus.Completed }
|
||||||
|
val nativeName = doc.select("span[title=\"الاسم باليابانية\"]").text().replace(".*:".toRegex(),"")
|
||||||
|
val type = doc.select("h1 small").text().let {
|
||||||
|
if (it.contains("movie")) TvType.AnimeMovie
|
||||||
|
if (it.contains("ova|ona".toRegex())) TvType.OVA
|
||||||
|
else TvType.Anime
|
||||||
|
}
|
||||||
|
|
||||||
|
val malId = doc.select("a.blue.cta:contains(المزيد من المعلومات)").attr("href").replace(".*e\\/|\\/.*".toRegex(),"").toInt()
|
||||||
|
val episodes = arrayListOf<Episode>()
|
||||||
|
val episodeElements = doc.select(".episode-link")
|
||||||
|
if(episodeElements.isEmpty()) {
|
||||||
|
episodes.add(Episode(
|
||||||
|
url,
|
||||||
|
"Watch",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
episodeElements.map {
|
||||||
|
val a = it.select("a")
|
||||||
|
episodes.add(Episode(
|
||||||
|
mainUrl + a.attr("href"),
|
||||||
|
a.text().replace(":"," "),
|
||||||
|
episode = a.select("span").not(".pull-left").last()?.text()?.toIntOrNull()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newAnimeLoadResponse(title, url, type) {
|
||||||
|
addMalId(malId)
|
||||||
|
japName = nativeName
|
||||||
|
engName = title
|
||||||
|
posterUrl = poster
|
||||||
|
this.year = year
|
||||||
|
addEpisodes(DubStatus.Subbed, episodes) // TODO CHECK
|
||||||
|
plot = description
|
||||||
|
tags = genre
|
||||||
|
|
||||||
|
showStatus = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val doc = app.get(data).document
|
||||||
|
doc.select(".panel .panel-body a").forEach {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
it.attr("title") + " " + it.select("small").text(),
|
||||||
|
it.attr("href"),
|
||||||
|
this.mainUrl,
|
||||||
|
it.text().replace("p.*| ".toRegex(),"").toInt(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
CimaNowProvider/build.gradle.kts
Normal file
12
CimaNowProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=cimanow.cc&sz=24"
|
||||||
|
}
|
2
CimaNowProvider/src/main/AndroidManifest.xml
Normal file
2
CimaNowProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.cimanow"/>
|
11
CimaNowProvider/src/main/kotlin/com/cimanow/CimaNowPlugin.kt
Normal file
11
CimaNowProvider/src/main/kotlin/com/cimanow/CimaNowPlugin.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.cimanow
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class CimaNowPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(CimaNow())
|
||||||
|
}
|
||||||
|
}
|
164
CimaNowProvider/src/main/kotlin/com/cimanow/CimaNowProvider.kt
Normal file
164
CimaNowProvider/src/main/kotlin/com/cimanow/CimaNowProvider.kt
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package com.cimanow
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class CimaNow : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://cimanow.cc"
|
||||||
|
override var name = "CimaNow"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie)
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = this.attr("href")
|
||||||
|
val posterUrl = select("img")?.attr("data-src")
|
||||||
|
var title = select("li[aria-label=\"title\"]").html().replace(" <em>.*|\\\\n".toRegex(), "").replace(" ", "")
|
||||||
|
val year = select("li[aria-label=\"year\"]").text().toIntOrNull()
|
||||||
|
val tvType = if (url.contains("فيلم|مسرحية|حفلات".toRegex())) TvType.Movie else TvType.TvSeries
|
||||||
|
val quality = select("li[aria-label=\"ribbon\"]").first()?.text()?.replace(" |-|1080|720".toRegex(), "")
|
||||||
|
val dubEl = select("li[aria-label=\"ribbon\"]:nth-child(2)").isNotEmpty()
|
||||||
|
val dubStatus = if(dubEl) select("li[aria-label=\"ribbon\"]:nth-child(2)").text().contains("مدبلج")
|
||||||
|
else select("li[aria-label=\"ribbon\"]:nth-child(1)").text().contains("مدبلج")
|
||||||
|
if(dubStatus) title = "$title (مدبلج)"
|
||||||
|
return MovieSearchResponse(
|
||||||
|
"$title ${select("li[aria-label=\"ribbon\"]:contains(الموسم)").text()}",
|
||||||
|
url,
|
||||||
|
this@CimaNow.name,
|
||||||
|
tvType,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
null,
|
||||||
|
quality = getQualityFromString(quality)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
|
||||||
|
val doc = app.get("$mainUrl/home", headers = mapOf("user-agent" to "MONKE")).document
|
||||||
|
val pages = doc.select("section").not("section:contains(أختر وجهتك المفضلة)").not("section:contains(تم اضافته حديثاً)").apmap {
|
||||||
|
val name = it.select("span").html().replace("<em>.*| <i c.*".toRegex(), "")
|
||||||
|
val list = it.select("a").mapNotNull {
|
||||||
|
if(it.attr("href").contains("$mainUrl/category/|$mainUrl/الاكثر-مشاهدة/".toRegex())) return@mapNotNull null
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
HomePageList(name, list)
|
||||||
|
}
|
||||||
|
return HomePageResponse(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val result = arrayListOf<SearchResponse>()
|
||||||
|
val doc = app.get("$mainUrl/page/1/?s=$query").document
|
||||||
|
val paginationElement = doc.select("ul[aria-label=\"pagination\"]")
|
||||||
|
doc.select("section article a").map {
|
||||||
|
val postUrl = it.attr("href")
|
||||||
|
if(it.select("li[aria-label=\"episode\"]").isNotEmpty()) return@map
|
||||||
|
if(postUrl.contains("$mainUrl/expired-download/|$mainUrl/افلام-اون-لاين/".toRegex())) return@map
|
||||||
|
result.add(it.toSearchResponse()!!)
|
||||||
|
}
|
||||||
|
if(paginationElement.isNotEmpty()) {
|
||||||
|
val max = paginationElement.select("li").not("li.active").last()?.text()?.toIntOrNull()
|
||||||
|
if (max != null) {
|
||||||
|
if(max > 5) return result.distinct().sortedBy { it.name }
|
||||||
|
(2..max!!).toList().apmap {
|
||||||
|
app.get("$mainUrl/page/$it/?s=$query\"").document.select("section article a").map { element ->
|
||||||
|
val postUrl = element.attr("href")
|
||||||
|
if(element.select("li[aria-label=\"episode\"]").isNotEmpty()) return@map
|
||||||
|
if(postUrl.contains("$mainUrl/expired-download/|$mainUrl/افلام-اون-لاين/".toRegex())) return@map
|
||||||
|
result.add(element.toSearchResponse()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.distinct().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val posterUrl = doc.select("body > script:nth-child(3)").html().replace(".*,\"image\":\"|\".*".toRegex(),"").ifEmpty { doc.select("meta[property=\"og:image\"]").attr("content") }
|
||||||
|
val year = doc.select("article ul:nth-child(1) li a").last()?.text()?.toIntOrNull()
|
||||||
|
val title = doc.select("title").text().split(" | ")[0]
|
||||||
|
val isMovie = title.contains("فيلم|حفلات|مسرحية".toRegex())
|
||||||
|
val youtubeTrailer = doc.select("iframe")?.attr("src")
|
||||||
|
|
||||||
|
val synopsis = doc.select("ul#details li:contains(لمحة) p").text()
|
||||||
|
|
||||||
|
val tags = doc.select("article ul").first()?.select("li")?.map { it.text() }
|
||||||
|
|
||||||
|
val recommendations = doc.select("ul#related li").map { element ->
|
||||||
|
MovieSearchResponse(
|
||||||
|
apiName = this@CimaNow.name,
|
||||||
|
url = element.select("a").attr("href"),
|
||||||
|
name = element.select("img:nth-child(2)").attr("alt"),
|
||||||
|
posterUrl = element.select("img:nth-child(2)").attr("src")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isMovie) {
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
"$url/watching"
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.recommendations = recommendations
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
addTrailer(youtubeTrailer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = doc.select("ul#eps li").map { episode ->
|
||||||
|
Episode(
|
||||||
|
episode.select("a").attr("href")+"/watching",
|
||||||
|
episode.select("a img:nth-child(2)").attr("alt"),
|
||||||
|
doc.select("span[aria-label=\"season-title\"]").html().replace("<p>.*|\n".toRegex(), "").getIntFromText(),
|
||||||
|
episode.select("a em").text().toIntOrNull(),
|
||||||
|
episode.select("a img:nth-child(2)").attr("src")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes.distinct().sortedBy { it.episode }) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.tags = tags
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.recommendations = recommendations
|
||||||
|
addTrailer(youtubeTrailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get("$data").document.select("ul#download [aria-label=\"quality\"]").forEach {
|
||||||
|
val name = if(it.select("span").text().contains("فائق السرعة")) "Fast Servers" else "Servers"
|
||||||
|
it.select("a").forEach { media ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = name,
|
||||||
|
url = media.attr("href"),
|
||||||
|
referer = this.mainUrl,
|
||||||
|
quality = media.text().getIntFromText() ?: Qualities.Unknown.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
EgyBestProvider/build.gradle.kts
Normal file
12
EgyBestProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" , "Anime" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=www.egy.best&sz=24"
|
||||||
|
}
|
2
EgyBestProvider/src/main/AndroidManifest.xml
Normal file
2
EgyBestProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.egybest"/>
|
11
EgyBestProvider/src/main/kotlin/com/egybest/EgyBestPlugin.kt
Normal file
11
EgyBestProvider/src/main/kotlin/com/egybest/EgyBestPlugin.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.egybest
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class EgyBestPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(EgyBest())
|
||||||
|
}
|
||||||
|
}
|
262
EgyBestProvider/src/main/kotlin/com/egybest/EgyBestProvider.kt
Normal file
262
EgyBestProvider/src/main/kotlin/com/egybest/EgyBestProvider.kt
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
package com.egybest
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import com.lagradost.nicehttp.Requests
|
||||||
|
|
||||||
|
class EgyBest : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://www.egy.best"
|
||||||
|
override var name = "EgyBest"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie, TvType.Anime)
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = this.attr("href") ?: return null
|
||||||
|
val posterUrl = select("img")?.attr("src")
|
||||||
|
var title = select("span.title").text()
|
||||||
|
val year = title.getYearFromTitle()
|
||||||
|
val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url)
|
||||||
|
val tvType = if (isMovie) TvType.Movie else TvType.TvSeries
|
||||||
|
title = if (year !== null) title else title.split(" (")[0].trim()
|
||||||
|
val quality = select("span.ribbon span").text().replace("-", "")
|
||||||
|
// If you need to differentiate use the url.
|
||||||
|
return MovieSearchResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
this@EgyBest.name,
|
||||||
|
tvType,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
null,
|
||||||
|
quality = getQualityFromString(quality)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
// url, title
|
||||||
|
val doc = app.get(mainUrl).document
|
||||||
|
val pages = arrayListOf<HomePageList>()
|
||||||
|
doc.select("#mainLoad div.mbox").apmap {
|
||||||
|
val name = it.select(".bdb.pda > strong").text()
|
||||||
|
if (it.select(".movie").first()?.attr("href")?.contains("season-(.....)|ep-(.....)".toRegex()) == true) return@apmap
|
||||||
|
val list = arrayListOf<SearchResponse>()
|
||||||
|
it.select(".movie").map { element ->
|
||||||
|
list.add(element.toSearchResponse()!!)
|
||||||
|
}
|
||||||
|
pages.add(HomePageList(name, list))
|
||||||
|
}
|
||||||
|
return HomePageResponse(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val q = query.replace(" ","%20")
|
||||||
|
val result = arrayListOf<SearchResponse>()
|
||||||
|
listOf("$mainUrl/explore/?q=$q").apmap { url ->
|
||||||
|
val d = app.get(url).document
|
||||||
|
d.select("div.movies a").not("a.auto.load.btn.b").mapNotNull {
|
||||||
|
it.toSearchResponse()?.let { it1 -> result.add(it1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.distinct().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.getYearFromTitle(): Int? {
|
||||||
|
return Regex("""\(\d{4}\)""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url)
|
||||||
|
val posterUrl = doc.select("div.movie_img a img")?.attr("src")
|
||||||
|
val year = doc.select("div.movie_title h1 a")?.text()?.toIntOrNull()
|
||||||
|
val title = doc.select("div.movie_title h1 span").text()
|
||||||
|
val youtubeTrailer = doc.select("div.play")?.attr("url")
|
||||||
|
|
||||||
|
val synopsis = doc.select("div.mbox").firstOrNull {
|
||||||
|
it.text().contains("القصة")
|
||||||
|
}?.text()?.replace("القصة ", "")
|
||||||
|
|
||||||
|
val tags = doc.select("table.movieTable tbody tr").firstOrNull {
|
||||||
|
it.text().contains("النوع")
|
||||||
|
}?.select("a")?.map { it.text() }
|
||||||
|
|
||||||
|
val actors = doc.select("div.cast_list .cast_item").mapNotNull {
|
||||||
|
val name = it.selectFirst("div > a > img")?.attr("alt") ?: return@mapNotNull null
|
||||||
|
val image = it.selectFirst("div > a > img")?.attr("src") ?: return@mapNotNull null
|
||||||
|
val roleString = it.selectFirst("div > span")!!.text()
|
||||||
|
val mainActor = Actor(name, image)
|
||||||
|
ActorData(actor = mainActor, roleString = roleString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isMovie) {
|
||||||
|
val recommendations = doc.select(".movies_small .movie").mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.recommendations = recommendations
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
this.actors = actors
|
||||||
|
addTrailer(youtubeTrailer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = ArrayList<Episode>()
|
||||||
|
doc.select("#mainLoad > div:nth-child(2) > div.h_scroll > div a").map {
|
||||||
|
it.attr("href")
|
||||||
|
}.apmap {
|
||||||
|
val d = app.get(it).document
|
||||||
|
val season = Regex("season-(.....)").find(it)?.groupValues?.getOrNull(1)?.getIntFromText()
|
||||||
|
if(d.select("tr.published").isNotEmpty()) {
|
||||||
|
d.select("tr.published").map { element ->
|
||||||
|
val ep = Regex("ep-(.....)").find(element.select(".ep_title a").attr("href"))?.groupValues?.getOrNull(1)?.getIntFromText()
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
element.select(".ep_title a").attr("href"),
|
||||||
|
name = element.select("td.ep_title").html().replace(".*</span>|</a>".toRegex(), ""),
|
||||||
|
season,
|
||||||
|
ep,
|
||||||
|
rating = element.select("td.tam:not(.date, .ep_len)").text().getIntFromText()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d.select("#mainLoad > div:nth-child(3) > div.movies_small a").map { eit ->
|
||||||
|
val ep = Regex("ep-(.....)").find(eit.attr("href"))?.groupValues?.getOrNull(1)?.getIntFromText()
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
eit.attr("href"),
|
||||||
|
eit.select("span.title").text(),
|
||||||
|
season,
|
||||||
|
ep,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes.distinct().sortedBy { it.episode }) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.tags = tags
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.actors = actors
|
||||||
|
addTrailer(youtubeTrailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class Sources (
|
||||||
|
@JsonProperty("quality") val quality: Int?,
|
||||||
|
@JsonProperty("link") val link: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.ExtractLinks(): Boolean? {
|
||||||
|
val list = Regex("#EXT.*\\n.*").findAll(this).toList()
|
||||||
|
println(list)
|
||||||
|
list.map {
|
||||||
|
val url = Regex(".*stream\\.m3u8").find(it.value)?.value.toString()
|
||||||
|
val quality = Regex("[0-9]{3,4}x[0-9]{3,4}").find(it.value)?.value?.replace(".*x".toRegex(),"")?.toInt()
|
||||||
|
ExtractorLink(
|
||||||
|
this@EgyBest.name,
|
||||||
|
this@EgyBest.name,
|
||||||
|
url,
|
||||||
|
this@EgyBest.mainUrl,
|
||||||
|
quality ?: Qualities.Unknown.value,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
/*val baseURL = data.split("/")[0] + "//" + data.split("/")[2]
|
||||||
|
|
||||||
|
val session = Requests()
|
||||||
|
val episodeSoup = session.get(data).document
|
||||||
|
|
||||||
|
val vidstreamURL = fixUrlNull(episodeSoup.selectFirst("iframe.auto-size")?.attr("src") ) ?: throw ErrorLoadingException("No iframe")
|
||||||
|
|
||||||
|
val videoSoup = app.get(vidstreamURL).document
|
||||||
|
fixUrlNull( videoSoup.select("source").firstOrNull { it.hasAttr("src") }?.attr("src"))?.let {
|
||||||
|
app.get(it).text.replace("#EXTM3U\n","").ExtractLinks()
|
||||||
|
} ?: run {
|
||||||
|
var jsCode = videoSoup.select("script")[1].data()
|
||||||
|
println(jsCode)
|
||||||
|
val verificationToken = Regex("\\{'[0-9a-zA-Z_]*':'ok'\\}").findAll(jsCode).first().value.replace("\\{'|':.*".toRegex(), "")
|
||||||
|
val encodedAdLinkVar = Regex("\\([0-9a-zA-Z_]{2,12}\\[Math").findAll(jsCode).first().value.replace("\\(|\\[M.*".toRegex(),"")
|
||||||
|
val encodingArraysRegEx = Regex(",[0-9a-zA-Z_]{2,12}=\\[\\]").findAll(jsCode).toList()
|
||||||
|
|
||||||
|
val firstEncodingArray = encodingArraysRegEx[1].value.replace(",|=.*".toRegex(),"")
|
||||||
|
val secondEncodingArray = encodingArraysRegEx[2].value.replace(",|=.*".toRegex(),"")
|
||||||
|
|
||||||
|
jsCode = jsCode.replace("^<script type=\"text/javascript\">".toRegex(),"")
|
||||||
|
jsCode = jsCode.replace("[;,]\\\$\\('\\*'\\)(.*)\$".toRegex(),";")
|
||||||
|
jsCode = jsCode.replace(",ismob=(.*)\\(navigator\\[(.*)\\]\\)[,;]".toRegex(),";")
|
||||||
|
jsCode = jsCode.replace("var a0b=function\\(\\)(.*)a0a\\(\\);".toRegex(),"")
|
||||||
|
jsCode += "var link = ''; for (var i = 0; i <= $secondEncodingArray['length']; i++) { link += $firstEncodingArray[$secondEncodingArray[i]] || ''; } return [link, $encodedAdLinkVar[0]] }"
|
||||||
|
|
||||||
|
// till here everything should be fine
|
||||||
|
val jsCodeReturn = js(jsCode)()
|
||||||
|
val verificationPath = jsCodeReturn[0]
|
||||||
|
val encodedAdPath = jsCodeReturn[1]
|
||||||
|
|
||||||
|
val adLink = baseURL + "/" + str(decode(encodedAdPath + "=" * (-len(encodedAdPath) % 4)), "utf-8")
|
||||||
|
val session.get(adLink)
|
||||||
|
|
||||||
|
val verificationLink = baseURL + "/tvc.php?verify=" + verificationPath
|
||||||
|
val session.post(verificationLink, data={verificationToken: "ok"})
|
||||||
|
|
||||||
|
val vidstreamResponseText = session.get(vidstreamURL).text
|
||||||
|
val videoSoup = BeautifulSoup(vidstreamResponseText, features="html.parser")
|
||||||
|
|
||||||
|
val qualityLinksFileURL = baseURL + videoSoup.body.find("source").get("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true*/
|
||||||
|
|
||||||
|
val requestJSON = app.get("https://api.zr5.repl.co/egybest?url=$data").text
|
||||||
|
// To solve this you need to send a verify request which is pretty hidden, see
|
||||||
|
// https://vear.egybest.deals/tvc.php?verify=.......
|
||||||
|
val jsonArray = parseJson<List<Sources>>(requestJSON)
|
||||||
|
for (i in jsonArray) {
|
||||||
|
val quality = i.quality
|
||||||
|
val link = i.link
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
link,
|
||||||
|
this.mainUrl,
|
||||||
|
quality!!,
|
||||||
|
true,
|
||||||
|
// Does not work without these headers!
|
||||||
|
headers = mapOf("range" to "bytes=0-"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
10
Extractors/build.gradle.kts
Normal file
10
Extractors/build.gradle.kts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "Others" )
|
||||||
|
}
|
2
Extractors/src/main/AndroidManifest.xml
Normal file
2
Extractors/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.extractors"/>
|
62
Extractors/src/main/kotlin/com/extractors/DoodExtractor.kt
Normal file
62
Extractors/src/main/kotlin/com/extractors/DoodExtractor.kt
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
class DoodCxExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.cx"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoodShExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.sh"
|
||||||
|
}
|
||||||
|
class DoodWatchExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.watch"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoodPmExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.pm"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoodToExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.to"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoodSoExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoodWsExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.ws"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open class DoodLaExtractor : ExtractorApi() {
|
||||||
|
override var name = "DoodStream"
|
||||||
|
override var mainUrl = "https://dood.la"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override fun getExtractorUrl(id: String): String {
|
||||||
|
return "$mainUrl/d/$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
|
||||||
|
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
|
||||||
|
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
||||||
|
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
trueUrl,
|
||||||
|
this.name,
|
||||||
|
trueUrl,
|
||||||
|
mainUrl,
|
||||||
|
getQualityFromName(quality),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) // links are valid in 8h
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
28
Extractors/src/main/kotlin/com/extractors/ExtractorPlugin.kt
Normal file
28
Extractors/src/main/kotlin/com/extractors/ExtractorPlugin.kt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package com.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class ExtractorPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerExtractorAPI(DoodLaExtractor())
|
||||||
|
registerExtractorAPI(DoodCxExtractor())
|
||||||
|
registerExtractorAPI(DoodShExtractor())
|
||||||
|
registerExtractorAPI(DoodWatchExtractor())
|
||||||
|
registerExtractorAPI(DoodPmExtractor())
|
||||||
|
registerExtractorAPI(DoodToExtractor())
|
||||||
|
registerExtractorAPI(DoodSoExtractor())
|
||||||
|
registerExtractorAPI(DoodWsExtractor())
|
||||||
|
registerExtractorAPI(Uqload())
|
||||||
|
registerExtractorAPI(Uqload1())
|
||||||
|
registerExtractorAPI(StreamTape())
|
||||||
|
registerExtractorAPI(VoeExtractor())
|
||||||
|
registerExtractorAPI(JWPlayer())
|
||||||
|
registerExtractorAPI(VidBom())
|
||||||
|
registerExtractorAPI(UpstreamExtractor())
|
||||||
|
registerExtractorAPI(Streamlare())
|
||||||
|
registerExtractorAPI(Slmaxed())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
class VidBom : JWPlayer() {
|
||||||
|
override val name = "Vidbom"
|
||||||
|
override val mainUrl = "https://vidbam.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JWPlayer : ExtractorApi() {
|
||||||
|
override val name = "JWPlayer"
|
||||||
|
override val mainUrl = "https://www.jwplayer.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
with(app.get(url).document) {
|
||||||
|
val data = this.select("script").mapNotNull { script ->
|
||||||
|
if (script.data().contains("sources: [")) {
|
||||||
|
script.data().substringAfter("sources: [")
|
||||||
|
.substringBefore("],").replace("'", "\"")
|
||||||
|
} else if (script.data().contains("otakudesu('")) {
|
||||||
|
script.data().substringAfter("otakudesu('")
|
||||||
|
.substringBefore("');")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryParseJson<List<ResponseSource>>("$data")?.map {
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
it.file,
|
||||||
|
referer = url,
|
||||||
|
quality = getQualityFromName(
|
||||||
|
Regex("(\\d{3,4}p)").find(it.file)?.groupValues?.get(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ResponseSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("type") val type: String?,
|
||||||
|
@JsonProperty("label") val label: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
class StreamTape : ExtractorApi() {
|
||||||
|
override var name = "StreamTape"
|
||||||
|
override var mainUrl = "https://streamtape.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
private val linkRegex =
|
||||||
|
Regex("""'robotlink'\)\.innerHTML = '(.+?)'\+ \('(.+?)'\)""")
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
with(app.get(url)) {
|
||||||
|
linkRegex.find(this.text)?.let {
|
||||||
|
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
extractedUrl,
|
||||||
|
url,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.nicehttp.RequestBodyTypes
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
|
|
||||||
|
class Streamlare : Slmaxed() {
|
||||||
|
override val mainUrl = "https://streamlare.com/"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Slmaxed : ExtractorApi() {
|
||||||
|
override val name = "Streamlare"
|
||||||
|
override val mainUrl = "https://slmaxed.com/"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
// https://slmaxed.com/e/oLvgezw3LjPzbp8E -> oLvgezw3LjPzbp8E
|
||||||
|
val embedRegex = Regex("""/e/([^/]*)""")
|
||||||
|
|
||||||
|
|
||||||
|
data class JsonResponse(
|
||||||
|
@JsonProperty val status: String? = null,
|
||||||
|
@JsonProperty val message: String? = null,
|
||||||
|
@JsonProperty val type: String? = null,
|
||||||
|
@JsonProperty val token: String? = null,
|
||||||
|
@JsonProperty val result: Map<String, Result>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
@JsonProperty val label: String? = null,
|
||||||
|
@JsonProperty val file: String? = null,
|
||||||
|
@JsonProperty val type: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val id = embedRegex.find(url)!!.groupValues[1]
|
||||||
|
val json = app.post(
|
||||||
|
"${mainUrl}api/video/stream/get",
|
||||||
|
requestBody = """{"id":"$id"}""".toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
||||||
|
).parsed<JsonResponse>()
|
||||||
|
return json.result?.mapNotNull {
|
||||||
|
it.value.let { result ->
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
result.file ?: return@mapNotNull null,
|
||||||
|
url,
|
||||||
|
result.label?.replace("p", "", ignoreCase = true)?.trim()?.toIntOrNull()
|
||||||
|
?: Qualities.Unknown.value,
|
||||||
|
isM3u8 = result.type?.contains("hls", ignoreCase = true) == true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
class UpstreamExtractor: ExtractorApi() {
|
||||||
|
override val name: String = "Upstream.to"
|
||||||
|
override val mainUrl: String = "https://upstream.to"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
// WIP: m3u8 link fetched but sometimes not playing
|
||||||
|
//Log.i(this.name, "Result => (no extractor) ${url}")
|
||||||
|
val sources: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
val doc = app.get(url, referer = referer).text
|
||||||
|
if (doc.isNotBlank()) {
|
||||||
|
var reg = Regex("(?<=master)(.*)(?=hls)")
|
||||||
|
val result = reg.find(doc)?.groupValues?.map {
|
||||||
|
it.trim('|')
|
||||||
|
}?.toList()
|
||||||
|
reg = Regex("(?<=\\|file\\|)(.*)(?=\\|remove\\|)")
|
||||||
|
val domainList = reg.find(doc)?.groupValues?.get(1)?.split("|")
|
||||||
|
var domain = when (!domainList.isNullOrEmpty()) {
|
||||||
|
true -> {
|
||||||
|
if (domainList.isNotEmpty()) {
|
||||||
|
var domName = ""
|
||||||
|
for (part in domainList) {
|
||||||
|
domName = "${part}.${domName}"
|
||||||
|
}
|
||||||
|
domName.trimEnd('.')
|
||||||
|
} else { "" }
|
||||||
|
}
|
||||||
|
false -> ""
|
||||||
|
}
|
||||||
|
//Log.i(this.name, "Result => (domain) ${domain}")
|
||||||
|
if (domain.isEmpty()) {
|
||||||
|
domain = "s96.upstreamcdn.co"
|
||||||
|
//Log.i(this.name, "Result => (default domain) ${domain}")
|
||||||
|
}
|
||||||
|
|
||||||
|
result?.forEach {
|
||||||
|
val linkUrl = "https://${domain}/hls/${it}/master.m3u8"
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name = "Upstream m3u8",
|
||||||
|
source = this.name,
|
||||||
|
url = linkUrl,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
referer = referer ?: linkUrl,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
49
Extractors/src/main/kotlin/com/extractors/UqloadExtractor.kt
Normal file
49
Extractors/src/main/kotlin/com/extractors/UqloadExtractor.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
|
||||||
|
class Uqload1 : Uqload() {
|
||||||
|
override var mainUrl = "https://uqload.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Uqload : ExtractorApi() {
|
||||||
|
override val name: String = "Uqload"
|
||||||
|
override val mainUrl: String = "https://www.uqload.com"
|
||||||
|
private val srcRegex = Regex("""sources:.\[(.*?)\]""") // would be possible to use the parse and find src attribute
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val lang = url.substring(0, 2)
|
||||||
|
val flag =
|
||||||
|
if (lang == "vo") {
|
||||||
|
" \uD83C\uDDEC\uD83C\uDDE7"
|
||||||
|
}
|
||||||
|
else if (lang == "vf"){
|
||||||
|
" \uD83C\uDDE8\uD83C\uDDF5"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
url.substring(2, url.length)
|
||||||
|
}
|
||||||
|
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
||||||
|
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name + flag,
|
||||||
|
link,
|
||||||
|
cleaned_url,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
51
Extractors/src/main/kotlin/com/extractors/VoeExtractor.kt
Normal file
51
Extractors/src/main/kotlin/com/extractors/VoeExtractor.kt
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package com.stable
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
open class VoeExtractor : ExtractorApi() {
|
||||||
|
override val name: String = "Voe"
|
||||||
|
override val mainUrl: String = "https://voe.sx"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
private data class ResponseLinks(
|
||||||
|
@JsonProperty("hls") val url: String?,
|
||||||
|
@JsonProperty("video_height") val label: Int?
|
||||||
|
//val type: String // Mp4
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
val doc = app.get(url).text
|
||||||
|
if (doc.isNotBlank()) {
|
||||||
|
val start = "const sources ="
|
||||||
|
var src = doc.substring(doc.indexOf(start))
|
||||||
|
src = src.substring(start.length, src.indexOf(";"))
|
||||||
|
.replace("0,", "0")
|
||||||
|
.trim()
|
||||||
|
//Log.i(this.name, "Result => (src) ${src}")
|
||||||
|
parseJson<ResponseLinks?>(src)?.let { voelink ->
|
||||||
|
//Log.i(this.name, "Result => (voelink) ${voelink}")
|
||||||
|
val linkUrl = voelink.url
|
||||||
|
val linkLabel = voelink.label?.toString() ?: ""
|
||||||
|
if (!linkUrl.isNullOrEmpty()) {
|
||||||
|
extractedLinksList.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name = this.name,
|
||||||
|
source = this.name,
|
||||||
|
url = linkUrl,
|
||||||
|
quality = getQualityFromName(linkLabel),
|
||||||
|
referer = url,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractedLinksList
|
||||||
|
}
|
||||||
|
}
|
12
FaselHDProvider/build.gradle.kts
Normal file
12
FaselHDProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" , "Anime" , "AsianDrama" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=faselhd.io&sz=24"
|
||||||
|
}
|
2
FaselHDProvider/src/main/AndroidManifest.xml
Normal file
2
FaselHDProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.faselhd"/>
|
11
FaselHDProvider/src/main/kotlin/com/faselhd/FaselHDPlugin.kt
Normal file
11
FaselHDProvider/src/main/kotlin/com/faselhd/FaselHDPlugin.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.faselhd
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class FaselHDPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(FaselHD())
|
||||||
|
}
|
||||||
|
}
|
160
FaselHDProvider/src/main/kotlin/com/faselhd/FaselHDProvider.kt
Normal file
160
FaselHDProvider/src/main/kotlin/com/faselhd/FaselHDProvider.kt
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package com.faselhd
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class FaselHD : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://faselhd.io"
|
||||||
|
override var name = "FaselHD"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie, TvType.AsianDrama, TvType.Anime)
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = select("div.postDiv a").attr("href") ?: return null
|
||||||
|
val posterUrl = select("div.postDiv a div img").attr("data-src") ?:
|
||||||
|
select("div.postDiv a div img").attr("src")
|
||||||
|
val title = select("div.postDiv a div img").attr("alt")
|
||||||
|
val quality = select(".quality").first()?.text()?.replace("1080p |-".toRegex(), "")
|
||||||
|
val type = if(title.contains("فيلم")) TvType.Movie else TvType.TvSeries
|
||||||
|
return MovieSearchResponse(
|
||||||
|
title.replace("الموسم الأول|برنامج|فيلم|مترجم|اون لاين|مسلسل|مشاهدة|انمي|أنمي".toRegex(),""),
|
||||||
|
url,
|
||||||
|
this@FaselHD.name,
|
||||||
|
type,
|
||||||
|
posterUrl,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
quality = getQualityFromString(quality)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/all-movies/page/" to "Movies",
|
||||||
|
"$mainUrl/series/page/" to "Series",
|
||||||
|
"$mainUrl/movies_top_imdb/page/" to "Top Movies IMDB",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div[id=\"postList\"] div[class=\"col-xl-2 col-lg-2 col-md-3 col-sm-3\"]")
|
||||||
|
.mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val q = query.replace(" ","+")
|
||||||
|
val d = app.get("$mainUrl/?s=$q").document
|
||||||
|
return d.select("div[id=\"postList\"] div[class=\"col-xl-2 col-lg-2 col-md-3 col-sm-3\"]")
|
||||||
|
.mapNotNull {
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val isMovie = doc.select("div.epAll").isEmpty()
|
||||||
|
val posterUrl = doc.select("div.posterImg img").attr("src")
|
||||||
|
.ifEmpty { doc.select("div.seasonDiv.active img").attr("data-src") }
|
||||||
|
|
||||||
|
val year = doc.select("div[id=\"singleList\"] div[class=\"col-xl-6 col-lg-6 col-md-6 col-sm-6\"]").firstOrNull {
|
||||||
|
it.text().contains("سنة|موعد".toRegex())
|
||||||
|
}?.text()?.getIntFromText()
|
||||||
|
|
||||||
|
val title =
|
||||||
|
doc.select("title").text().replace(" - فاصل إعلاني", "")
|
||||||
|
.replace("الموسم الأول|برنامج|فيلم|مترجم|اون لاين|مسلسل|مشاهدة|انمي|أنمي|$year".toRegex(),"")
|
||||||
|
// A bit iffy to parse twice like this, but it'll do.
|
||||||
|
val duration = doc.select("div[id=\"singleList\"] div[class=\"col-xl-6 col-lg-6 col-md-6 col-sm-6\"]").firstOrNull {
|
||||||
|
it.text().contains("مدة|توقيت".toRegex())
|
||||||
|
}?.text()?.getIntFromText()
|
||||||
|
|
||||||
|
val tags = doc.select("div[id=\"singleList\"] div[class=\"col-xl-6 col-lg-6 col-md-6 col-sm-6\"]:contains(تصنيف الفيلم) a").map {
|
||||||
|
it.text()
|
||||||
|
}
|
||||||
|
val recommendations = doc.select("div#postList div.postDiv").mapNotNull {
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
val synopsis = doc.select("div.singleDesc p").text()
|
||||||
|
return if (isMovie) {
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.duration = duration
|
||||||
|
this.tags = tags
|
||||||
|
this.recommendations = recommendations
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = ArrayList<Episode>()
|
||||||
|
doc.select("div.epAll a").map {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
doc.select("div.seasonDiv.active div.title").text().getIntFromText() ?: 1,
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
doc.select("div[id=\"seasonList\"] div[class=\"col-xl-2 col-lg-3 col-md-6\"] div.seasonDiv")
|
||||||
|
.not(".active").apmap { it ->
|
||||||
|
val s = app.get("$mainUrl/?p="+it.attr("data-href")).document
|
||||||
|
s.select("div.epAll a").map {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
s.select("div.seasonDiv.active div.title").text().getIntFromText(),
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes.distinct().sortedBy { it.episode }) {
|
||||||
|
this.duration = duration
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
this.recommendations = recommendations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val player = app.get(app.get(data).document.select("iframe[name=\"player_iframe\"]").attr("src")).document
|
||||||
|
player.select("div.quality_change button.hd_btn").map {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
it.attr("data-url"),
|
||||||
|
this.mainUrl,
|
||||||
|
quality = it.text().getIntFromText() ?: 0,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
MovizlandProvider/build.gradle.kts
Normal file
12
MovizlandProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "Movie" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=movizland.cyou&sz=24"
|
||||||
|
}
|
2
MovizlandProvider/src/main/AndroidManifest.xml
Normal file
2
MovizlandProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.movizland"/>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.movizland
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class MovizlandPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(Movizland())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package com.movizland
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class Movizland : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://movizland.cyou"
|
||||||
|
override var name = "Movizland"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.Movie)
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = select("div.BlockItem")
|
||||||
|
val posterUrl = select("div img")?.attr("data-src")
|
||||||
|
val year = select("ul.InfoEndBlock li").last()?.text()?.getIntFromText()
|
||||||
|
var quality = select("ul.RestInformation li").last()?.text()?.replace(" |-|1080p|720p".toRegex(), "")
|
||||||
|
?.replace("WEB DL","WEBDL")?.replace("BluRay","BLURAY")
|
||||||
|
val title = select("div.BlockTitle").text()
|
||||||
|
.replace("اون لاين", "")
|
||||||
|
.replace("مشاهدة و تحميل", "")
|
||||||
|
.replace("4K", "")
|
||||||
|
.replace("${year.toString()}", "")
|
||||||
|
.replace("فيلم", "")
|
||||||
|
.replace("مترجم", "")
|
||||||
|
.replace("مشاهدة", "")
|
||||||
|
.replace("بجودة", "")
|
||||||
|
.replace("3D", "")
|
||||||
|
.replace("وتحميل", "")
|
||||||
|
// val quality =select("ul.RestInformation li").last()?.text()
|
||||||
|
return MovieSearchResponse(
|
||||||
|
title,
|
||||||
|
url.select("a").attr("href"),
|
||||||
|
this@Movizland.name,
|
||||||
|
if(url.select("div.BlockTitle").text().contains("فيلم")) TvType.Movie else TvType.TvSeries,
|
||||||
|
posterUrl,
|
||||||
|
year,
|
||||||
|
null,
|
||||||
|
quality = getQualityFromString(quality),
|
||||||
|
// rating = url.select("div.StarsIMDB").text()?.getIntFromText()?.toDouble()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/category/movies/page/" to "Movies",
|
||||||
|
"$mainUrl/quality/4K/page/" to "4K",
|
||||||
|
"$mainUrl/quality/1080p-bluray/page/" to "Bluray",
|
||||||
|
"$mainUrl/category/movies/anime/page/" to "Animation",
|
||||||
|
"$mainUrl/category/movies/documentary/page/" to "Documentary",
|
||||||
|
"$mainUrl/category/movies/netflix/page/" to "Netflix",
|
||||||
|
"$mainUrl/category/movies/foreign/page/" to "Foreign",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div.BlockItem").mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val q = query.replace(" ", "%20")
|
||||||
|
val result = arrayListOf<SearchResponse>()
|
||||||
|
listOf(
|
||||||
|
"$mainUrl/category/movies/?s=$q",
|
||||||
|
//"$mainUrl/category/series/?s=$q"
|
||||||
|
).apmap { url ->
|
||||||
|
val d = app.get(url).document
|
||||||
|
d.select("div.BlockItem").mapNotNull {
|
||||||
|
if (it.text().contains("اعلان")) return@mapNotNull null
|
||||||
|
it.toSearchResponse()?.let { it1 -> result.add(it1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.distinct().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
var doc = app.get(url).document
|
||||||
|
val posterUrl = doc.select("img")?.attr("data-src")
|
||||||
|
val year = doc.select("div.SingleDetails a").last()?.text()?.getIntFromText()
|
||||||
|
val title = doc.select("h2.postTitle").text()
|
||||||
|
.replace("اون لاين", "")
|
||||||
|
.replace("مشاهدة و تحميل", "")
|
||||||
|
.replace("4K", "")
|
||||||
|
.replace("${year.toString()}", "")
|
||||||
|
.replace("فيلم", "")
|
||||||
|
.replace("مترجم", "")
|
||||||
|
.replace("مشاهدة", "")
|
||||||
|
.replace("بجودة", "")
|
||||||
|
.replace("3D", "")
|
||||||
|
.replace("وتحميل", "")
|
||||||
|
val isMovie = doc.select("h2.postTitle").text().contains("فيلم".toRegex())
|
||||||
|
val synopsis = doc.select("section.story").text()
|
||||||
|
val trailer = doc.select("div.InnerTrailer iframe").attr("src")
|
||||||
|
val tags = doc.select("div.SingleDetails").select("li")?.map { it.text() }
|
||||||
|
|
||||||
|
|
||||||
|
return newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.tags = tags
|
||||||
|
this.plot = synopsis
|
||||||
|
addTrailer(trailer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val doc = app.get(data).document
|
||||||
|
// doc.select(
|
||||||
|
// "li:contains(dood), li:contains(streamlare), li:contains(streamtape), li:contains(uqload), li:contains(upstream)"
|
||||||
|
// ).map {
|
||||||
|
// val dataServer = it.attr("data-server")
|
||||||
|
// val url = it.select("code#EmbedSc$dataServer iframe").attr("data-srcout")
|
||||||
|
// println(url)
|
||||||
|
// loadExtractor(url, data, subtitleCallback, callback)
|
||||||
|
// }
|
||||||
|
doc.select("table tbody tr").map {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
it.select("a").attr("href"),
|
||||||
|
this.mainUrl,
|
||||||
|
quality = getQualityFromName(it.select("td:nth-child(2)").text().replace("Original","1080"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
MyCimaProvider/build.gradle.kts
Normal file
12
MyCimaProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" , "Anime" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=mycima.tv&sz=24"
|
||||||
|
}
|
2
MyCimaProvider/src/main/AndroidManifest.xml
Normal file
2
MyCimaProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.mycima"/>
|
11
MyCimaProvider/src/main/kotlin/com/mycima/MyCimaPlugin.kt
Normal file
11
MyCimaProvider/src/main/kotlin/com/mycima/MyCimaPlugin.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package com.mycima
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class MyCimaPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(MyCima())
|
||||||
|
}
|
||||||
|
}
|
327
MyCimaProvider/src/main/kotlin/com/mycima/MyCimaProvider.kt
Normal file
327
MyCimaProvider/src/main/kotlin/com/mycima/MyCimaProvider.kt
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
package com.mycima
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class MyCima : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://mycima.tv"
|
||||||
|
override var name = "MyCima"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie, TvType.Anime)
|
||||||
|
|
||||||
|
private fun String.getImageURL(): String? {
|
||||||
|
return this.replace("--im(age|g):url\\(|\\);".toRegex(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.getIntFromText(): Int? {
|
||||||
|
return Regex("""\d+""").find(this)?.groupValues?.firstOrNull()?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val url = select("div.Thumb--GridItem a")
|
||||||
|
val posterUrl = select("span.BG--GridItem")?.attr("data-lazy-style")
|
||||||
|
?.getImageURL()
|
||||||
|
val year = select("div.GridItem span.year")?.text()
|
||||||
|
val title = select("div.Thumb--GridItem strong").text()
|
||||||
|
.replace("$year", "")
|
||||||
|
.replace("مشاهدة|فيلم|مسلسل|مترجم".toRegex(), "")
|
||||||
|
.replace("( نسخة مدبلجة )", " ( نسخة مدبلجة ) ")
|
||||||
|
// If you need to differentiate use the url.
|
||||||
|
return MovieSearchResponse(
|
||||||
|
title,
|
||||||
|
url.attr("href"),
|
||||||
|
this@MyCima.name,
|
||||||
|
if(url.attr("title").contains("فيلم")) TvType.Movie else TvType.TvSeries,
|
||||||
|
posterUrl,
|
||||||
|
year?.getIntFromText(),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/movies/top/page/" to "Top Movies",
|
||||||
|
"$mainUrl/movies/page/" to "New Movies",
|
||||||
|
"$mainUrl/movies/recent/page/" to "Recently Added Movies",
|
||||||
|
"$mainUrl/seriestv/top/page/" to "Top Series",
|
||||||
|
"$mainUrl/seriestv/new/page/" to "New Series",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div.Grid--MycimaPosts div.GridItem").mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val q = query.replace(" ", "%20")
|
||||||
|
val result = arrayListOf<SearchResponse>()
|
||||||
|
listOf(
|
||||||
|
"$mainUrl/search/$q",
|
||||||
|
"$mainUrl/search/$q/list/series/",
|
||||||
|
"$mainUrl/search/$q/list/anime/"
|
||||||
|
).apmap { url ->
|
||||||
|
val d = app.get(url).document
|
||||||
|
d.select("div.Grid--MycimaPosts div.GridItem").mapNotNull {
|
||||||
|
if (it.text().contains("اعلان")) return@mapNotNull null
|
||||||
|
it.toSearchResponse()?.let { it1 -> result.add(it1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.distinct().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MoreEPS(
|
||||||
|
val output: String
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val isMovie = doc.select("ol li:nth-child(3)").text().contains("افلام")
|
||||||
|
val posterUrl =
|
||||||
|
doc.select("mycima.separated--top")?.attr("data-lazy-style")?.getImageURL()
|
||||||
|
?.ifEmpty { doc.select("meta[itemprop=\"thumbnailUrl\"]")?.attr("content") }
|
||||||
|
?.ifEmpty { doc.select("mycima.separated--top")?.attr("style")?.getImageURL() }
|
||||||
|
val year =
|
||||||
|
doc.select("div.Title--Content--Single-begin h1 a.unline")?.text()?.getIntFromText()
|
||||||
|
val title = doc.select("div.Title--Content--Single-begin h1").text()
|
||||||
|
.replace("($year)", "")
|
||||||
|
.replace("مشاهدة|فيلم|مسلسل|مترجم|انمي".toRegex(), "")
|
||||||
|
// A bit iffy to parse twice like this, but it'll do.
|
||||||
|
val duration =
|
||||||
|
doc.select("ul.Terms--Content--Single-begin li").firstOrNull {
|
||||||
|
it.text().contains("المدة")
|
||||||
|
}?.text()?.getIntFromText()
|
||||||
|
|
||||||
|
val synopsis = doc.select("div.StoryMovieContent").text()
|
||||||
|
.ifEmpty { doc.select("div.PostItemContent").text() }
|
||||||
|
|
||||||
|
val tags = doc.select("li:nth-child(3) > p > a").map { it.text() }
|
||||||
|
|
||||||
|
val actors = doc.select("div.List--Teamwork > ul.Inner--List--Teamwork > li")?.mapNotNull {
|
||||||
|
val name = it?.selectFirst("a > div.ActorName > span")?.text() ?: return@mapNotNull null
|
||||||
|
val image = it.attr("style")
|
||||||
|
?.getImageURL()
|
||||||
|
?: return@mapNotNull null
|
||||||
|
Actor(name, image)
|
||||||
|
}
|
||||||
|
val recommendations =
|
||||||
|
doc.select("div.Grid--MycimaPosts div.GridItem")?.mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isMovie) {
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
this.duration = duration
|
||||||
|
this.recommendations = recommendations
|
||||||
|
addActors(actors)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = ArrayList<Episode>()
|
||||||
|
val seasons = doc.select("div.List--Seasons--Episodes a").not(".selected").map {
|
||||||
|
it.attr("href")
|
||||||
|
}
|
||||||
|
val moreButton = doc.select("div.MoreEpisodes--Button")
|
||||||
|
val season =
|
||||||
|
doc.select("div.List--Seasons--Episodes a.selected").text().getIntFromText()
|
||||||
|
doc.select("div.Seasons--Episodes div.Episodes--Seasons--Episodes a")
|
||||||
|
.apmap {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
season,
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (moreButton.isNotEmpty()) {
|
||||||
|
val n = doc.select("div.Seasons--Episodes div.Episodes--Seasons--Episodes a").size
|
||||||
|
val totals =
|
||||||
|
doc.select("div.Episodes--Seasons--Episodes a").first()!!.text().getIntFromText()
|
||||||
|
val mEPS = arrayListOf(
|
||||||
|
n,
|
||||||
|
n + 40,
|
||||||
|
n + 80,
|
||||||
|
n + 120,
|
||||||
|
n + 160,
|
||||||
|
n + 200,
|
||||||
|
n + 240,
|
||||||
|
n + 280,
|
||||||
|
n + 320,
|
||||||
|
n + 360,
|
||||||
|
n + 400,
|
||||||
|
n + 440,
|
||||||
|
n + 480,
|
||||||
|
n + 520,
|
||||||
|
n + 660,
|
||||||
|
n + 700,
|
||||||
|
n + 740,
|
||||||
|
n + 780,
|
||||||
|
n + 820,
|
||||||
|
n + 860,
|
||||||
|
n + 900,
|
||||||
|
n + 940,
|
||||||
|
n + 980,
|
||||||
|
n + 1020,
|
||||||
|
n + 1060,
|
||||||
|
n + 1100,
|
||||||
|
n + 1140,
|
||||||
|
n + 1180,
|
||||||
|
n + 1220,
|
||||||
|
totals
|
||||||
|
)
|
||||||
|
mEPS.apmap { it ->
|
||||||
|
if (it != null) {
|
||||||
|
if (it > totals!!) return@apmap
|
||||||
|
val ajaxURL =
|
||||||
|
"$mainUrl/AjaxCenter/MoreEpisodes/${moreButton.attr("data-term")}/$it"
|
||||||
|
val jsonResponse = app.get(ajaxURL)
|
||||||
|
val json = parseJson<MoreEPS>(jsonResponse.text)
|
||||||
|
val document = Jsoup.parse(json.output?.replace("""\""", ""))
|
||||||
|
document.select("a").map {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
season,
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seasons.isNotEmpty()) {
|
||||||
|
seasons.apmap { surl ->
|
||||||
|
if (surl.contains("%d9%85%d8%af%d8%a8%d9%84%d8%ac")) return@apmap
|
||||||
|
val seasonsite = app.get(surl).document
|
||||||
|
val fmoreButton = seasonsite.select("div.MoreEpisodes--Button")
|
||||||
|
val fseason = seasonsite.select("div.List--Seasons--Episodes a.selected").text()
|
||||||
|
.getIntFromText() ?: 1
|
||||||
|
seasonsite.select("div.Seasons--Episodes div.Episodes--Seasons--Episodes a")
|
||||||
|
.map {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
fseason,
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (fmoreButton.isNotEmpty()) {
|
||||||
|
val n =
|
||||||
|
seasonsite.select("div.Seasons--Episodes div.Episodes--Seasons--Episodes a").size
|
||||||
|
val totals =
|
||||||
|
seasonsite.select("div.Episodes--Seasons--Episodes a").first()!!.text()
|
||||||
|
.getIntFromText()
|
||||||
|
val mEPS = arrayListOf(
|
||||||
|
n,
|
||||||
|
n + 40,
|
||||||
|
n + 80,
|
||||||
|
n + 120,
|
||||||
|
n + 160,
|
||||||
|
n + 200,
|
||||||
|
n + 240,
|
||||||
|
n + 280,
|
||||||
|
n + 320,
|
||||||
|
n + 360,
|
||||||
|
n + 400,
|
||||||
|
n + 440,
|
||||||
|
n + 480,
|
||||||
|
n + 520,
|
||||||
|
n + 660,
|
||||||
|
n + 700,
|
||||||
|
n + 740,
|
||||||
|
n + 780,
|
||||||
|
n + 820,
|
||||||
|
n + 860,
|
||||||
|
n + 900,
|
||||||
|
n + 940,
|
||||||
|
n + 980,
|
||||||
|
n + 1020,
|
||||||
|
n + 1060,
|
||||||
|
n + 1100,
|
||||||
|
n + 1140,
|
||||||
|
n + 1180,
|
||||||
|
n + 1220,
|
||||||
|
totals
|
||||||
|
)
|
||||||
|
mEPS.apmap { it ->
|
||||||
|
if (it != null) {
|
||||||
|
if (it > totals!!) return@apmap
|
||||||
|
val ajaxURL =
|
||||||
|
"$mainUrl/AjaxCenter/MoreEpisodes/${fmoreButton.attr("data-term")}/$it"
|
||||||
|
val jsonResponse = app.get(ajaxURL)
|
||||||
|
val json = parseJson<MoreEPS>(jsonResponse.text)
|
||||||
|
val document = Jsoup.parse(json.output?.replace("""\""", ""))
|
||||||
|
document.select("a").map {
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
it.attr("href"),
|
||||||
|
it.text(),
|
||||||
|
fseason,
|
||||||
|
it.text().getIntFromText(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else return@apmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTvSeriesLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.TvSeries,
|
||||||
|
episodes.distinct().sortedBy { it.episode }) {
|
||||||
|
this.duration = duration
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.tags = tags
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.recommendations = recommendations
|
||||||
|
addActors(actors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
app.get(data).document
|
||||||
|
.select("ul.List--Download--Mycima--Single:nth-child(2) li").map {
|
||||||
|
it.select("a").map { linkElement ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
linkElement.attr("href"),
|
||||||
|
this.mainUrl,
|
||||||
|
quality = linkElement.select("resolution").text().getIntFromText() ?: 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.flatten()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
Shahid4uProvider/build.gradle.kts
Normal file
12
Shahid4uProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "TvSeries" , "Movie" , "Anime" , "AsianDrama" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=shahid4u.cc&sz=24"
|
||||||
|
}
|
2
Shahid4uProvider/src/main/AndroidManifest.xml
Normal file
2
Shahid4uProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.shahid4u"/>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.shahid4u
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class Shahid4uPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(Shahid4u())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package com.shahid4u
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class Shahid4u : MainAPI() {
|
||||||
|
override var lang = "ar"
|
||||||
|
override var mainUrl = "https://shahid4u.cc"
|
||||||
|
override var name = "Shahid4u"
|
||||||
|
override val usesWebView = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes =
|
||||||
|
setOf(TvType.TvSeries, TvType.Movie, TvType.Anime, TvType.AsianDrama)
|
||||||
|
|
||||||
|
private fun Element.toSearchResponse(): SearchResponse? {
|
||||||
|
val urlElement = select("a.fullClick")
|
||||||
|
val posterUrl =
|
||||||
|
select("a.image img").let { it.attr("data-src").ifEmpty { it.attr("data-image") } }
|
||||||
|
val quality = select("span.quality").text().replace("1080p |-".toRegex(), "")
|
||||||
|
val type =
|
||||||
|
if (select(".category").text().contains("افلام")) TvType.Movie else TvType.TvSeries
|
||||||
|
return MovieSearchResponse(
|
||||||
|
urlElement.attr("title")
|
||||||
|
.replace("برنامج|فيلم|مترجم|اون لاين|مسلسل|مشاهدة|انمي|أنمي".toRegex(), ""),
|
||||||
|
urlElement.attr("href") ?: return null,
|
||||||
|
this@Shahid4u.name,
|
||||||
|
type,
|
||||||
|
posterUrl,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
quality = getQualityFromString(quality)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$mainUrl/movies-2/page/" to "Movies",
|
||||||
|
"$mainUrl/netflix/page/" to "Series & Anime",
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
val doc = app.get(request.data + page).document
|
||||||
|
val list = doc.select("div.content-box")
|
||||||
|
.mapNotNull { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse> {
|
||||||
|
val finalResult = arrayListOf<SearchResponse>()
|
||||||
|
listOf(
|
||||||
|
"$mainUrl/?s=$query&category=&type=movie",
|
||||||
|
"$mainUrl/?s=$query&type=series"
|
||||||
|
).apmap { url ->
|
||||||
|
app.get(url).document.select("div.content-box").mapNotNull {
|
||||||
|
finalResult.add(it.toSearchResponse() ?: return@mapNotNull null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finalResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val isMovie =
|
||||||
|
doc.select("ul.half-tags:contains(القسم) li:nth-child(2)").text().contains("افلام")
|
||||||
|
val posterUrl =
|
||||||
|
doc.select("a.poster-image").attr("style").replace(".*url\\(|\\);".toRegex(), "")
|
||||||
|
|
||||||
|
val year = doc.select("ul.half-tags:contains(السنة) li:nth-child(2)").text().toIntOrNull()
|
||||||
|
|
||||||
|
val title =
|
||||||
|
doc.select("div.breadcrumb a:nth-child(3)").text()
|
||||||
|
.replace(
|
||||||
|
"الموسم الأول|برنامج|فيلم|مترجم|اون لاين|مسلسل|مشاهدة|انمي|أنمي|$year".toRegex(),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
val tags = doc.select("ul.half-tags:contains(النوع) li").not(":nth-child(1)").map {
|
||||||
|
it.text()
|
||||||
|
}
|
||||||
|
val recommendations =
|
||||||
|
doc.select("div.MediaGrid").first()?.select("div.content-box")?.mapNotNull {
|
||||||
|
it.toSearchResponse()
|
||||||
|
}
|
||||||
|
val synopsis = doc.select("div.post-story:contains(قصة) p").text()
|
||||||
|
|
||||||
|
val rating = doc.select("div.imdbR div span").text().toRatingInt()
|
||||||
|
return if (isMovie) {
|
||||||
|
newMovieLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.Movie,
|
||||||
|
url
|
||||||
|
) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
this.recommendations = recommendations
|
||||||
|
this.rating = rating
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val episodes = ArrayList<Episode>()
|
||||||
|
val episodeElement = doc.select("div.MediaGrid")
|
||||||
|
val allEpisodesUrl = doc.select("div.btns:contains(جميع الحلقات) a").attr("href")
|
||||||
|
if(allEpisodesUrl.isNotEmpty()) {
|
||||||
|
app.get(allEpisodesUrl).document.select("div.row > div").let {
|
||||||
|
it.mapIndexedNotNull { index, element ->
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
element.select("a.fullClick").attr("href"),
|
||||||
|
element.select("a.fullClick").attr("title"),
|
||||||
|
1,
|
||||||
|
it.size - index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episodeElement[1].select("div.content-box").apmap {
|
||||||
|
val seasonNumber = it.select("div.number em").text().toIntOrNull()
|
||||||
|
val seasonUrl = it.select("a.fullClick").attr("href")
|
||||||
|
app.get(seasonUrl).document.select(".episode-block").map { episode ->
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
episode.select("a").attr("href"),
|
||||||
|
episode.select("div.title").text(),
|
||||||
|
seasonNumber,
|
||||||
|
episode.select("div.number em").text().toIntOrNull(),
|
||||||
|
episode.select("div.poster img").attr("data-image")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTvSeriesLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
TvType.TvSeries,
|
||||||
|
episodes.distinct().sortedBy { it.episode }) {
|
||||||
|
this.posterUrl = posterUrl
|
||||||
|
this.year = year
|
||||||
|
this.plot = synopsis
|
||||||
|
this.tags = tags
|
||||||
|
this.recommendations = recommendations
|
||||||
|
this.rating = rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val watchUrl = "$data/watch"
|
||||||
|
app.get(watchUrl).document.select(
|
||||||
|
".servers-list li:contains(ok), li:contains(Streamtape), li:contains(DoodStream), li:contains(Uqload), li:contains(Voe), li:contains(VIDBOM), li:contains(Upstream)"
|
||||||
|
).forEach {
|
||||||
|
val id = it.attr("data-id")
|
||||||
|
val i = it.attr("data-i")
|
||||||
|
val sourceUrl = app.post(
|
||||||
|
"https://shahed4u.mx/wp-content/themes/Shahid4u-WP_HOME/Ajaxat/Single/Server.php",
|
||||||
|
headers = mapOf("referer" to watchUrl, "x-requested-with" to "XMLHttpRequest"),
|
||||||
|
data = mapOf("id" to id, "i" to i)
|
||||||
|
).document.select("iframe").attr("src")
|
||||||
|
loadExtractor(sourceUrl, watchUrl, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
12
YallaShootsProvider/build.gradle.kts
Normal file
12
YallaShootsProvider/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
description = ""
|
||||||
|
authors = listOf( "ImZaw" )
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
tvTypes = listOf( "Live" )
|
||||||
|
|
||||||
|
iconUrl = "https://www.google.com/s2/favicons?domain=www.yalla-shoots.com&sz=24"
|
||||||
|
}
|
2
YallaShootsProvider/src/main/AndroidManifest.xml
Normal file
2
YallaShootsProvider/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.yallashoots"/>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.yallashoots
|
||||||
|
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
|
||||||
|
import com.lagradost.cloudstream3.plugins.Plugin
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
@CloudstreamPlugin
|
||||||
|
class YallaShootsPlugin: Plugin() {
|
||||||
|
override fun load(context: Context) {
|
||||||
|
registerMainAPI(YallaShoots())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.yallashoots
|
||||||
|
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class YallaShoots : MainAPI() {
|
||||||
|
override var mainUrl = "https://www.yalla-shoots.com"
|
||||||
|
override var name = "Yalla Shoots"
|
||||||
|
override var lang = "ar"
|
||||||
|
override val hasDownloadSupport = false
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Live
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
|
||||||
|
val dataMap = mapOf(
|
||||||
|
"Matches Today" to "$mainUrl",
|
||||||
|
)
|
||||||
|
return HomePageResponse(dataMap.apmap { (title, data) ->
|
||||||
|
val document = app.get(data).document
|
||||||
|
val shows = document.select("div.albaflex > div.live").mapNotNull {
|
||||||
|
if(it.select("a").attr("href") === "$mainUrl/#/") return@mapNotNull null
|
||||||
|
val linkElement = it.select("a")
|
||||||
|
LiveSearchResponse(
|
||||||
|
linkElement.attr("title"),
|
||||||
|
linkElement.attr("href"),
|
||||||
|
this@YallaShoots.name,
|
||||||
|
TvType.Live,
|
||||||
|
document.select(".blog-post:contains(${linkElement.attr("title").replace("Vs ","و")}) img").attr("src"),
|
||||||
|
lang = "ar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HomePageList(
|
||||||
|
title,
|
||||||
|
shows.ifEmpty {
|
||||||
|
arrayListOf(LiveSearchResponse(
|
||||||
|
"لا يوجد اي مباراة حاليا",
|
||||||
|
mainUrl,
|
||||||
|
this@YallaShoots.name,
|
||||||
|
TvType.Live,
|
||||||
|
"$mainUrl/wp-content/uploads/2021/12/يلا-شوت-1.png",
|
||||||
|
lang = "ar"
|
||||||
|
))
|
||||||
|
},
|
||||||
|
isHorizontalImages = true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val title = doc.select("h1").text()
|
||||||
|
val poster = fixUrl(doc.select("img.img-responsive").attr("src"))
|
||||||
|
return LiveStreamLoadResponse(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
this.name,
|
||||||
|
doc.select("iframe[loading=\"lazy\"]").attr("src"),
|
||||||
|
poster,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override suspend fun loadLinks(
|
||||||
|
data: String,
|
||||||
|
isCasting: Boolean,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val doc = app.get("$data?serv=2").document
|
||||||
|
val sourceLink = doc.select("script:contains(.m3u8)").html().replace(".*hls: \"|\"\\};.*".toRegex(),"")
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
sourceLink,
|
||||||
|
mainUrl,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
86
build.gradle.kts
Normal file
86
build.gradle.kts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import com.lagradost.cloudstream3.gradle.CloudstreamExtension
|
||||||
|
import com.android.build.gradle.BaseExtension
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
// Shitpack repo which contains our tools and dependencies
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:7.0.4")
|
||||||
|
// Cloudstream gradle plugin which makes everything work and builds plugins
|
||||||
|
classpath("com.github.recloudstream:gradle:master-SNAPSHOT")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.cloudstream(configuration: CloudstreamExtension.() -> Unit) = extensions.getByName<CloudstreamExtension>("cloudstream").configuration()
|
||||||
|
|
||||||
|
fun Project.android(configuration: BaseExtension.() -> Unit) = extensions.getByName<BaseExtension>("android").configuration()
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply(plugin = "com.android.library")
|
||||||
|
apply(plugin = "kotlin-android")
|
||||||
|
apply(plugin = "com.lagradost.cloudstream3.gradle")
|
||||||
|
|
||||||
|
cloudstream {
|
||||||
|
// when running through github workflow, GITHUB_REPOSITORY should contain current repository name
|
||||||
|
setRepo(System.getenv("GITHUB_REPOSITORY") ?: "user/repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion(30)
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8" // Required
|
||||||
|
// Disables some unnecessary features
|
||||||
|
freeCompilerArgs = freeCompilerArgs +
|
||||||
|
"-Xno-call-assertions" +
|
||||||
|
"-Xno-param-assertions" +
|
||||||
|
"-Xno-receiver-assertions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val apk by configurations
|
||||||
|
val implementation by configurations
|
||||||
|
|
||||||
|
// Stubs for all Cloudstream classes
|
||||||
|
apk("com.lagradost:cloudstream3:pre-release")
|
||||||
|
|
||||||
|
// these dependencies can include any of those which are added by the app,
|
||||||
|
// but you dont need to include any of them if you dont need them
|
||||||
|
// https://github.com/recloudstream/cloudstream/blob/master/app/build.gradle
|
||||||
|
implementation(kotlin("stdlib")) // adds standard kotlin features, like listOf, mapOf etc
|
||||||
|
implementation("com.github.Blatzar:NiceHttp:0.3.2") // http library
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1") // html parser
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.+")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task<Delete>("clean") {
|
||||||
|
delete(rootProject.buildDir)
|
||||||
|
}
|
19
gradle.properties
Normal file
19
gradle.properties
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
android.enableJetifier=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Sun Feb 20 16:26:11 CET 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
172
gradlew
vendored
Normal file
172
gradlew
vendored
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
84
gradlew.bat
vendored
Normal file
84
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
13
settings.gradle.kts
Normal file
13
settings.gradle.kts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
rootProject.name = "CloudstreamExtensionsArabic"
|
||||||
|
|
||||||
|
val disabled = listOf<String>("")
|
||||||
|
|
||||||
|
File(rootDir, ".").eachDir { dir ->
|
||||||
|
if (!disabled.contains(dir.name) && File(dir, "build.gradle.kts").exists()) {
|
||||||
|
include(dir.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.eachDir(block: (File) -> Unit) {
|
||||||
|
listFiles()?.filter { it.isDirectory }?.forEach { block(it) }
|
||||||
|
}
|
Loading…
Reference in a new issue