diff --git a/SoraStream/Icon.png b/SoraStream/Icon.png new file mode 100644 index 00000000..3b3a2250 Binary files /dev/null and b/SoraStream/Icon.png differ diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts new file mode 100644 index 00000000..55c39336 --- /dev/null +++ b/SoraStream/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated", "Sora") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "TvSeries", + "Anime", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=kisskh.me&sz=%size%" +} \ No newline at end of file diff --git a/SoraStream/src/main/AndroidManifest.xml b/SoraStream/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/SoraStream/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt new file mode 100644 index 00000000..c3341e1b --- /dev/null +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -0,0 +1,338 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.metaproviders.TmdbProvider +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import kotlin.math.roundToInt + +class SoraStream : TmdbProvider() { + override var name = "SoraStream" + override val hasMainPage = true + override val hasDownloadSupport = true + override val instantLinkLoading = true + override val useMetaLoadResponse = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + /** AUTHOR : Hexated & Sora */ + companion object { + private const val tmdbAPI = "https://api.themoviedb.org/3" + private const val apiKey = "b030404650f279792a8d3287232358e3" // PLEASE DON'T STEAL + private var mainAPI = base64Decode("aHR0cHM6Ly94cHdhdGNoLnZlcmNlbC5hcHA=") + private var mainServerAPI = base64Decode("aHR0cHM6Ly9zb3JhLW1vdmllLnZlcmNlbC5hcHA=") + + fun getType(t: String?): TvType { + return when (t) { + "movie" -> TvType.Movie + else -> TvType.TvSeries + } + } + + fun getActorRole(t: String?): ActorRole { + return when (t) { + "Acting" -> ActorRole.Main + else -> ActorRole.Background + } + } + + + fun getStatus(t: String?): ShowStatus { + return when (t) { + "Returning Series" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$tmdbAPI/movie/popular?api_key=$apiKey®ion=&page=" to "Popular Movies", + "$tmdbAPI/tv/popular?api_key=$apiKey®ion=&page=" to "Popular TV Shows", + "$tmdbAPI/movie/top_rated?api_key=$apiKey®ion=&page=" to "Top Rated Movies", + "$tmdbAPI/tv/top_rated?api_key=$apiKey®ion=&page=" to "Top Rated TV Shows", +// "$tmdbAPI/discover/tv?api_key=$apiKey&with_keywords=210024|222243&page=" to "Anime", +// "$tmdbAPI/discover/movie?api_key=$apiKey&with_keywords=210024|222243&page=" to "Anime Movies", + ) + + 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 + } + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val type = if (request.data.contains("/movie")) "movie" else "tv" + val home = app.get(request.data + 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 search(query: String): List { + return app.get( + "$tmdbAPI/search/multi?api_key=$apiKey&language=en-US&query=$query&page=1&include_adult=false", + referer = "$mainAPI/" + ).parsedSafe()?.results?.mapNotNull { media -> + media.toSearchResponse() + } ?: throw ErrorLoadingException("Invalid Json reponse") + } + + override suspend fun load(url: String): LoadResponse? { + val data = parseJson(url) + val buildId = + app.get("$mainAPI/").text.substringAfterLast("\"buildId\":\"").substringBefore("\",") + val responses = + app.get("$mainAPI/_next/data/$buildId/${data.type}/${data.id}.json?id=${data.id}") + .parsedSafe()?.pageProps + ?: throw ErrorLoadingException("Invalid Json Response") + val res = responses.result ?: return null + val type = getType(data.type) + val actors = responses.cast?.mapNotNull { cast -> + ActorData( + Actor( + cast.name ?: cast.originalName ?: return@mapNotNull null, + getImageUrl(cast.profilePath) + ), + getActorRole(cast.knownForDepartment) + ) + } ?: return null + val recommendations = + responses.recommandations?.mapNotNull { media -> media.toSearchResponse() } + + return if (type == TvType.TvSeries) { + val episodes = mutableListOf() + res.seasons?.apmap { season -> + app.get("$tmdbAPI/${data.type}/${data.id}/season/${season.seasonNumber}?api_key=$apiKey") + .parsedSafe()?.episodes?.map { eps -> + episodes.add(Episode( + LinkData( + data.id, + responses.imdbId, + data.type, + eps.seasonNumber, + eps.episodeNumber + ).toJson(), + name = eps.name, + season = eps.seasonNumber, + episode = eps.episodeNumber, + posterUrl = getImageUrl(eps.stillPath), + rating = eps.voteAverage?.times(10)?.roundToInt(), + description = eps.overview + ).apply { + this.addDate(eps.airDate) + }) + } + } + newTvSeriesLoadResponse( + res.title ?: res.name ?: res.originalTitle ?: res.originalName ?: return null, + url, + TvType.TvSeries, + episodes + ) { + this.posterUrl = getImageUrl(res.posterPath) + this.year = + (res.releaseDate ?: res.firstAirDate)?.split("-")?.first()?.toIntOrNull() + this.plot = res.overview + this.tags = res.genres?.mapNotNull { it.name } + this.showStatus = getStatus(res.status) + this.recommendations = recommendations + this.actors = actors + } + } else { + newMovieLoadResponse( + res.title ?: res.name ?: res.originalTitle ?: res.originalName ?: return null, + url, + TvType.Movie, + LinkData( + data.id, + responses.imdbId, + data.type, + ).toJson(), + ) { + this.posterUrl = getImageUrl(res.posterPath) + this.year = + (res.releaseDate ?: res.firstAirDate)?.split("-")?.first()?.toIntOrNull() + this.plot = res.overview + this.tags = res.genres?.mapNotNull { it.name } + this.recommendations = recommendations + this.actors = actors + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val res = parseJson(data) + val query = if (res.type == "tv") { + "$mainServerAPI/tv-shows/${res.id}/season/${res.season}/episode/${res.episode}?_data=routes/tv-shows/\$tvId.season.\$seasonId.episode.\$episodeId" + } else { + "$mainServerAPI/movies/${res.id}/watch?_data=routes/movies/\$movieId.watch" + } + val referer = if (res.type == "tv") { + "$mainServerAPI/tv-shows/${res.id}/season/${res.season}/episode/${res.episode}" + } else { + "$mainServerAPI/movies/${res.id}/watch" + } + + val json = app.get( + query, + referer = referer + ).parsedSafe() + + json?.sources?.map { source -> + callback.invoke( + ExtractorLink( + this.name, + this.name, + source.url ?: return@map null, + "$mainServerAPI/", + source.quality?.toIntOrNull() ?: Qualities.Unknown.value, + isM3u8 = source.isM3U8, + headers = mapOf("Origin" to mainServerAPI) + ) + ) + } + + json?.subtitles?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + sub.lang.toString(), + sub.url ?: return@map null + ) + ) + } + + return true + } + + private data class LinkData( + val id: Int? = null, + val imdbId: String? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + ) + + data class Data( + val id: Int? = null, + val type: String? = null, + ) + + data class Subtitles( + @JsonProperty("url") val url: String? = null, + @JsonProperty("lang") val lang: String? = null, + ) + + data class Sources( + @JsonProperty("url") val url: String? = null, + @JsonProperty("quality") val quality: String? = null, + @JsonProperty("isM3U8") val isM3U8: Boolean = true, + ) + + data class LoadLinks( + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), + ) + + 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 Seasons( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("season_number") val seasonNumber: Int? = 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 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("release_date") val releaseDate: String? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("genres") val genres: ArrayList? = arrayListOf(), + @JsonProperty("seasons") val seasons: ArrayList? = arrayListOf(), + ) + + data class PageProps( + @JsonProperty("id") val id: String? = null, + @JsonProperty("imdb") val imdbId: String? = null, + @JsonProperty("result") val result: MediaDetail? = null, + @JsonProperty("recommandations") val recommandations: ArrayList? = arrayListOf(), + @JsonProperty("cast") val cast: ArrayList? = arrayListOf(), + ) + + data class Detail( + @JsonProperty("pageProps") val pageProps: PageProps? = null, + ) + +} diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamPlugin.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamPlugin.kt new file mode 100644 index 00000000..bdf4216e --- /dev/null +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamPlugin.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 SoraStreamPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(SoraStream()) + } +} \ No newline at end of file