mirror of
https://github.com/hexated/cloudstream-extensions-hexated.git
synced 2024-08-15 00:03:22 +00:00
move superstream
This commit is contained in:
parent
1a341e6cf1
commit
0465f91c71
12 changed files with 1135 additions and 37 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -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: |
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -65,7 +65,7 @@ class StremioC : MainAPI() {
|
|||
val loadData = parseJson<LoadData>(data)
|
||||
val request = app.get(
|
||||
"${mainUrl}/stream/${loadData.type}/${loadData.id}.json",
|
||||
interceptor = interceptor
|
||||
timeout = 120L
|
||||
)
|
||||
if (request.isSuccessful) {
|
||||
val res = request.parsedSafe<StreamsResponse>()
|
||||
|
@ -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<StreamsResponse>()
|
||||
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<CatalogResponse>()
|
||||
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<CatalogResponse>()
|
||||
res?.metas?.forEach { entry ->
|
||||
entries.add(entry.toSearchResponse(provider))
|
||||
|
|
|
@ -238,7 +238,7 @@ class StremioX : TmdbProvider() {
|
|||
} else {
|
||||
"$fixMainUrl/stream/series/$imdbId:$season:$episode.json"
|
||||
}
|
||||
val res = app.get(url, interceptor = interceptor).parsedSafe<StreamsResponse>()
|
||||
val res = app.get(url, timeout = 120L).parsedSafe<StreamsResponse>()
|
||||
res?.streams?.forEach { stream ->
|
||||
stream.runCallback(subtitleCallback, callback)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ object SubsExtractors {
|
|||
} else {
|
||||
"series/$imdbId:$season:$episode"
|
||||
}
|
||||
app.get("${openSubAPI}/subtitles/$slug.json", interceptor = interceptor).parsedSafe<OsResult>()?.subtitles?.map { sub ->
|
||||
app.get("${openSubAPI}/subtitles/$slug.json", timeout = 120L).parsedSafe<OsResult>()?.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")
|
||||
|
|
|
@ -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://")
|
||||
}
|
||||
|
|
42
Superstream/build.gradle.kts
Normal file
42
Superstream/build.gradle.kts
Normal file
|
@ -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"
|
||||
}
|
2
Superstream/src/main/AndroidManifest.xml
Normal file
2
Superstream/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.hexated"/>
|
242
Superstream/src/main/kotlin/com/hexated/Extractors.kt
Normal file
242
Superstream/src/main/kotlin/com/hexated/Extractors.kt
Normal file
|
@ -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<LinkDataProp>(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<SubtitleDataProp>(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<ExternalResponse>()?.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<ExternalResponse>()?.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<ExternalResponse>()?.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<WatchsomuchResponses>()?.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<WatchsomuchSubResponses>()?.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<OsResult>()?.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<List<VidsrcSubtitles>>(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<String, String> {
|
||||
return if (season == null && episode == null) {
|
||||
"" to ""
|
||||
} else {
|
||||
(if (season!! < 10) "0$season" else "$season") to (if (episode!! < 10) "0$episode" else "$episode")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
818
Superstream/src/main/kotlin/com/hexated/Superstream.kt
Normal file
818
Superstream/src/main/kotlin/com/hexated/Superstream.kt
Normal file
|
@ -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 <reified T : Any> 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<PostJSON> = arrayListOf(),
|
||||
)
|
||||
|
||||
private data class DataJSON(
|
||||
@JsonProperty("data") val data: ArrayList<ListJSON> = 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<DataJSON>(
|
||||
"""{"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<Data> = arrayListOf()
|
||||
)
|
||||
|
||||
private data class MainData(
|
||||
@JsonProperty("data") val data: MainDataList
|
||||
)
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
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<MainData>(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<Data> = 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<SeriesEpisode>? = 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<String> = 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<Int> = arrayListOf(),
|
||||
@JsonProperty("season") val season: ArrayList<Int> = arrayListOf(),
|
||||
@JsonProperty("history") val history: ArrayList<String> = arrayListOf(),
|
||||
@JsonProperty("imdb_link") val imdbLink: String? = null,
|
||||
@JsonProperty("episode") val episode: ArrayList<SeriesEpisode> = arrayListOf(),
|
||||
// @JsonProperty("is_collect") val isCollect: Int? = null,
|
||||
@JsonProperty("language") val language: ArrayList<SeriesLanguage> = 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<LoadData>(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<MovieDataProp>(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<SeriesDataProp>(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<SeriesSeasonProp>(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<String> = arrayListOf(),
|
||||
@JsonProperty("list") val list: ArrayList<LinkList> = 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<Subtitles> = arrayListOf()
|
||||
)
|
||||
|
||||
data class PrivateSubtitleData(
|
||||
@JsonProperty("select") val select: ArrayList<String> = arrayListOf(),
|
||||
@JsonProperty("list") val list: ArrayList<SubtitleList> = arrayListOf()
|
||||
)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
|
||||
val parsed = parseJson<LinkData>(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<FileList>? = 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<WatchsomuchTorrents>? = 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<WatchsomuchSubtitles>? = arrayListOf(),
|
||||
)
|
||||
|
||||
data class OsSubtitles(
|
||||
@JsonProperty("url") val url: String? = null,
|
||||
@JsonProperty("lang") val lang: String? = null,
|
||||
)
|
||||
|
||||
data class OsResult(
|
||||
@JsonProperty("subtitles") val subtitles: ArrayList<OsSubtitles>? = arrayListOf(),
|
||||
)
|
||||
|
||||
data class VidsrcSubtitles(
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
)
|
||||
|
||||
}
|
||||
|
14
Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt
Normal file
14
Superstream/src/main/kotlin/com/hexated/SuperstreamPlugin.kt
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue