From 0465f91c71b4e64c24968872a265deac9301f6f1 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 17 Jan 2024 04:20:08 +0700 Subject: [PATCH] move superstream --- .github/workflows/build.yml | 8 + .../main/kotlin/com/hexated/SoraExtractor.kt | 4 +- .../src/main/kotlin/com/hexated/SoraUtils.kt | 13 - .../src/main/kotlin/com/hexated/StremioC.kt | 8 +- .../src/main/kotlin/com/hexated/StremioX.kt | 2 +- .../main/kotlin/com/hexated/SubsExtractors.kt | 4 +- StremioX/src/main/kotlin/com/hexated/Utils.kt | 15 - Superstream/build.gradle.kts | 42 + Superstream/src/main/AndroidManifest.xml | 2 + .../src/main/kotlin/com/hexated/Extractors.kt | 242 ++++++ .../main/kotlin/com/hexated/Superstream.kt | 818 ++++++++++++++++++ .../kotlin/com/hexated/SuperstreamPlugin.kt | 14 + 12 files changed, 1135 insertions(+), 37 deletions(-) create mode 100644 Superstream/build.gradle.kts create mode 100644 Superstream/src/main/AndroidManifest.xml create mode 100644 Superstream/src/main/kotlin/com/hexated/Extractors.kt create mode 100644 Superstream/src/main/kotlin/com/hexated/Superstream.kt create mode 100644 Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5e17d0d..4e558ec6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,10 @@ jobs: SFMOVIES_API: ${{ secrets.SFMOVIES_API }} CINEMATV_API: ${{ secrets.CINEMATV_API }} OMOVIES_API: ${{ secrets.OMOVIES_API }} + SUPERSTREAM_FIRST_API: ${{ secrets.SUPERSTREAM_FIRST_API }} + SUPERSTREAM_SECOND_API: ${{ secrets.SUPERSTREAM_SECOND_API }} + SUPERSTREAM_THIRD_API: ${{ secrets.SUPERSTREAM_THIRD_API }} + SUPERSTREAM_FOURTH_API: ${{ secrets.SUPERSTREAM_FOURTH_API }} run: | cd $GITHUB_WORKSPACE/src echo TMDB_API=$TMDB_API >> local.properties @@ -70,6 +74,10 @@ jobs: echo SFMOVIES_API=$SFMOVIES_API >> local.properties echo CINEMATV_API=$CINEMATV_API >> local.properties echo OMOVIES_API=$OMOVIES_API >> local.properties + echo SUPERSTREAM_FIRST_API=$SUPERSTREAM_FIRST_API >> local.properties + echo SUPERSTREAM_SECOND_API=$SUPERSTREAM_SECOND_API >> local.properties + echo SUPERSTREAM_THIRD_API=$SUPERSTREAM_THIRD_API >> local.properties + echo SUPERSTREAM_FOURTH_API=$SUPERSTREAM_FOURTH_API >> local.properties - name: Build Plugins run: | diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 9823c0c2..8cdb4848 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -1834,7 +1834,7 @@ object SoraExtractor : SoraStream() { data = mapOf( "index" to "0", "mid" to "$id", - "wsk" to "f6ea6cde-e42b-4c26-98d3-b4fe48cdd4fb", + "wsk" to "30fb68aa-1c71-4b8c-b5d4-4ca9222cfb45", "lid" to "", "liu" to "" ), @@ -2053,7 +2053,7 @@ object SoraExtractor : SoraStream() { "$dahmerMoviesAPI/tvs/${title?.replace(":", " -")}/Season $season/" } - val request = app.get(url, interceptor = TimeOutInterceptor()) + val request = app.get(url, timeout = 60L) if (!request.isSuccessful) return val paths = request.document.select("a").map { it.text() to it.attr("href") diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index e85277a1..6c7fde38 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -1286,19 +1286,6 @@ private enum class Symbol(val decimalValue: Int) { } } -class TimeOutInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val call = chain - .withConnectTimeout(60, TimeUnit.SECONDS) - .withReadTimeout(60, TimeUnit.SECONDS) - .withWriteTimeout(60, TimeUnit.SECONDS) - .request() - .newBuilder() - .build() - return chain.proceed(call) - } -} - // steal from https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/aniwave/src/eu/kanade/tachiyomi/animeextension/en/nineanime/AniwaveUtils.kt // credits to @samfundev object AniwaveUtils { diff --git a/StremioX/src/main/kotlin/com/hexated/StremioC.kt b/StremioX/src/main/kotlin/com/hexated/StremioC.kt index b6137816..b4871b26 100644 --- a/StremioX/src/main/kotlin/com/hexated/StremioC.kt +++ b/StremioX/src/main/kotlin/com/hexated/StremioC.kt @@ -65,7 +65,7 @@ class StremioC : MainAPI() { val loadData = parseJson(data) val request = app.get( "${mainUrl}/stream/${loadData.type}/${loadData.id}.json", - interceptor = interceptor + timeout = 120L ) if (request.isSuccessful) { val res = request.parsedSafe() @@ -110,7 +110,7 @@ class StremioC : MainAPI() { sites.filter { it.parentJavaClass == "StremioX" }.apmap { site -> val res = app.get( "${site.url.fixSourceUrl()}/stream/${type}/${id}.json", - interceptor = interceptor + timeout = 120L ).parsedSafe() res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) @@ -154,7 +154,7 @@ class StremioC : MainAPI() { types.forEach { type -> val res = app.get( "${provider.mainUrl}/catalog/${type}/${id}/search=${query}.json", - interceptor = interceptor + timeout = 120L ).parsedSafe() res?.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) @@ -168,7 +168,7 @@ class StremioC : MainAPI() { types.forEach { type -> val res = app.get( "${provider.mainUrl}/catalog/${type}/${id}.json", - interceptor = interceptor + timeout = 120L ).parsedSafe() res?.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) diff --git a/StremioX/src/main/kotlin/com/hexated/StremioX.kt b/StremioX/src/main/kotlin/com/hexated/StremioX.kt index 995479fa..44d98f47 100644 --- a/StremioX/src/main/kotlin/com/hexated/StremioX.kt +++ b/StremioX/src/main/kotlin/com/hexated/StremioX.kt @@ -238,7 +238,7 @@ class StremioX : TmdbProvider() { } else { "$fixMainUrl/stream/series/$imdbId:$season:$episode.json" } - val res = app.get(url, interceptor = interceptor).parsedSafe() + val res = app.get(url, timeout = 120L).parsedSafe() res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) } diff --git a/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt b/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt index 122a7225..0af2d9fd 100644 --- a/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt +++ b/StremioX/src/main/kotlin/com/hexated/SubsExtractors.kt @@ -20,7 +20,7 @@ object SubsExtractors { } else { "series/$imdbId:$season:$episode" } - app.get("${openSubAPI}/subtitles/$slug.json", interceptor = interceptor).parsedSafe()?.subtitles?.map { sub -> + app.get("${openSubAPI}/subtitles/$slug.json", timeout = 120L).parsedSafe()?.subtitles?.map { sub -> subtitleCallback.invoke( SubtitleFile( SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang @@ -42,7 +42,7 @@ object SubsExtractors { "${watchSomuchAPI}/Watch/ajMovieTorrents.aspx", data = mapOf( "index" to "0", "mid" to "$id", - "wsk" to "f6ea6cde-e42b-4c26-98d3-b4fe48cdd4fb", + "wsk" to "30fb68aa-1c71-4b8c-b5d4-4ca9222cfb45", "lid" to "", "liu" to "" ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") diff --git a/StremioX/src/main/kotlin/com/hexated/Utils.kt b/StremioX/src/main/kotlin/com/hexated/Utils.kt index 6640cbc4..51feea3f 100644 --- a/StremioX/src/main/kotlin/com/hexated/Utils.kt +++ b/StremioX/src/main/kotlin/com/hexated/Utils.kt @@ -9,21 +9,6 @@ import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit -val interceptor = TimeOutInterceptor() - -class TimeOutInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val call = chain - .withConnectTimeout(60, TimeUnit.SECONDS) - .withReadTimeout(60, TimeUnit.SECONDS) - .withWriteTimeout(60, TimeUnit.SECONDS) - .request() - .newBuilder() - .build() - return chain.proceed(call) - } -} - fun String.fixSourceUrl(): String { return this.replace("/manifest.json", "").replace("stremio://", "https://") } diff --git a/Superstream/build.gradle.kts b/Superstream/build.gradle.kts new file mode 100644 index 00000000..6109c0eb --- /dev/null +++ b/Superstream/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.konan.properties.Properties + +// use an integer for version numbers +version = 1 + +android { + defaultConfig { + val properties = Properties() + properties.load(project.rootProject.file("local.properties").inputStream()) + + buildConfigField("String", "SUPERSTREAM_FIRST_API", "\"${properties.getProperty("SUPERSTREAM_FIRST_API")}\"") + buildConfigField("String", "SUPERSTREAM_SECOND_API", "\"${properties.getProperty("SUPERSTREAM_SECOND_API")}\"") + buildConfigField("String", "SUPERSTREAM_THIRD_API", "\"${properties.getProperty("SUPERSTREAM_THIRD_API")}\"") + buildConfigField("String", "SUPERSTREAM_FOURTH_API", "\"${properties.getProperty("SUPERSTREAM_FOURTH_API")}\"") + } +} + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Blatzar") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "Anime", + "TvSeries", + "Movie", + ) + + + iconUrl = "https://cdn.discordapp.com/attachments/1109266606292488297/1196694385061003334/icon.png" +} \ No newline at end of file diff --git a/Superstream/src/main/AndroidManifest.xml b/Superstream/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/Superstream/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Superstream/src/main/kotlin/com/hexated/Extractors.kt b/Superstream/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..73a21ff6 --- /dev/null +++ b/Superstream/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,242 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* + +object Extractors : Superstream() { + + suspend fun invokeInternalSource( + id: Int? = null, + type: Int? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ) { + fun LinkList.toExtractorLink(): ExtractorLink? { + if (this.path.isNullOrBlank()) return null + return ExtractorLink( + "Internal", + "Internal [${this.size}]", + this.path.replace("\\/", ""), + "", + getQualityFromName(this.quality), + ) + } + + // No childmode when getting links + // New api does not return video links :( + val query = if (type == ResponseTypes.Movies.value) { + """{"childmode":"0","uid":"","app_version":"11.5","appid":"$appId","module":"Movie_downloadurl_v3","channel":"Website","mid":"$id","lang":"","expired_date":"${getExpiryDate()}","platform":"android","oss":"1","group":""}""" + } else { + """{"childmode":"0","app_version":"11.5","module":"TV_downloadurl_v3","channel":"Website","episode":"$episode","expired_date":"${getExpiryDate()}","platform":"android","tid":"$id","oss":"1","uid":"","appid":"$appId","season":"$season","lang":"en","group":""}""" + } + + val linkData = queryApiParsed(query, false) + linkData.data?.list?.forEach { + callback.invoke(it.toExtractorLink() ?: return@forEach) + } + + // Should really run this query for every link :( + val fid = linkData.data?.list?.firstOrNull { it.fid != null }?.fid + + val subtitleQuery = if (type == ResponseTypes.Movies.value) { + """{"childmode":"0","fid":"$fid","uid":"","app_version":"11.5","appid":"$appId","module":"Movie_srt_list_v2","channel":"Website","mid":"$id","lang":"en","expired_date":"${getExpiryDate()}","platform":"android"}""" + } else { + """{"childmode":"0","fid":"$fid","app_version":"11.5","module":"TV_srt_list_v2","channel":"Website","episode":"$episode","expired_date":"${getExpiryDate()}","platform":"android","tid":"$id","uid":"","appid":"$appId","season":"$season","lang":"en"}""" + } + + val subtitles = queryApiParsed(subtitleQuery).data + subtitles?.list?.forEach { subs -> + val sub = subs.subtitles.maxByOrNull { it.support_total ?: 0 } + subtitleCallback.invoke( + SubtitleFile( + sub?.language ?: sub?.lang ?: return@forEach, + sub?.filePath ?: return@forEach + ) + ) + } + } + + suspend fun invokeExternalSource( + mediaId: Int? = null, + type: Int? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode) + val shareKey = app.get( + "$fourthAPI/index/share_link?id=${mediaId}&type=$type" + ).parsedSafe()?.data?.link?.substringAfterLast("/") + + val headers = mapOf("Accept-Language" to "en") + val shareRes = app.get( + "$thirdAPI/file/file_share_list?share_key=${shareKey ?: return}", + headers = headers + ).parsedSafe()?.data + + val fids = if (season == null) { + shareRes?.file_list + } else { + val parentId = + shareRes?.file_list?.find { it.file_name.equals("season $season", true) }?.fid + app.get( + "$thirdAPI/file/file_share_list?share_key=$shareKey&parent_id=$parentId&page=1", + headers = headers + ).parsedSafe()?.data?.file_list?.filter { + it.file_name?.contains( + "s${seasonSlug}e${episodeSlug}", + true + ) == true + } + } + + fids?.apmapIndexed { index, fileList -> + val player = app.get("$thirdAPI/file/player?fid=${fileList.fid}&share_key=$shareKey").text + val video = """"(https.*?m3u8.*?)"""".toRegex().find(player)?.groupValues?.get(1) + callback.invoke( + ExtractorLink( + "External", + "External [Server ${index + 1}]", + video?.replace("\\/", "/") ?: return@apmapIndexed, + "$thirdAPI/", + getIndexQuality(fileList.file_name), + isM3u8 = true + ) + ) + } + } + + suspend fun invokeWatchsomuch( + imdbId: String? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + ) { + val id = imdbId?.removePrefix("tt") + val epsId = app.post( + "$watchSomuchAPI/Watch/ajMovieTorrents.aspx", + data = mapOf( + "index" to "0", + "mid" to "$id", + "wsk" to "30fb68aa-1c71-4b8c-b5d4-4ca9222cfb45", + "lid" to "", + "liu" to "" + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe()?.movie?.torrents?.let { eps -> + if (season == null) { + eps.firstOrNull()?.id + } else { + eps.find { it.episode == episode && it.season == season }?.id + } + } ?: return + + val (seasonSlug, episodeSlug) = getEpisodeSlug( + season, + episode + ) + + val subUrl = if (season == null) { + "$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part=" + } else { + "$watchSomuchAPI/Watch/ajMovieSubtitles.aspx?mid=$id&tid=$epsId&part=S${seasonSlug}E${episodeSlug}" + } + + app.get(subUrl) + .parsedSafe()?.subtitles + ?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + sub.label ?: "", + fixUrl(sub.url ?: return@map null, watchSomuchAPI) + ) + ) + } + + + } + + suspend fun invokeOpenSubs( + imdbId: String? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + ) { + val slug = if(season == null) { + "movie/$imdbId" + } else { + "series/$imdbId:$season:$episode" + } + app.get("${openSubAPI}/subtitles/$slug.json", timeout = 120L).parsedSafe()?.subtitles?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang + ?: return@map, + sub.url ?: return@map + ) + ) + } + } + + suspend fun invokeVidsrcto( + imdbId: String?, + season: Int?, + episode: Int?, + subtitleCallback: (SubtitleFile) -> Unit, + ) { + val url = if (season == null) { + "$vidsrctoAPI/embed/movie/$imdbId" + } else { + "$vidsrctoAPI/embed/tv/$imdbId/$season/$episode" + } + + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val subtitles = app.get("$vidsrctoAPI/ajax/embed/episode/$mediaId/subtitles").text + AppUtils.tryParseJson>(subtitles)?.map { + subtitleCallback.invoke( + SubtitleFile( + it.label ?: "", + it.file ?: return@map + ) + ) + } + + } + + private fun fixUrl(url: String, domain: String): String { + if (url.startsWith("http")) { + return url + } + if (url.isEmpty()) { + return "" + } + + val startsWithNoHttp = url.startsWith("//") + if (startsWithNoHttp) { + return "https:$url" + } else { + if (url.startsWith('/')) { + return domain + url + } + return "$domain/$url" + } + } + + private fun getIndexQuality(str: String?): Int { + return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + + private fun getEpisodeSlug( + season: Int? = null, + episode: Int? = null, + ): Pair { + return if (season == null && episode == null) { + "" to "" + } else { + (if (season!! < 10) "0$season" else "$season") to (if (episode!! < 10) "0$episode" else "$episode") + } + } + +} \ No newline at end of file diff --git a/Superstream/src/main/kotlin/com/hexated/Superstream.kt b/Superstream/src/main/kotlin/com/hexated/Superstream.kt new file mode 100644 index 00000000..525e3607 --- /dev/null +++ b/Superstream/src/main/kotlin/com/hexated/Superstream.kt @@ -0,0 +1,818 @@ +package com.hexated + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.Extractors.invokeExternalSource +import com.hexated.Extractors.invokeInternalSource +import com.hexated.Extractors.invokeOpenSubs +import com.hexated.Extractors.invokeVidsrcto +import com.hexated.Extractors.invokeWatchsomuch +import com.hexated.Superstream.CipherUtils.getVerify +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.capitalize +import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.nicehttp.NiceResponse +import okhttp3.Interceptor +import okhttp3.Response +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import javax.crypto.Cipher +import javax.crypto.Cipher.DECRYPT_MODE +import javax.crypto.Cipher.ENCRYPT_MODE +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import kotlin.math.roundToInt + +open class Superstream : MainAPI() { + private val timeout = 60L + override var name = "SuperStream" + override val hasMainPage = true + override val hasChromecastSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + TvType.AnimeMovie, + ) + + enum class ResponseTypes(val value: Int) { + Series(2), + Movies(1); + + fun toTvType(): TvType { + return if (this == Series) TvType.TvSeries else TvType.Movie + } + + companion object { + fun getResponseType(value: Int?): ResponseTypes { + return values().firstOrNull { it.value == value } ?: Movies + } + } + } + + override val instantLinkLoading = true + + private val interceptor = UserAgentInterceptor() + + private val headers = mapOf( + "Platform" to "android", + "Accept" to "charset=utf-8", + ) + + private class UserAgentInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed( + chain.request() + .newBuilder() + .removeHeader("user-agent") + .build() + ) + } + } + + // Random 32 length string + private fun randomToken(): String { + return (0..31).joinToString("") { + (('0'..'9') + ('a'..'f')).random().toString() + } + } + + private val token = randomToken() + + private object CipherUtils { + private const val ALGORITHM = "DESede" + private const val TRANSFORMATION = "DESede/CBC/PKCS5Padding" + fun encrypt(str: String, key: String, iv: String): String? { + return try { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + val bArr = ByteArray(24) + val bytes: ByteArray = key.toByteArray() + var length = if (bytes.size <= 24) bytes.size else 24 + System.arraycopy(bytes, 0, bArr, 0, length) + while (length < 24) { + bArr[length] = 0 + length++ + } + cipher.init( + ENCRYPT_MODE, + SecretKeySpec(bArr, ALGORITHM), + IvParameterSpec(iv.toByteArray()) + ) + + String(Base64.encode(cipher.doFinal(str.toByteArray()), 2), StandardCharsets.UTF_8) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + // Useful for deobfuscation + fun decrypt(str: String, key: String, iv: String): String? { + return try { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + val bArr = ByteArray(24) + val bytes: ByteArray = key.toByteArray() + var length = if (bytes.size <= 24) bytes.size else 24 + System.arraycopy(bytes, 0, bArr, 0, length) + while (length < 24) { + bArr[length] = 0 + length++ + } + cipher.init( + DECRYPT_MODE, + SecretKeySpec(bArr, ALGORITHM), + IvParameterSpec(iv.toByteArray()) + ) + val inputStr = Base64.decode(str.toByteArray(), Base64.DEFAULT) + cipher.doFinal(inputStr).decodeToString() + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + fun md5(str: String): String? { + return MD5Util.md5(str)?.let { HexDump.toHexString(it).lowercase() } + } + + fun getVerify(str: String?, str2: String, str3: String): String? { + if (str != null) { + return md5(md5(str2) + str3 + str) + } + return null + } + } + + private object HexDump { + private val HEX_DIGITS = charArrayOf( + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F' + ) + + @JvmOverloads + fun toHexString(bArr: ByteArray, i: Int = 0, i2: Int = bArr.size): String { + val cArr = CharArray(i2 * 2) + var i3 = 0 + for (i4 in i until i + i2) { + val b = bArr[i4].toInt() + val i5 = i3 + 1 + val cArr2 = HEX_DIGITS + cArr[i3] = cArr2[b ushr 4 and 15] + i3 = i5 + 1 + cArr[i5] = cArr2[b and 15] + } + return String(cArr) + } + } + + private object MD5Util { + fun md5(str: String): ByteArray? { + return md5(str.toByteArray()) + } + + fun md5(bArr: ByteArray?): ByteArray? { + return try { + val digest = MessageDigest.getInstance("MD5") + digest.update(bArr ?: return null) + digest.digest() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + null + } + } + } + + suspend fun queryApi(query: String, useAlternativeApi: Boolean): NiceResponse { + val encryptedQuery = CipherUtils.encrypt(query, key, iv)!! + val appKeyHash = CipherUtils.md5(appKey)!! + val newBody = + """{"app_key":"$appKeyHash","verify":"${ + getVerify( + encryptedQuery, + appKey, + key + ) + }","encrypt_data":"$encryptedQuery"}""" + val base64Body = String(Base64.encode(newBody.toByteArray(), Base64.DEFAULT)) + + val data = mapOf( + "data" to base64Body, + "appid" to "27", + "platform" to "android", + "version" to appVersionCode, + // Probably best to randomize this + "medium" to "Website&token$token" + ) + + val url = if (useAlternativeApi) secondAPI else firstAPI + return app.post( + url, + headers = headers, + data = data, + timeout = timeout, + interceptor = interceptor + ) + } + + suspend inline fun queryApiParsed( + query: String, + useAlternativeApi: Boolean = true + ): T { + return queryApi(query, useAlternativeApi).parsed() + } + + fun getExpiryDate(): Long { + // Current time + 12 hours + return unixTime + 60 * 60 * 12 + } + + private data class PostJSON( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("poster_2") val poster2: String? = null, + @JsonProperty("box_type") val boxType: Int? = null, + @JsonProperty("imdb_rating") val imdbRating: String? = null, + @JsonProperty("quality_tag") val quality_tag: String? = null, + ) + + private data class ListJSON( + @JsonProperty("code") val code: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("box_type") val boxType: Int? = null, + @JsonProperty("list") val list: ArrayList = arrayListOf(), + ) + + private data class DataJSON( + @JsonProperty("data") val data: ArrayList = arrayListOf() + ) + + // We do not want content scanners to notice this scraping going on so we've hidden all constants + // The source has its origins in China so I added some extra security with banned words + // Mayhaps a tiny bit unethical, but this source is just too good :) + // If you are copying this code please use precautions so they do not change their api. + + // Free Tibet, The Tienanmen Square protests of 1989 + private val iv = base64Decode("d0VpcGhUbiE=") + private val key = base64Decode("MTIzZDZjZWRmNjI2ZHk1NDIzM2FhMXc2") + + private val firstAPI = BuildConfig.SUPERSTREAM_FIRST_API + + // Another url because the first one sucks at searching + // This one was revealed to me in a dream + private val secondAPI = BuildConfig.SUPERSTREAM_SECOND_API + + val thirdAPI = BuildConfig.SUPERSTREAM_THIRD_API + val fourthAPI = BuildConfig.SUPERSTREAM_FOURTH_API + + val watchSomuchAPI = "https://watchsomuch.tv" + val openSubAPI = "https://opensubtitles-v3.strem.io" + val vidsrctoAPI = "https://vidsrc.to" + + private val appKey = base64Decode("bW92aWVib3g=") + val appId = base64Decode("Y29tLnRkby5zaG93Ym94") + private val appIdSecond = base64Decode("Y29tLm1vdmllYm94cHJvLmFuZHJvaWQ=") + private val appVersion = "11.5" + private val appVersionCode = "129" + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1 + val data = queryApiParsed( + """{"childmode":"$hideNsfw","app_version":"$appVersion","appid":"$appIdSecond","module":"Home_list_type_v5","channel":"Website","page":"$page","lang":"en","type":"all","pagelimit":"10","expired_date":"${getExpiryDate()}","platform":"android"} + """.trimIndent() + ) + + // Cut off the first row (featured) + val pages = data.data.let { it.subList(minOf(it.size, 1), it.size) } + .mapNotNull { + var name = it.name + if (name.isNullOrEmpty()) name = "Featured" + val postList = it.list.mapNotNull second@{ post -> + val type = if (post.boxType == 1) TvType.Movie else TvType.TvSeries + newMovieSearchResponse( + name = post.title ?: return@second null, + url = LoadData(post.id ?: return@mapNotNull null, post.boxType).toJson(), + type = type, + fix = false + ) { + posterUrl = post.poster ?: post.poster2 + quality = getQualityFromString(post.quality_tag ?: "") + } + } + if (postList.isEmpty()) return@mapNotNull null + HomePageList(name, postList) + } + return HomePageResponse(pages, hasNext = !pages.any { it.list.isEmpty() }) + } + + private data class Data( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("mid") val mid: Int? = null, + @JsonProperty("box_type") val boxType: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("poster_org") val posterOrg: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("cats") val cats: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("imdb_rating") val imdbRating: String? = null, + @JsonProperty("quality_tag") val qualityTag: String? = null, + ) { + fun toSearchResponse(api: MainAPI): MovieSearchResponse? { + return api.newMovieSearchResponse( + this.title ?: "", + LoadData( + this.id ?: this.mid ?: return null, + this.boxType ?: ResponseTypes.Movies.value + ).toJson(), + ResponseTypes.getResponseType(this.boxType).toTvType(), + false + ) { + posterUrl = this@Data.posterOrg ?: this@Data.poster + year = this@Data.year + quality = getQualityFromString(this@Data.qualityTag?.replace("-", "") ?: "") + } + } + } + + private data class MainDataList( + @JsonProperty("list") val list: ArrayList = arrayListOf() + ) + + private data class MainData( + @JsonProperty("data") val data: MainDataList + ) + + override suspend fun search(query: String): List { + val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1 + val apiQuery = + // Originally 8 pagelimit + """{"childmode":"$hideNsfw","app_version":"$appVersion","appid":"$appIdSecond","module":"Search4","channel":"Website","page":"1","lang":"en","type":"all","keyword":"$query","pagelimit":"20","expired_date":"${getExpiryDate()}","platform":"android"}""" + val searchResponse = queryApiParsed(apiQuery, true).data.list.mapNotNull { + it.toSearchResponse(this) + } + return searchResponse + } + + private data class LoadData( + val id: Int, + val type: Int? + ) + + private data class MovieData( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("director") val director: String? = null, + @JsonProperty("writer") val writer: String? = null, + @JsonProperty("actors") val actors: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("description") val description: String? = null, + @JsonProperty("cats") val cats: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("imdb_rating") val imdbRating: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("content_rating") val contentRating: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Int? = null, + @JsonProperty("tomato_meter") val tomatoMeter: Int? = null, + @JsonProperty("poster_org") val posterOrg: String? = null, + @JsonProperty("trailer_url") val trailerUrl: String? = null, + @JsonProperty("imdb_link") val imdbLink: String? = null, + @JsonProperty("box_type") val boxType: Int? = null, + @JsonProperty("recommend") val recommend: List = listOf(), + ) + + private data class MovieDataProp( + @JsonProperty("data") val data: MovieData? = MovieData() + ) + + + private data class SeriesDataProp( + @JsonProperty("code") val code: Int? = null, + @JsonProperty("msg") val msg: String? = null, + @JsonProperty("data") val data: SeriesData? = SeriesData() + ) + + private data class SeriesSeasonProp( + @JsonProperty("code") val code: Int? = null, + @JsonProperty("msg") val msg: String? = null, + @JsonProperty("data") val data: ArrayList? = arrayListOf() + ) +// data class PlayProgress ( +// +// @JsonProperty("over" ) val over : Int? = null, +// @JsonProperty("seconds" ) val seconds : Int? = null, +// @JsonProperty("mp4_id" ) val mp4Id : Int? = null, +// @JsonProperty("last_time" ) val lastTime : Int? = null +// +//) + + private data class SeriesEpisode( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("tid") val tid: Int? = null, + @JsonProperty("mb_id") val mbId: Int? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("imdb_id_status") val imdbIdStatus: Int? = null, + @JsonProperty("srt_status") val srtStatus: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("state") val state: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("thumbs") val thumbs: String? = null, + @JsonProperty("thumbs_bak") val thumbsBak: String? = null, + @JsonProperty("thumbs_original") val thumbsOriginal: String? = null, + @JsonProperty("poster_imdb") val posterImdb: Int? = null, + @JsonProperty("synopsis") val synopsis: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("view") val view: Int? = null, + @JsonProperty("download") val download: Int? = null, + @JsonProperty("source_file") val sourceFile: Int? = null, + @JsonProperty("code_file") val codeFile: Int? = null, + @JsonProperty("add_time") val addTime: Int? = null, + @JsonProperty("update_time") val updateTime: Int? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("released_timestamp") val releasedTimestamp: Long? = null, + @JsonProperty("audio_lang") val audioLang: String? = null, + @JsonProperty("quality_tag") val qualityTag: String? = null, + @JsonProperty("3d") val _3d: Int? = null, + @JsonProperty("remark") val remark: String? = null, + @JsonProperty("pending") val pending: String? = null, + @JsonProperty("imdb_rating") val imdbRating: String? = null, + @JsonProperty("display") val display: Int? = null, + @JsonProperty("sync") val sync: Int? = null, + @JsonProperty("tomato_meter") val tomatoMeter: Int? = null, + @JsonProperty("tomato_meter_count") val tomatoMeterCount: Int? = null, + @JsonProperty("tomato_audience") val tomatoAudience: Int? = null, + @JsonProperty("tomato_audience_count") val tomatoAudienceCount: Int? = null, + @JsonProperty("thumbs_min") val thumbsMin: String? = null, + @JsonProperty("thumbs_org") val thumbsOrg: String? = null, + @JsonProperty("imdb_link") val imdbLink: String? = null, +// @JsonProperty("quality_tags") val qualityTags: ArrayList = arrayListOf(), +// @JsonProperty("play_progress" ) val playProgress : PlayProgress? = PlayProgress() + + ) + + private data class SeriesLanguage( + @JsonProperty("title") val title: String? = null, + @JsonProperty("lang") val lang: String? = null + ) + + private data class SeriesData( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("mb_id") val mbId: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("display") val display: Int? = null, + @JsonProperty("state") val state: Int? = null, + @JsonProperty("vip_only") val vipOnly: Int? = null, + @JsonProperty("code_file") val codeFile: Int? = null, + @JsonProperty("director") val director: String? = null, + @JsonProperty("writer") val writer: String? = null, + @JsonProperty("actors") val actors: String? = null, + @JsonProperty("add_time") val addTime: Int? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("poster_imdb") val posterImdb: Int? = null, + @JsonProperty("banner_mini") val bannerMini: String? = null, + @JsonProperty("description") val description: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("cats") val cats: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("collect") val collect: Int? = null, + @JsonProperty("view") val view: Int? = null, + @JsonProperty("download") val download: Int? = null, + @JsonProperty("update_time") val updateTime: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("released_timestamp") val releasedTimestamp: Int? = null, + @JsonProperty("episode_released") val episodeReleased: String? = null, + @JsonProperty("episode_released_timestamp") val episodeReleasedTimestamp: Int? = null, + @JsonProperty("max_season") val maxSeason: Int? = null, + @JsonProperty("max_episode") val maxEpisode: Int? = null, + @JsonProperty("remark") val remark: String? = null, + @JsonProperty("imdb_rating") val imdbRating: String? = null, + @JsonProperty("content_rating") val contentRating: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Int? = null, + @JsonProperty("tomato_url") val tomatoUrl: String? = null, + @JsonProperty("tomato_meter") val tomatoMeter: Int? = null, + @JsonProperty("tomato_meter_count") val tomatoMeterCount: Int? = null, + @JsonProperty("tomato_meter_state") val tomatoMeterState: String? = null, + @JsonProperty("reelgood_url") val reelgoodUrl: String? = null, + @JsonProperty("audience_score") val audienceScore: Int? = null, + @JsonProperty("audience_score_count") val audienceScoreCount: Int? = null, + @JsonProperty("no_tomato_url") val noTomatoUrl: Int? = null, + @JsonProperty("order_year") val orderYear: Int? = null, + @JsonProperty("episodate_id") val episodateId: String? = null, + @JsonProperty("weights_day") val weightsDay: Double? = null, + @JsonProperty("poster_min") val posterMin: String? = null, + @JsonProperty("poster_org") val posterOrg: String? = null, + @JsonProperty("banner_mini_min") val bannerMiniMin: String? = null, + @JsonProperty("banner_mini_org") val bannerMiniOrg: String? = null, + @JsonProperty("trailer_url") val trailerUrl: String? = null, + @JsonProperty("years") val years: ArrayList = arrayListOf(), + @JsonProperty("season") val season: ArrayList = arrayListOf(), + @JsonProperty("history") val history: ArrayList = arrayListOf(), + @JsonProperty("imdb_link") val imdbLink: String? = null, + @JsonProperty("episode") val episode: ArrayList = arrayListOf(), +// @JsonProperty("is_collect") val isCollect: Int? = null, + @JsonProperty("language") val language: ArrayList = arrayListOf(), + @JsonProperty("box_type") val boxType: Int? = null, + @JsonProperty("year_year") val yearYear: String? = null, + @JsonProperty("season_episode") val seasonEpisode: String? = null + ) + + + override suspend fun load(url: String): LoadResponse { + val loadData = parseJson(url) + // val module = if(type === "TvType.Movie") "Movie_detail" else "*tv series module*" + + val isMovie = loadData.type == ResponseTypes.Movies.value + val hideNsfw = if (settingsForProvider.enableAdult) 0 else 1 + if (isMovie) { // 1 = Movie + val apiQuery = + """{"childmode":"$hideNsfw","uid":"","app_version":"$appVersion","appid":"$appIdSecond","module":"Movie_detail","channel":"Website","mid":"${loadData.id}","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","oss":"","group":""}""" + val data = (queryApiParsed(apiQuery)).data + ?: throw RuntimeException("API error") + + return newMovieLoadResponse( + data.title ?: "", + url, + TvType.Movie, + LinkData( + data.id ?: throw RuntimeException("No movie ID"), + ResponseTypes.Movies.value, + null, + null, + data.id, + data.imdbId + ), + ) { + this.recommendations = + data.recommend.mapNotNull { it.toSearchResponse(this@Superstream) } + this.posterUrl = data.posterOrg ?: data.poster + this.year = data.year + this.plot = data.description + this.tags = data.cats?.split(",")?.map { it.capitalize() } + this.rating = data.imdbRating?.split("/")?.get(0)?.toIntOrNull() + addTrailer(data.trailerUrl) + this.addImdbId(data.imdbId) + } + } else { // 2 Series + val apiQuery = + """{"childmode":"$hideNsfw","uid":"","app_version":"$appVersion","appid":"$appIdSecond","module":"TV_detail_1","display_all":"1","channel":"Website","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","tid":"${loadData.id}"}""" + val data = (queryApiParsed(apiQuery)).data + ?: throw RuntimeException("API error") + + val episodes = data.season.mapNotNull { + val seasonQuery = + """{"childmode":"$hideNsfw","app_version":"$appVersion","year":"0","appid":"$appIdSecond","module":"TV_episode","display_all":"1","channel":"Website","season":"$it","lang":"en","expired_date":"${getExpiryDate()}","platform":"android","tid":"${loadData.id}"}""" + (queryApiParsed(seasonQuery)).data + }.flatten() + + return newTvSeriesLoadResponse( + data.title ?: "", + url, + TvType.TvSeries, + episodes.mapNotNull { + Episode( + LinkData( + it.tid ?: it.id ?: return@mapNotNull null, + ResponseTypes.Series.value, + it.season, + it.episode, + data.id, + data.imdbId + ).toJson(), + it.title, + it.season, + it.episode, + it.thumbs ?: it.thumbsBak ?: it.thumbsMin ?: it.thumbsOriginal + ?: it.thumbsOrg, + it.imdbRating?.toDoubleOrNull()?.times(10)?.roundToInt(), + it.synopsis, + it.releasedTimestamp + ) + } + ) { + this.year = data.year + this.plot = data.description + this.posterUrl = data.posterOrg ?: data.poster + this.rating = data.imdbRating?.split("/")?.get(0)?.toIntOrNull() + this.tags = data.cats?.split(",")?.map { it.capitalize() } + this.addImdbId(data.imdbId) + } + } + } + + + private data class LinkData( + val id: Int, + val type: Int, + val season: Int?, + val episode: Int?, + val mediaId: Int?, + val imdbId: String?, + ) + + + data class LinkDataProp( + @JsonProperty("code") val code: Int? = null, + @JsonProperty("msg") val msg: String? = null, + @JsonProperty("data") val data: ParsedLinkData? = ParsedLinkData() + ) + + data class LinkList( + @JsonProperty("path") val path: String? = null, + @JsonProperty("quality") val quality: String? = null, + @JsonProperty("real_quality") val realQuality: String? = null, + @JsonProperty("format") val format: String? = null, + @JsonProperty("size") val size: String? = null, + @JsonProperty("size_bytes") val sizeBytes: Long? = null, + @JsonProperty("count") val count: Int? = null, + @JsonProperty("dateline") val dateline: Long? = null, + @JsonProperty("fid") val fid: Int? = null, + @JsonProperty("mmfid") val mmfid: Int? = null, + @JsonProperty("h265") val h265: Int? = null, + @JsonProperty("hdr") val hdr: Int? = null, + @JsonProperty("filename") val filename: String? = null, + @JsonProperty("original") val original: Int? = null, + @JsonProperty("colorbit") val colorbit: Int? = null, + @JsonProperty("success") val success: Int? = null, + @JsonProperty("timeout") val timeout: Int? = null, + @JsonProperty("vip_link") val vipLink: Int? = null, + @JsonProperty("fps") val fps: Int? = null, + @JsonProperty("bitstream") val bitstream: String? = null, + @JsonProperty("width") val width: Int? = null, + @JsonProperty("height") val height: Int? = null + ) + + data class ParsedLinkData( + @JsonProperty("seconds") val seconds: Int? = null, + @JsonProperty("quality") val quality: ArrayList = arrayListOf(), + @JsonProperty("list") val list: ArrayList = arrayListOf() + ) + + data class SubtitleDataProp( + @JsonProperty("code") val code: Int? = null, + @JsonProperty("msg") val msg: String? = null, + @JsonProperty("data") val data: PrivateSubtitleData? = PrivateSubtitleData() + ) + + data class Subtitles( + @JsonProperty("sid") val sid: Int? = null, + @JsonProperty("mid") val mid: String? = null, + @JsonProperty("file_path") val filePath: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("delay") val delay: Int? = null, + @JsonProperty("point") val point: String? = null, + @JsonProperty("order") val order: Int? = null, + @JsonProperty("support_total") val support_total: Int? = null, + @JsonProperty("admin_order") val adminOrder: Int? = null, + @JsonProperty("myselect") val myselect: Int? = null, + @JsonProperty("add_time") val addTime: Long? = null, + @JsonProperty("count") val count: Int? = null + ) + + data class SubtitleList( + @JsonProperty("language") val language: String? = null, + @JsonProperty("subtitles") val subtitles: ArrayList = arrayListOf() + ) + + data class PrivateSubtitleData( + @JsonProperty("select") val select: ArrayList = arrayListOf(), + @JsonProperty("list") val list: ArrayList = arrayListOf() + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val parsed = parseJson(data) + + argamap( + { + invokeVidsrcto( + parsed.imdbId, + parsed.season, + parsed.episode, + subtitleCallback + ) + }, + { + invokeExternalSource( + parsed.mediaId, + parsed.type, + parsed.season, + parsed.episode, + callback + ) + }, + { + invokeInternalSource( + parsed.id, + parsed.type, + parsed.season, + parsed.episode, + subtitleCallback, + callback + ) + }, + { + invokeOpenSubs( + parsed.imdbId, + parsed.season, + parsed.episode, + subtitleCallback + ) + }, + { + invokeWatchsomuch( + parsed.imdbId, + parsed.season, + parsed.episode, + subtitleCallback + ) + } + ) + + return true + } + + data class ExternalResponse( + @JsonProperty("data") val data: Data? = null, + ) { + data class Data( + @JsonProperty("link") val link: String? = null, + @JsonProperty("file_list") val file_list: ArrayList? = arrayListOf(), + ) { + data class FileList( + @JsonProperty("fid") val fid: Long? = null, + @JsonProperty("file_name") val file_name: String? = null, + @JsonProperty("oss_fid") val oss_fid: Long? = null, + ) + } + } + + data class WatchsomuchTorrents( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("movieId") val movieId: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + ) + + data class WatchsomuchMovies( + @JsonProperty("torrents") val torrents: ArrayList? = arrayListOf(), + ) + + data class WatchsomuchResponses( + @JsonProperty("movie") val movie: WatchsomuchMovies? = null, + ) + + data class WatchsomuchSubtitles( + @JsonProperty("url") val url: String? = null, + @JsonProperty("label") val label: String? = null, + ) + + data class WatchsomuchSubResponses( + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), + ) + + data class OsSubtitles( + @JsonProperty("url") val url: String? = null, + @JsonProperty("lang") val lang: String? = null, + ) + + data class OsResult( + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), + ) + + data class VidsrcSubtitles( + @JsonProperty("label") val label: String? = null, + @JsonProperty("file") val file: String? = null, + ) + +} + diff --git a/Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt b/Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt new file mode 100644 index 00000000..099210fe --- /dev/null +++ b/Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt @@ -0,0 +1,14 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class SuperstreamPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Superstream()) + } +} \ No newline at end of file