diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index 5df4d746..d20c4ca2 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 96 +version = 97 cloudstream { diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 1249e5f5..2a564c55 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -1573,6 +1573,65 @@ object SoraExtractor : SoraStream() { } } + suspend fun invokeKickassanime( + title: String? = null, + epsTitle: String? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val body = """{"query":"$title"}""".toRequestBody( + RequestBodyTypes.JSON.toMediaTypeOrNull() + ) + val animeId = app.post( + "$kickassanimeAPI/api/search", requestBody = body + ).text.let { tryParseJson>(it) }.let { res -> + (if (res?.size == 1) { + res.firstOrNull() + } else { + res?.find { + it.title?.equals( + title, + true + ) == true || it.title.createSlug() + ?.equals("${title.createSlug()}", true) == true + } + })?._id + } ?: return + + val seasonData = + app.get("$kickassanimeAPI/api/season/$animeId").text.let { tryParseJson>(it) }?.find { + val seasonNumber = when (title) { + "One Piece" -> 13 + "Hunter x Hunter" -> 5 + else -> season + } + it.number == seasonNumber + } + + val language = seasonData?.languages?.filter { + it == "ja-JP" || it == "en-US" + } + + language?.apmap { lang -> + val episodeSlug = + app.get("$kickassanimeAPI/api/episodes/${seasonData.id}?lh=$lang&page=1") + .parsedSafe()?.result?.find { eps -> + eps.episodeNumber == episode || eps.slug?.contains("${epsTitle.createSlug()}", true) == true + }?.slug ?: return@apmap + + val server = app.get("$kickassanimeAPI/api/watch/$episodeSlug").parsedSafe()?.servers?.find { + it.contains("/sapphire-duck/") + } ?: return@apmap + + invokeSapphire(server, lang == "en-US", subtitleCallback, callback) + + } + + + } + suspend fun invokeMoviesbay( title: String? = null, year: Int? = null, @@ -2921,4 +2980,45 @@ data class SorastreamResponse( data class SorastreamVideos( @JsonProperty("mediaUrl") val mediaUrl: String? = null, @JsonProperty("currentDefinition") val currentDefinition: String? = null, +) + +data class KaaServers( + @JsonProperty("servers") val servers: ArrayList? = arrayListOf(), +) + +data class KaaEpisode( + @JsonProperty("episodeNumber") val episodeNumber: Int? = null, + @JsonProperty("slug") val slug: String? = null, +) + +data class KaaEpisodeResults( + @JsonProperty("result") val result: ArrayList? = arrayListOf(), +) + +data class KaaSeason( + @JsonProperty("id") val id: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("languages") val languages: ArrayList? = arrayListOf(), +) + +data class KaaSearchResponse( + @JsonProperty("_id") val _id: String? = null, + @JsonProperty("title") val title: String? = null, +) + +data class SapphireSubtitles( + @JsonProperty("language") val language: String? = null, + @JsonProperty("url") val url: String? = null, +) + +data class SapphireStreams( + @JsonProperty("format") val format: String? = null, + @JsonProperty("audio_lang") val audio_lang: String? = null, + @JsonProperty("hardsub_lang") val hardsub_lang: String? = null, + @JsonProperty("url") val url: String? = null, +) + +data class SapphireSources( + @JsonProperty("streams") val streams: ArrayList? = arrayListOf(), + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), ) \ 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 index a323c1a4..b3003c97 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -32,6 +32,7 @@ import com.hexated.SoraExtractor.invokeGMovies import com.hexated.SoraExtractor.invokeGomovies import com.hexated.SoraExtractor.invokeJmdkhMovies import com.hexated.SoraExtractor.invokeJsmovies +import com.hexated.SoraExtractor.invokeKickassanime import com.hexated.SoraExtractor.invokeKisskh import com.hexated.SoraExtractor.invokeLing import com.hexated.SoraExtractor.invokeM4uhd @@ -98,7 +99,8 @@ open class SoraStream : TmdbProvider() { const val xMovieAPI = "https://xemovies.to" const val consumetFlixhqAPI = "https://api.consumet.org/movies/flixhq" const val consumetZoroAPI = "https://api.consumet.org/anime/zoro" - const val consumetCrunchyrollAPI = "https://api.consumet.org/anime/crunchyroll" + const val consumetCrunchyrollAPI = "https://api.consumet.org/anime/crunchyroll" // dead + const val kickassanimeAPI = "https://www2.kickassanime.ro" const val kissKhAPI = "https://kisskh.me" const val lingAPI = "https://ling-online.net" const val uhdmoviesAPI = "https://uhdmovies.world" @@ -405,7 +407,7 @@ open class SoraStream : TmdbProvider() { ) }, { - if (res.season != null && res.isAnime) invokeCrunchyroll( + if (res.season != null && res.isAnime) invokeKickassanime( res.title, res.epsTitle, res.season, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index d4005b26..c9bb1089 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -11,6 +11,7 @@ import com.hexated.SoraExtractor.invokeFwatayako import com.hexated.SoraExtractor.invokeGomovies import com.hexated.SoraExtractor.invokeHDMovieBox import com.hexated.SoraExtractor.invokeIdlix +import com.hexated.SoraExtractor.invokeKickassanime import com.hexated.SoraExtractor.invokeKimcartoon import com.hexated.SoraExtractor.invokeKisskh import com.hexated.SoraExtractor.invokeLing @@ -107,7 +108,7 @@ class SoraStreamLite : SoraStream() { ) }, { - if (res.season != null && res.isAnime) invokeCrunchyroll( + if (res.season != null && res.isAnime) invokeKickassanime( res.title, res.epsTitle, res.season, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index fdbc1dce..346d3e92 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -506,6 +506,42 @@ suspend fun fetchSoraEpisodes(id: String, type: String, episode: Int?) : Episode } } +suspend fun invokeSapphire( + url: String? = null, + isDub: Boolean = false, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, +) { + var data = app.get("${url?.replace("player.php", "config.php")}&action=config", referer = url).text + while (true) { + if (data.startsWith("{")) break + if (data == "null") { + data = app.get("$url&action=config", referer = url).text + delay(1000) + } + data = data.decodeBase64() + } + + tryParseJson(data).let { res -> + res?.streams?.filter { it.format == "adaptive_hls" && it.hardsub_lang.isNullOrEmpty() }?.reversed()?.map { source -> + val name = if (isDub) "English Dub" else "Raw" + M3u8Helper.generateM3u8( + "Crunchyroll [$name]", + source.url ?: return@map null, + "https://static.crunchyroll.com/", + ).forEach(callback) + } + res?.subtitles?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + getLanguage(sub.language ?: return@map null), + sub.url ?: return@map null + ) + ) + } + } +} + suspend fun bypassOuo(url: String?): String? { var res = session.get(url ?: return null) run lit@{ @@ -951,6 +987,10 @@ fun getBaseUrl(url: String): String { } } +fun String.decodeBase64(): String { + return Base64.decode(this, Base64.DEFAULT).toString(Charsets.UTF_8) +} + fun encode(input: String): String? = URLEncoder.encode(input, "utf-8") fun decryptStreamUrl(data: String): String {