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
+ )
+}