diff --git a/StremioProvider/build.gradle.kts b/StremioProvider/build.gradle.kts index 0a0b6c7..29c0e52 100644 --- a/StremioProvider/build.gradle.kts +++ b/StremioProvider/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 2 +version = 3 cloudstream { @@ -21,4 +21,4 @@ cloudstream { tvTypes = listOf("Others") iconUrl = "https://www.google.com/s2/favicons?domain=www.stremio.com&sz=%size%" -} \ No newline at end of file +} diff --git a/StremioProvider/src/main/kotlin/com/lagradost/StremioProvider.kt b/StremioProvider/src/main/kotlin/com/lagradost/StremioProvider.kt index c47d8b3..1c7824c 100644 --- a/StremioProvider/src/main/kotlin/com/lagradost/StremioProvider.kt +++ b/StremioProvider/src/main/kotlin/com/lagradost/StremioProvider.kt @@ -1,26 +1,34 @@ package com.lagradost import android.util.Log -import com.lagradost.StremioProvider.Companion.encodeUri import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson - import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.loadExtractor import org.json.JSONObject +import java.net.URI import java.net.URLEncoder -private const val TRACKER_LIST_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" +private const val TRACKER_LIST_URL = + "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" class StremioProvider : MainAPI() { override var mainUrl = "https://stremio.github.io/stremio-static-addon-example" + + /** add this as default stream because we don't support torrent yet + and this add ons is the best for http stream */ + private var streamUrl = "https://2ecbbd610840-cinestream.baby-beamup.club" + override var name = "Stremio example" override val supportedTypes = setOf(TvType.Others) override val hasMainPage = true + // check if id is imdb/tmdb cause stremio addons like torrentio works base on imdbId + private fun isImdborTmdb(url: String?): Boolean { + return imdbUrlToIdNullable(url) != null || url?.startsWith("tmdb:") == true + } + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse? { val res = tryParseJson(app.get("${mainUrl}/manifest.json").text) ?: return null val lists = mutableListOf() @@ -45,8 +53,10 @@ class StremioProvider : MainAPI() { } override suspend fun load(url: String): LoadResponse? { - val res = tryParseJson(url) ?: throw RuntimeException(url) - return res.toLoadResponse(this) + val res = parseJson(url) + val json = app.get("${mainUrl}/meta/${res.type}/${res.id}.json").parsedSafe()?.meta ?: throw RuntimeException(url) + isMetaId = isImdborTmdb(res.id) + return json.toLoadResponse(this) } override suspend fun loadLinks( @@ -77,8 +87,11 @@ class StremioProvider : MainAPI() { suspend fun search(query: String, provider: StremioProvider): List { val entries = mutableListOf() types.forEach { type -> - val res = tryParseJson(app.get("${provider.mainUrl}/catalog/${type.encodeUri()}/${id.encodeUri()}/search=${query.encodeUri()}.json").text) ?: return@forEach - res.metas.forEach { entry -> + val json = app.get("${provider.mainUrl}/catalog/${type}/${id}/search=${query}.json").text + val res = + tryParseJson(json) + ?: return@forEach + res.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) } } @@ -88,8 +101,11 @@ class StremioProvider : MainAPI() { suspend fun toHomePageList(provider: StremioProvider): HomePageList? { val entries = mutableListOf() types.forEach { type -> - val res = tryParseJson(app.get("${provider.mainUrl}/catalog/${type.encodeUri()}/${id.encodeUri()}.json").text) ?: return@forEach - res.metas.forEach { entry -> + val json = app.get("${provider.mainUrl}/catalog/${type}/${id}.json").text + val res = + tryParseJson(json) + ?: return@forEach + res.metas?.forEach { entry -> entries.add(entry.toSearchResponse(provider)) } } @@ -100,7 +116,7 @@ class StremioProvider : MainAPI() { } } - private data class CatalogResponse(val metas: List) + private data class CatalogResponse(val metas: List?,val meta: CatalogEntry?) private data class CatalogEntry( val name: String, val id: String, @@ -111,20 +127,21 @@ class StremioProvider : MainAPI() { ) { fun toSearchResponse(provider: StremioProvider): SearchResponse { return provider.newMovieSearchResponse( - name, + fixTitle(name), this.toJson(), TvType.Others ) { posterUrl = poster } } + suspend fun toLoadResponse(provider: StremioProvider): LoadResponse { if (videos == null || videos.isEmpty()) { return provider.newMovieLoadResponse( name, - "${provider.mainUrl}/meta/${type?.encodeUri()}/${id.encodeUri()}.json", + "${provider.mainUrl}/meta/${type}/${id}.json", TvType.Others, - "${provider.mainUrl}/stream/${type?.encodeUri()}/${id.encodeUri()}.json" + if(isMetaId) "${provider.streamUrl}/stream/${type}/${id}.json" else "${provider.mainUrl}/stream/${type}/${id}.json" ) { posterUrl = poster plot = description @@ -132,7 +149,7 @@ class StremioProvider : MainAPI() { } else { return provider.newTvSeriesLoadResponse( name, - "${provider.mainUrl}/meta/${type?.encodeUri()}/${id.encodeUri()}.json", + "${provider.mainUrl}/meta/${type}/${id}.json", TvType.Others, videos.map { it.toEpisode(provider, type) @@ -146,60 +163,75 @@ class StremioProvider : MainAPI() { } } - private data class Video(val id: String, val title: String?, val thumbnail: String?, val overview: String?) { + private data class Video( + val id: String, + val title: String?, + val thumbnail: String?, + val overview: String? + ) { fun toEpisode(provider: StremioProvider, type: String?): Episode { + val ids = id.split(":") return provider.newEpisode( - "${provider.mainUrl}/stream/${type?.encodeUri()}/${id.encodeUri()}.json" + if(isMetaId) "${provider.streamUrl}/stream/${type}/${id}.json" else "${provider.mainUrl}/stream/${type}/${id}.json" ) { this.name = title this.posterUrl = thumbnail this.description = overview + this.episode = ids.lastOrNull()?.toIntOrNull() + this.season = ids.getOrNull(ids.lastIndex - 1)?.toIntOrNull() } } } private data class StreamsResponse(val streams: List) + private data class Subtitle( + val url: String?, + val lang: String?, + val id: String?, + ) private data class Stream( val name: String?, val title: String?, val url: String?, + val description: String?, val ytId: String?, val externalUrl: String?, val behaviorHints: JSONObject?, val infoHash: String?, - val sources: List = emptyList() + val sources: List = emptyList(), + val subtitles: List = emptyList() ) { - suspend fun runCallback(subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + suspend fun runCallback( + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { if (url != null) { var referer: String? = null try { val headers = ((behaviorHints?.get("proxyHeaders") as? JSONObject) ?.get("request") as? JSONObject) - referer = headers?.get("referer") as? String ?: headers?.get("origin") as? String + referer = + headers?.get("referer") as? String ?: headers?.get("origin") as? String } catch (ex: Throwable) { Log.e("Stremio", Log.getStackTraceString(ex)) } - - if (url.endsWith(".m3u8")) { - callback.invoke( - ExtractorLink( + callback.invoke( + ExtractorLink( name ?: "", title ?: name ?: "", url, - referer ?: "", - Qualities.Unknown.value, - isM3u8 = true - )) - } else { - callback.invoke( - ExtractorLink( - name ?: "", - title ?: name ?: "", - url, - referer ?: "", - Qualities.Unknown.value, - isM3u8 = false - )) + referer ?: "", + getQualityFromName(description), + isM3u8 = URI(url).path.endsWith(".m3u8") + ) + ) + subtitles.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang ?: "", + sub.url ?: return@map + ) + ) } } if (ytId != null) { @@ -212,17 +244,13 @@ class StremioProvider : MainAPI() { val resp = app.get(TRACKER_LIST_URL).text val otherTrackers = resp .split("\n") - .filterIndexed{i, s -> i%2==0} - .filter{s -> !s.isNullOrEmpty()} - .map{it -> "&tr=$it"} - .joinToString("") - + .filterIndexed { i, s -> i % 2 == 0 } + .filter { s -> s.isNotEmpty() }.joinToString("") { "&tr=$it" } + val sourceTrackers = sources - .filter{it->it.startsWith("tracker:")} - .map{it->it.removePrefix("tracker:")} - .filter{s -> !s.isNullOrEmpty()} - .map{it -> "&tr=$it"} - .joinToString("") + .filter { it -> it.startsWith("tracker:") } + .map { it -> it.removePrefix("tracker:") } + .filter { s -> s.isNotEmpty() }.joinToString("") { "&tr=$it" } val magnet = "magnet:?xt=urn:btih:${infoHash}${sourceTrackers}${otherTrackers}" callback.invoke( @@ -239,6 +267,7 @@ class StremioProvider : MainAPI() { } companion object { - fun String.encodeUri() = URLEncoder.encode(this, "utf8") + private var isMetaId = false + fun String.encodeUri(): String = URLEncoder.encode(this, "utf8") } }