diff --git a/Bolly2TollyProvider/build.gradle.kts b/Bolly2TollyProvider/build.gradle.kts new file mode 100644 index 0000000..94c2c47 --- /dev/null +++ b/Bolly2TollyProvider/build.gradle.kts @@ -0,0 +1,25 @@ +version = 1 + + +cloudstream { + language = "hi" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("darkdemon") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=bolly2tolly.desi/&sz=%size%" +} diff --git a/Bolly2TollyProvider/src/main/AndroidManifest.xml b/Bolly2TollyProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..36bb4af --- /dev/null +++ b/Bolly2TollyProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyPlugin.kt b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyPlugin.kt new file mode 100644 index 0000000..2e9d15b --- /dev/null +++ b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyPlugin.kt @@ -0,0 +1,15 @@ +package com.darkdemon + +import android.content.Context +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin + +@CloudstreamPlugin +class Bolly2TollyPlugin : Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Bolly2TollyProvider()) + registerExtractorAPI(NeoHD()) + registerExtractorAPI(NinjaHD()) + } +} diff --git a/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyProvider.kt b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyProvider.kt new file mode 100644 index 0000000..e9a6571 --- /dev/null +++ b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/Bolly2TollyProvider.kt @@ -0,0 +1,154 @@ +package com.darkdemon + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element + +class Bolly2TollyProvider : MainAPI() { // all providers must be an instance of MainAPI + override var mainUrl = "https://www.bolly2tolly.desi" + override var name = "Bolly2Tolly" + override val hasMainPage = true + override var lang = "hi" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries + ) + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "Latest ", + "$mainUrl/category/english-movies/page/" to "English", + "$mainUrl/category/hindi-movies/page/" to "Hindi", + "$mainUrl/category/telugu-movies/page/" to "Telugu", + "$mainUrl/category/tamil-movies/page/" to "Tamil", + "$mainUrl/category/kannada-movies/page/" to "Kannada", + "$mainUrl/category/malayalam-movies/page/" to "Malayalam", + "$mainUrl/category/bengali-movies/page/" to "Bengali" + + + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("ul.MovieList article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = if (this.selectFirst("img")?.attr("alt").isNullOrEmpty()) + this.selectFirst("h3")?.text()?.substringBefore("(") else this.selectFirst("img") + ?.attr("alt")?.trim() + val href = fixUrl(this.selectFirst("a")?.attr("href").toString()) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + + return newMovieSearchResponse(title ?: return null, href, TvType.Movie) { + this.posterUrl = posterUrl + } + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/?s=$query").document + + return document.select(".result-item").mapNotNull { + val title = it.select("SubTitle").text().trim() + val href = fixUrl(it.selectFirst(".title a")?.attr("href").toString()) + val posterUrl = fixUrlNull(it.selectFirst(".thumbnail img")?.attr("src")) + val quality = getQualityFromString(it.select("span.quality").text()) + val tvtype = if (href.contains("tvshows")) TvType.TvSeries else TvType.Movie + newMovieSearchResponse(title, href, tvtype) { + this.posterUrl = posterUrl + this.quality = quality + } + } + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst(".SubTitle")?.text()?.trim() ?: return null + val poster = fixUrlNull(document.selectFirst(".Image img")?.attr("src")) + val tags = document.select(".InfoList li:eq(2) a").map { it.text() } + val year = document.select("span.Date").text().trim().toIntOrNull() + val tvType = + if (document.select(".AA-cont").isNullOrEmpty()) TvType.Movie else TvType.TvSeries + val description = document.selectFirst(".Description p")?.text()?.trim() + //val rating = document.select(".post-ratings strong").last()!!.text().toRatingInt() + val actors = document.select(".ListCast a").map { it.text().trim() } + val recommendations = document.select(".Wdgt ul.MovieList li").mapNotNull { + it.toSearchResult() + } + + return if (tvType == TvType.TvSeries) { + val episodes = document.select("tbody tr").mapNotNull { + val href = fixUrl(it.select(".MvTbTtl a").attr("href") ?: return null) + Log.d("href", href) + val name = it.select(".MvTbTtl a").text().trim() + val thumbs = "https:" + it.select("img").attr("src") + val season = document.select(".AA-Season").attr("data-tab").toInt() + val episode = it.select("span.Num").text().toInt() + Episode( + href, + name, + season, + episode, + thumbs + ) + } + + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + //this.rating = rating + addActors(actors) + this.recommendations = recommendations + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + //this.rating = rating + addActors(actors) + this.recommendations = recommendations + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + println(data) + val sources = mutableListOf() + val document = app.get(data).document + sources.add(document.select(".TPlayer iframe").attr("src")) + val srcRegex = Regex("""(https.*?)"\s""") + srcRegex.find( + document.select(".TPlayer").text() + )?.groupValues?.map { sources.add(it.replace("#038;", "")) } + println(sources) + sources.forEach { + val source = app.get(it, referer = data).document.select("iframe").attr("src") + println(source) + loadExtractor( + source, + subtitleCallback, + callback + ) + } + return true + } +} diff --git a/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/NeoHD.kt b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/NeoHD.kt new file mode 100644 index 0000000..919b024 --- /dev/null +++ b/Bolly2TollyProvider/src/main/kotlin/com/darkdemon/NeoHD.kt @@ -0,0 +1,145 @@ +package com.darkdemon + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class NinjaHD : NeoHD() { + override var name = "NinjaHD" + override var mainUrl = "https://ninjahd.one" +} + +open class NeoHD : ExtractorApi() { + override val name = "NeoHD" + override val mainUrl = "https://neohd.xyz" + override val requiresReferer = false + + override suspend fun getUrl(url: String, referer: String?): List { + val sources = mutableListOf() + val document = app.get(url).text + val cryptoRegex = Regex("""var\s*playerConfig\s*=\s*([^;]+)""") + val json = cryptoRegex.find(document)?.groupValues?.getOrNull(1).toString() + val password = "F1r3b4Ll_GDP~5H".toByteArray() + val data1 = parseJson(json) + val decryptedData = + cryptoAESHandler(data1, password, false)?.replace("\\", "")?.substringAfter("\"") + ?.substringBeforeLast("\"") + val apiQuery = parseJson(decryptedData!!).apiQuery + val doc = app.get( + url = "https://ninjahd.one/api/?$apiQuery&_=${System.currentTimeMillis() * 1000}", + headers = mapOf( + "X-Requested-With" to "XMLHttpRequest", + "Referer" to "https://ninjahd.one/embed/zilnv7x6da1s84" + ) + ).text + val source = parseJson(doc).sources[0].file + sources.add( + ExtractorLink( + name, + name, + source, + "$mainUrl/", + Qualities.Unknown.value, + headers = mapOf("range" to "bytes=0-") + ) + ) + return sources + } + + private fun GenerateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = "MD5", + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): List? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return listOf( + generatedData.copyOfRange(0, keyLength), + generatedData.copyOfRange(keyLength, targetKeySize) + ) + } catch (e: DigestException) { + return null + } + } + + private fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private fun cryptoAESHandler( + data: AesData, + pass: ByteArray, + encrypt: Boolean = true + ): String? { + val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null + val cipher = Cipher.getInstance("AES/CBC/NoPadding") + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(data.ct))) + } else { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(data.ct.toByteArray())) + + } + } + + data class AesData( + @JsonProperty("ct") var ct: String, + @JsonProperty("iv") var iv: String, + @JsonProperty("s") var s: String + ) + + data class CryptoResponse( + @JsonProperty("apiQuery") var apiQuery: String + ) + + data class VideoUrl( + @JsonProperty("sources") var sources: ArrayList = arrayListOf(), + ) + + data class Sources( + @JsonProperty("file") var file: String + ) +}