package com.hexated import com.fasterxml.jackson.annotation.JsonProperty import com.hexated.SubsExtractors.invokeOpenSubs import com.hexated.SubsExtractors.invokeWatchsomuch import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import java.util.ArrayList import kotlin.math.roundToInt import com.lagradost.cloudstream3.metaproviders.TmdbProvider class StremioX : TmdbProvider() { override var mainUrl = "https://torrentio.strem.fun" override var name = "StremioX" override val hasMainPage = true override val hasQuickSearch = true override val supportedTypes = setOf(TvType.Others) companion object { const val TRACKER_LIST_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" private const val tmdbAPI = "https://api.themoviedb.org/3" private const val apiKey = BuildConfig.TMDB_API fun getType(t: String?): TvType { return when (t) { "movie" -> TvType.Movie else -> TvType.TvSeries } } fun getStatus(t: String?): ShowStatus { return when (t) { "Returning Series" -> ShowStatus.Ongoing else -> ShowStatus.Completed } } } override val mainPage = mainPageOf( "$tmdbAPI/trending/all/day?api_key=$apiKey®ion=US" to "Trending", "$tmdbAPI/movie/popular?api_key=$apiKey®ion=US" to "Popular Movies", "$tmdbAPI/tv/popular?api_key=$apiKey®ion=US&with_original_language=en" to "Popular TV Shows", "$tmdbAPI/tv/airing_today?api_key=$apiKey®ion=US&with_original_language=en" to "Airing Today TV Shows", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=213" to "Netflix", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=1024" to "Amazon", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=2739" to "Disney+", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=453" to "Hulu", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=2552" to "Apple TV+", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=49" to "HBO", "$tmdbAPI/discover/tv?api_key=$apiKey&with_networks=4330" to "Paramount+", "$tmdbAPI/movie/top_rated?api_key=$apiKey®ion=US" to "Top Rated Movies", "$tmdbAPI/tv/top_rated?api_key=$apiKey®ion=US" to "Top Rated TV Shows", "$tmdbAPI/movie/upcoming?api_key=$apiKey®ion=US" to "Upcoming Movies", "$tmdbAPI/discover/tv?api_key=$apiKey&with_original_language=ko" to "Korean Shows", ) private fun getImageUrl(link: String?): String? { if (link == null) return null return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link } private fun getOriImageUrl(link: String?): String? { if (link == null) return null return if (link.startsWith("/")) "https://image.tmdb.org/t/p/original/$link" else link } override suspend fun getMainPage( page: Int, request: MainPageRequest ): HomePageResponse { val adultQuery = if (settingsForProvider.enableAdult) "" else "&without_keywords=190370|13059|226161|195669|190370" val type = if (request.data.contains("/movie")) "movie" else "tv" val home = app.get("${request.data}$adultQuery&page=$page") .parsedSafe()?.results?.mapNotNull { media -> media.toSearchResponse(type) } ?: throw ErrorLoadingException("Invalid Json reponse") return newHomePageResponse(request.name, home) } private fun Media.toSearchResponse(type: String? = null): SearchResponse? { return newMovieSearchResponse( title ?: name ?: originalTitle ?: return null, Data(id = id, type = mediaType ?: type).toJson(), TvType.Movie, ) { this.posterUrl = getImageUrl(posterPath) } } override suspend fun quickSearch(query: String): List? = search(query) override suspend fun search(query: String): List? { return app.get( "$tmdbAPI/search/multi?api_key=$apiKey&language=en-US&query=$query&page=1&include_adult=${settingsForProvider.enableAdult}" ).parsedSafe()?.results?.mapNotNull { media -> media.toSearchResponse() } } override suspend fun load(url: String): LoadResponse? { val data = parseJson(url) val type = getType(data.type) val resUrl = if (type == TvType.Movie) { "$tmdbAPI/movie/${data.id}?api_key=$apiKey&append_to_response=keywords,credits,external_ids,videos,recommendations" } else { "$tmdbAPI/tv/${data.id}?api_key=$apiKey&append_to_response=keywords,credits,external_ids,videos,recommendations" } val res = app.get(resUrl).parsedSafe() ?: throw ErrorLoadingException("Invalid Json Response") val title = res.title ?: res.name ?: return null val poster = getOriImageUrl(res.posterPath) val bgPoster = getOriImageUrl(res.backdropPath) val releaseDate = res.releaseDate ?: res.firstAirDate val year = releaseDate?.split("-")?.first()?.toIntOrNull() val rating = res.vote_average.toString().toRatingInt() val genres = res.genres?.mapNotNull { it.name } val isAnime = genres?.contains("Animation") == true && (res.original_language == "zh" || res.original_language == "ja") val keywords = res.keywords?.results?.mapNotNull { it.name }.orEmpty() .ifEmpty { res.keywords?.keywords?.mapNotNull { it.name } } val actors = res.credits?.cast?.mapNotNull { cast -> ActorData( Actor( cast.name ?: cast.originalName ?: return@mapNotNull null, getImageUrl(cast.profilePath) ), roleString = cast.character ) } ?: return null val recommendations = res.recommendations?.results?.mapNotNull { media -> media.toSearchResponse() } val trailer = res.videos?.results?.map { "https://www.youtube.com/watch?v=${it.key}" }?.randomOrNull() return if (type == TvType.TvSeries) { val episodes = res.seasons?.mapNotNull { season -> app.get("$tmdbAPI/${data.type}/${data.id}/season/${season.seasonNumber}?api_key=$apiKey") .parsedSafe()?.episodes?.map { eps -> Episode( LoadData( res.external_ids?.imdb_id, eps.seasonNumber, eps.episodeNumber ).toJson(), name = eps.name + if (isUpcoming(eps.airDate)) " • [UPCOMING]" else "", season = eps.seasonNumber, episode = eps.episodeNumber, posterUrl = getImageUrl(eps.stillPath), rating = eps.voteAverage?.times(10)?.roundToInt(), description = eps.overview ).apply { this.addDate(eps.airDate) } } }?.flatten() ?: listOf() newTvSeriesLoadResponse( title, url, if (isAnime) TvType.Anime else TvType.TvSeries, episodes ) { this.posterUrl = poster this.backgroundPosterUrl = bgPoster this.year = year this.plot = res.overview this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres this.rating = rating this.showStatus = getStatus(res.status) this.recommendations = recommendations this.actors = actors this.contentRating = fetchContentRating(data.id, "US") addTrailer(trailer) addTMDbId(data.id.toString()) addImdbId(res.external_ids?.imdb_id) } } else { newMovieLoadResponse( title, url, TvType.Movie, LoadData(res.external_ids?.imdb_id).toJson() ) { this.posterUrl = poster this.comingSoon = isUpcoming(releaseDate) this.backgroundPosterUrl = bgPoster this.year = year this.plot = res.overview this.duration = res.runtime this.tags = keywords.takeIf { !it.isNullOrEmpty() } ?: genres this.rating = rating this.recommendations = recommendations this.actors = actors this.contentRating = fetchContentRating(data.id, "US") addTrailer(trailer) addTMDbId(data.id.toString()) addImdbId(res.external_ids?.imdb_id) } } } override suspend fun loadLinks( data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ): Boolean { val res = parseJson(data) argamap( { invokeMainSource(res.imdbId, res.season, res.episode, subtitleCallback, callback) }, { invokeWatchsomuch(res.imdbId, res.season, res.episode, subtitleCallback) }, { invokeOpenSubs(res.imdbId, res.season, res.episode, subtitleCallback) }, ) return true } private suspend fun invokeMainSource( imdbId: String? = null, season: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { val fixMainUrl = mainUrl.fixSourceUrl() val url = if (season == null) { "$fixMainUrl/stream/movie/$imdbId.json" } else { "$fixMainUrl/stream/series/$imdbId:$season:$episode.json" } val res = app.get(url, timeout = 120L).parsedSafe() res?.streams?.forEach { stream -> stream.runCallback(subtitleCallback, callback) } } private data class StreamsResponse(val streams: List) private data class Subtitle( val url: String?, val lang: String?, val id: String?, ) private data class ProxyHeaders( val request: Map?, ) private data class BehaviorHints( val proxyHeaders: ProxyHeaders?, val headers: Map?, ) private data class Stream( val name: String?, val title: String?, val url: String?, val description: String?, val ytId: String?, val externalUrl: String?, val behaviorHints: BehaviorHints?, val infoHash: String?, val sources: List = emptyList(), val subtitles: List = emptyList() ) { suspend fun runCallback( subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { if (url != null) { callback.invoke( ExtractorLink( name ?: "", fixSourceName(name, title), url, "", getQuality(listOf(description,title,name)), headers = behaviorHints?.proxyHeaders?.request ?: behaviorHints?.headers ?: mapOf(), type = INFER_TYPE ) ) subtitles.map { sub -> subtitleCallback.invoke( SubtitleFile( SubtitleHelper.fromThreeLettersToLanguage(sub.lang ?: "") ?: sub.lang ?: "", sub.url ?: return@map ) ) } } if (ytId != null) { loadExtractor("https://www.youtube.com/watch?v=$ytId", subtitleCallback, callback) } if (externalUrl != null) { loadExtractor(externalUrl, subtitleCallback, callback) } if (infoHash != null) { val resp = app.get(TRACKER_LIST_URL).text val otherTrackers = resp .split("\n") .filterIndexed { i, _ -> i % 2 == 0 } .filter { s -> s.isNotEmpty() }.joinToString("") { "&tr=$it" } val sourceTrackers = sources .filter { it.startsWith("tracker:") } .map { it.removePrefix("tracker:") } .filter { s -> s.isNotEmpty() }.joinToString("") { "&tr=$it" } val magnet = "magnet:?xt=urn:btih:${infoHash}${sourceTrackers}${otherTrackers}" callback.invoke( ExtractorLink( name ?: "", title ?: name ?: "", magnet, "", Qualities.Unknown.value ) ) } } } data class LoadData( val imdbId: String? = null, val season: Int? = null, val episode: Int? = null, ) data class Data( val id: Int? = null, val type: String? = null, val aniId: String? = null, val malId: Int? = null, ) data class Results( @JsonProperty("results") val results: ArrayList? = arrayListOf(), ) data class Media( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, @JsonProperty("title") val title: String? = null, @JsonProperty("original_title") val originalTitle: String? = null, @JsonProperty("media_type") val mediaType: String? = null, @JsonProperty("poster_path") val posterPath: String? = null, ) data class Genres( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, ) data class Keywords( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, ) data class KeywordResults( @JsonProperty("results") val results: ArrayList? = arrayListOf(), @JsonProperty("keywords") val keywords: ArrayList? = arrayListOf(), ) data class Seasons( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, @JsonProperty("season_number") val seasonNumber: Int? = null, @JsonProperty("air_date") val airDate: String? = null, ) data class Cast( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, @JsonProperty("original_name") val originalName: String? = null, @JsonProperty("character") val character: String? = null, @JsonProperty("known_for_department") val knownForDepartment: String? = null, @JsonProperty("profile_path") val profilePath: String? = null, ) data class Episodes( @JsonProperty("id") val id: Int? = null, @JsonProperty("name") val name: String? = null, @JsonProperty("overview") val overview: String? = null, @JsonProperty("air_date") val airDate: String? = null, @JsonProperty("still_path") val stillPath: String? = null, @JsonProperty("vote_average") val voteAverage: Double? = null, @JsonProperty("episode_number") val episodeNumber: Int? = null, @JsonProperty("season_number") val seasonNumber: Int? = null, ) data class MediaDetailEpisodes( @JsonProperty("episodes") val episodes: ArrayList? = arrayListOf(), ) data class Trailers( @JsonProperty("key") val key: String? = null, ) data class ResultsTrailer( @JsonProperty("results") val results: ArrayList? = arrayListOf(), ) data class ExternalIds( @JsonProperty("imdb_id") val imdb_id: String? = null, @JsonProperty("tvdb_id") val tvdb_id: String? = null, ) data class Credits( @JsonProperty("cast") val cast: ArrayList? = arrayListOf(), ) data class ResultsRecommendations( @JsonProperty("results") val results: ArrayList? = arrayListOf(), ) data class LastEpisodeToAir( @JsonProperty("episode_number") val episode_number: Int? = null, @JsonProperty("season_number") val season_number: Int? = null, ) data class MediaDetail( @JsonProperty("id") val id: Int? = null, @JsonProperty("imdb_id") val imdbId: String? = null, @JsonProperty("title") val title: String? = null, @JsonProperty("name") val name: String? = null, @JsonProperty("original_title") val originalTitle: String? = null, @JsonProperty("original_name") val originalName: String? = null, @JsonProperty("poster_path") val posterPath: String? = null, @JsonProperty("backdrop_path") val backdropPath: String? = null, @JsonProperty("release_date") val releaseDate: String? = null, @JsonProperty("first_air_date") val firstAirDate: String? = null, @JsonProperty("overview") val overview: String? = null, @JsonProperty("runtime") val runtime: Int? = null, @JsonProperty("vote_average") val vote_average: Any? = null, @JsonProperty("original_language") val original_language: String? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("genres") val genres: ArrayList? = arrayListOf(), @JsonProperty("keywords") val keywords: KeywordResults? = null, @JsonProperty("last_episode_to_air") val last_episode_to_air: LastEpisodeToAir? = null, @JsonProperty("seasons") val seasons: ArrayList? = arrayListOf(), @JsonProperty("videos") val videos: ResultsTrailer? = null, @JsonProperty("external_ids") val external_ids: ExternalIds? = null, @JsonProperty("credits") val credits: Credits? = null, @JsonProperty("recommendations") val recommendations: ResultsRecommendations? = null, ) }