diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index 295981ff..66435639 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,5 +1,5 @@ // use an integer for version numbers -version = 114 +version = 115 cloudstream { diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index c9d89b26..15647b13 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -7,9 +7,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Session import com.hexated.RabbitStream.extractRabbitStream -import com.lagradost.cloudstream3.extractors.Filesim -import com.lagradost.cloudstream3.extractors.StreamSB -import com.lagradost.cloudstream3.extractors.XStreamCdn import com.lagradost.cloudstream3.network.CloudflareKiller import com.lagradost.nicehttp.RequestBodyTypes import kotlinx.coroutines.delay @@ -660,7 +657,7 @@ object SoraExtractor : SoraStream() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit, ) { - val (id, type) = getSoraIdAndType(title, year, season) ?: return invokeJustchill( + val (id, type) = getSoraIdAndType(title, year, season) ?: return invokeSoraBackup( title, year, season, @@ -698,7 +695,7 @@ object SoraExtractor : SoraStream() { } } - suspend fun invokeJustchill( + private suspend fun invokeSoraBackup( title: String? = null, year: Int? = null, season: Int? = null, @@ -707,7 +704,7 @@ object SoraExtractor : SoraStream() { callback: (ExtractorLink) -> Unit, ) { val results = - app.get("$chillAPI/api/search?keyword=$title").parsedSafe()?.data?.results + app.get("$soraBackupAPI/api/search?keyword=$title").parsedSafe()?.data?.results val media = if (results?.size == 1) { results.firstOrNull() } else { @@ -732,17 +729,17 @@ object SoraExtractor : SoraStream() { } } ?: return - val episodeId = app.get("$chillAPI/api/detail?id=${media.id}&category=${media.domainType}").parsedSafe()?.data?.episodeVo?.find { + val episodeId = app.get("$soraBackupAPI/api/detail?id=${media.id}&category=${media.domainType}").parsedSafe()?.data?.episodeVo?.find { it.seriesNo == (episode ?: 0) }?.id ?: return - val sources = app.get("$chillAPI/api/episode?id=${media.id}&category=${media.domainType}&episode=$episodeId").parsedSafe()?.data + val sources = app.get("$soraBackupAPI/api/episode?id=${media.id}&category=${media.domainType}&episode=$episodeId").parsedSafe()?.data sources?.qualities?.map { source -> callback.invoke( ExtractorLink( - "ChillMovie", - "ChillMovie", + this.name, + this.name, source.url ?: return@map null, "", source.quality ?: Qualities.Unknown.value, @@ -2785,6 +2782,80 @@ object SoraExtractor : SoraStream() { } + suspend fun invokePutlocker( + title: String? = null, + year: Int? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + val query = if (season == null) { + title + } else { + "$title - season $season" + } + + val res = app.get("$putlockerAPI/movie/search/$query").document + val scripData = res.select("div.movies-list div.ml-item").map { + it.selectFirst("h2")?.text() to it.selectFirst("a")?.attr("href") + } + val script = if (scripData.size == 1) { + scripData.first() + } else { + scripData.find { + if (season == null) { + it.first.equals(title, true) || (it.first?.contains( + "$title", true + ) == true && it.first?.contains("$year") == true) + } else { + it.first?.contains("$title", true) == true && it.first?.contains( + "Season $season", true + ) == true + } + } + } + + val id = fixUrl(script?.second ?: return).split("-").lastOrNull()?.removeSuffix("/") + val iframe = app.get("$putlockerAPI/ajax/movie_episodes/$id") + .parsedSafe()?.html?.let { Jsoup.parse(it) }?.let { server -> + if (season == null) { + server.select("div.les-content a").map { + it.attr("data-id") to it.attr("data-server") + } + } else { + server.select("div.les-content a").map { it } + .filter { it.text().contains("Episode $episode", true) }.map { + it.attr("data-id") to it.attr("data-server") + } + } + } + + iframe?.apmap { + delay(3000) + val embedUrl = app.get("$putlockerAPI/ajax/movie_embed/${it.first}") + .parsedSafe()?.src ?: return@apmap null + val sources = extractPutlockerSources(embedUrl)?.parsedSafe() + + argamap( + { + sources?.callback(embedUrl, "Server ${it.second}", callback) + }, + { + if (!sources?.backupLink.isNullOrBlank()) { + extractPutlockerSources(sources?.backupLink)?.parsedSafe() + ?.callback( + embedUrl, "Backup ${it.second}", callback + ) + } else { + return@argamap + } + }, + ) + + } + + } + } @@ -3176,4 +3247,23 @@ data class ChillData( data class ChillSearch( @JsonProperty("data") val data: ChillData? = null, +) + +data class PutlockerEpisodes( + @JsonProperty("html") val html: String? = null, +) + +data class PutlockerEmbed( + @JsonProperty("src") val src: String? = null, +) + +data class PutlockerSources( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String? = null, + @JsonProperty("type") val type: String? = null, +) + +data class PutlockerResponses( + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("backupLink") val backupLink: String? = null, ) \ 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 718b02ab..92f81bb7 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -41,6 +41,7 @@ import com.hexated.SoraExtractor.invokeMoviezAdd import com.hexated.SoraExtractor.invokeNinetv import com.hexated.SoraExtractor.invokePapaonMovies1 import com.hexated.SoraExtractor.invokePapaonMovies2 +import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeRinzrymovies import com.hexated.SoraExtractor.invokeRubyMovies @@ -122,6 +123,7 @@ open class SoraStream : TmdbProvider() { const val biliBiliAPI = "https://api-vn.kaguya.app/server" const val watchOnlineAPI = "https://watchonline.ag" const val nineTvAPI = "https://api.9animetv.live" + const val putlockerAPI = "https://ww7.putlocker.vip" // INDEX SITE const val baymoviesAPI = "https://opengatewayindex.pages.dev" // dead const val chillmovies0API = "https://chill.aicirou.workers.dev/0:" // dead @@ -538,6 +540,9 @@ open class SoraStream : TmdbProvider() { callback ) }, + { + invokePutlocker(res.title, res.year, res.season, res.episode, callback) + }, { invokeTvMovies(res.title, res.season, res.episode, callback) }, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index 68af1f78..011b4b36 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -18,6 +18,7 @@ import com.hexated.SoraExtractor.invokeM4uhd import com.hexated.SoraExtractor.invokeMovie123Net import com.hexated.SoraExtractor.invokeMovieHab import com.hexated.SoraExtractor.invokeNinetv +import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeSeries9 import com.hexated.SoraExtractor.invokeSmashyStream @@ -46,6 +47,15 @@ class SoraStreamLite : SoraStream() { val res = AppUtils.parseJson(data) argamap( + { + invokePutlocker( + res.title, + res.year, + res.season, + res.episode, + callback + ) + }, { invokeWatchsomuch( res.imdbId, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index bc13756e..1e0a6ed0 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -7,6 +7,7 @@ import com.hexated.SoraStream.Companion.baymoviesAPI import com.hexated.SoraStream.Companion.consumetCrunchyrollAPI import com.hexated.SoraStream.Companion.filmxyAPI import com.hexated.SoraStream.Companion.gdbot +import com.hexated.SoraStream.Companion.putlockerAPI import com.hexated.SoraStream.Companion.smashyStreamAPI import com.hexated.SoraStream.Companion.tvMoviesAPI import com.hexated.SoraStream.Companion.twoEmbedAPI @@ -14,13 +15,11 @@ import com.hexated.SoraStream.Companion.watchOnlineAPI import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getCaptchaToken import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall -import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.RequestBodyTypes -import com.lagradost.nicehttp.requestCreator import kotlinx.coroutines.delay import okhttp3.FormBody import okhttp3.Headers @@ -42,7 +41,7 @@ import kotlin.collections.ArrayList import kotlin.math.min val soraAPI = base64DecodeAPI("cA==YXA=cy8=Y20=di8=LnQ=b2s=a2w=bG8=aS4=YXA=ZS0=aWw=b2I=LW0=Z2E=Ly8=czo=dHA=aHQ=") -val chillAPI = base64DecodeAPI("dg==LnQ=bGw=aGk=dGM=dXM=Lmo=b2s=a2w=bG8=Ly8=czo=dHA=aHQ=") +val soraBackupAPI = base64DecodeAPI("dg==LnQ=bGw=aGk=dGM=dXM=Lmo=b2s=a2w=bG8=Ly8=czo=dHA=aHQ=") val soraHeaders = mapOf( "lang" to "en", @@ -826,6 +825,61 @@ fun Map>?.matchingEpisode( }?.firstOrNull() } +suspend fun extractPutlockerSources(url: String?): NiceResponse? { + val embedHost = url?.substringBefore("/embed-player") + val player = app.get( + url ?: return null, + referer = "${putlockerAPI}/" + ).document.select("div#player") + + val text = "\"${player.attr("data-id")}\"" + val password = player.attr("data-hash") + val cipher = CryptoAES.plEncrypt(password, text) + + return app.get( + "$embedHost/ajax/getSources/", params = mapOf( + "id" to cipher.cipherText, + "h" to cipher.password, + "a" to cipher.iv, + "t" to cipher.salt, + ), referer = url + ) +} + +suspend fun PutlockerResponses?.callback( + referer: String, + server: String, + callback: (ExtractorLink) -> Unit +) { + val ref = getBaseUrl(referer) + this?.sources?.map { source -> + val request = app.get(source.file, referer = ref) + callback.invoke( + ExtractorLink( + "Putlocker [$server]", + "Putlocker [$server]", + if (!request.isSuccessful) return@map null else source.file, + ref, + if (source.file.contains("m3u8")) getPutlockerQuality(request.text) else source.label?.replace( + Regex("[Pp]"), + "" + )?.trim()?.toIntOrNull() + ?: Qualities.P720.value, + source.file.contains("m3u8") + ) + ) + } +} + +fun getPutlockerQuality(quality: String): Int { + return when { + quality.contains("NAME=\"1080p\"") || quality.contains("RESOLUTION=1920x1080") -> Qualities.P1080.value + quality.contains("NAME=\"720p\"") || quality.contains("RESOLUTION=1280x720")-> Qualities.P720.value + else -> Qualities.P480.value + } +} + + fun getEpisodeSlug( season: Int? = null, episode: Int? = null, @@ -1195,6 +1249,25 @@ object CryptoAES { return String(bEncode) } + fun plEncrypt(password: String, plainText: String): EncryptResult { + val saltBytes = generateSalt(8) + val key = ByteArray(KEY_SIZE / 8) + val iv = ByteArray(IV_SIZE / 8) + EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) + val keyS = SecretKeySpec(key, AES) + val cipher = Cipher.getInstance(HASH_CIPHER) + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) + val cipherText = cipher.doFinal(plainText.toByteArray()) + val bEncode = Base64.encode(cipherText, Base64.NO_WRAP) + return EncryptResult( + String(bEncode).toHex(), + password.toHex(), + saltBytes.toHex(), + iv.toHex() + ) + } + /** * Decrypt * Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051 @@ -1272,6 +1345,18 @@ object CryptoAES { SecureRandom().nextBytes(this) } } + + private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } + + private fun String.toHex(): String = toByteArray().toHex() + + data class EncryptResult( + val cipherText: String, + val password: String, + val salt: String, + val iv: String + ) + } object RabbitStream {