From 10444fe64bab9acb8a43200aaacc930f68aa018b Mon Sep 17 00:00:00 2001 From: Mehdi GM Date: Mon, 21 Sep 2026 17:59:56 +0000 Subject: [PATCH 1/2] feat(extractors): add StreamRuby extractor --- library/api/jvm/library.api | 4 - .../cloudstream3/extractors/StreamRuby.kt | 86 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 + 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt diff --git a/library/api/jvm/library.api b/library/api/jvm/library.api index 71939de56ea..389c8e83f32 100644 --- a/library/api/jvm/library.api +++ b/library/api/jvm/library.api @@ -8072,8 +8072,6 @@ public class com/lagradost/cloudstream3/utils/AtomicList : java/util/List, kotli public fun add (Ljava/lang/Object;)Z public fun addAll (ILjava/util/Collection;)Z public fun addAll (Ljava/util/Collection;)Z - public fun addFirst (Ljava/lang/Object;)V - public fun addLast (Ljava/lang/Object;)V public fun clear ()V public fun contains (Ljava/lang/Object;)Z public fun containsAll (Ljava/util/Collection;)Z @@ -8092,8 +8090,6 @@ public class com/lagradost/cloudstream3/utils/AtomicList : java/util/List, kotli public fun remove (I)Ljava/lang/Object; public fun remove (Ljava/lang/Object;)Z public fun removeAll (Ljava/util/Collection;)Z - public fun removeFirst ()Ljava/lang/Object; - public fun removeLast ()Ljava/lang/Object; public fun replaceAll (Ljava/util/function/UnaryOperator;)V public fun retainAll (Ljava/util/Collection;)Z public fun set (ILjava/lang/Object;)Ljava/lang/Object; diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt new file mode 100644 index 00000000000..86475aa3e45 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.Prerelease +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.Qualities.Unknown +import com.lagradost.cloudstream3.utils.newExtractorLink + +@Prerelease +class StreamRubyCom : StreamRuby() { + override var mainUrl = "https://streamruby.com" +} + +@Prerelease +open class StreamRuby : ExtractorApi() { + override var name = "StreamRuby" + override open var mainUrl = "https://rubyvidhub.com" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val embedUrl = url.trim().trimEnd('/') + val fileCode = embedUrl.substringAfterLast("/") + .removeSuffix(".html") + .substringAfterLast("-") + if (fileCode.isBlank()) return + + try { + app.get(embedUrl, referer = referer ?: mainUrl) + } catch (_: Exception) { + return + } + + val dlText = try { + app.post( + "$mainUrl/dl", + data = mapOf( + "op" to "embed", + "file_code" to fileCode, + "auto" to "1", + "referer" to embedUrl, + ), + headers = mapOf("User-Agent" to USER_AGENT, "Referer" to embedUrl), + referer = embedUrl + ).text + } catch (_: Exception) { + return + } + + val unpacked = JsUnpacker(dlText).unpack() ?: dlText + + val streamUrl = Regex("""file:\s*["']([^"']+\.m3u8[^"']*)["']""") + .find(unpacked)?.groupValues?.get(1) + ?: Regex("""file:\s*["']([^"']+)["']""") + .find(unpacked)?.groupValues?.get(1) + ?.takeIf { it.contains("/hls") || it.endsWith(".mp4") } + + if (streamUrl.isNullOrBlank()) return + + callback.invoke( + newExtractorLink( + source = name, + name = name, + url = streamUrl, + type = ExtractorLinkType.M3U8 + ) { + this.referer = "$mainUrl/" + this.quality = Unknown.value + this.headers = mapOf( + "User-Agent" to USER_AGENT, + "Origin" to mainUrl, + "Referer" to "$mainUrl/" + ) + } + ) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index faf498024ad..fb37d6be60a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -239,6 +239,8 @@ import com.lagradost.cloudstream3.extractors.Streamhub2 import com.lagradost.cloudstream3.extractors.Streamlare import com.lagradost.cloudstream3.extractors.StreamoUpload import com.lagradost.cloudstream3.extractors.Streamplay +import com.lagradost.cloudstream3.extractors.StreamRuby +import com.lagradost.cloudstream3.extractors.StreamRubyCom import com.lagradost.cloudstream3.extractors.Streamsss import com.lagradost.cloudstream3.extractors.Streamwish2 import com.lagradost.cloudstream3.extractors.Strwish @@ -1346,6 +1348,8 @@ val extractorApis: AtomicMutableList = atomicListOf( Vids(), Playmate(), Hexload(), + StreamRuby(), + StreamRubyCom(), ) From a7b8f0d61c0850b2eb34a1128fb0b1d5fd462a80 Mon Sep 17 00:00:00 2001 From: Mehdi GM Date: Mon, 21 Sep 2026 23:18:49 +0000 Subject: [PATCH 2/2] refactor(extractors): make StreamRuby inherit from LuluStream --- library/api/jvm/library.api | 3 ++ .../cloudstream3/extractors/LuluStream.kt | 50 +++++++++++++------ .../cloudstream3/extractors/StreamRuby.kt | 40 +++++++++------ 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/library/api/jvm/library.api b/library/api/jvm/library.api index 389c8e83f32..bf200f5026d 100644 --- a/library/api/jvm/library.api +++ b/library/api/jvm/library.api @@ -3253,10 +3253,13 @@ public final class com/lagradost/cloudstream3/extractors/Linkbox$Responses$Compa public class com/lagradost/cloudstream3/extractors/LuluStream : com/lagradost/cloudstream3/utils/ExtractorApi { public fun ()V + protected fun getFileCode (Ljava/lang/String;)Ljava/lang/String; public fun getMainUrl ()Ljava/lang/String; public fun getName ()Ljava/lang/String; + protected fun getPlayerScript (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getRequiresReferer ()Z public fun getUrl (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + protected fun parsePlayerScript (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class com/lagradost/cloudstream3/extractors/Lulustream1 : com/lagradost/cloudstream3/extractors/LuluStream { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt index dec67959410..941e452f8d5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt @@ -22,30 +22,50 @@ class Lulustream2 : LuluStream() { } open class LuluStream : ExtractorApi() { - override val name = "LuluStream" + override val name = "LuluStream" override val mainUrl = "https://luluvdo.com" override val requiresReferer = true - override suspend fun getUrl( + protected open fun getFileCode(url: String): String { + return url.substringAfterLast("/") + } + + protected open suspend fun getPlayerScript( + fileCode: String, url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val filecode = url.substringAfterLast("/") - val postUrl = "$mainUrl/dl" - val post = app.post( - postUrl, + referer: String? + ): String { + return app.post( + "$mainUrl/dl", data = mapOf( "op" to "embed", - "file_code" to filecode, + "file_code" to fileCode, "auto" to "1", "referer" to (referer ?: "") ) ).document - post.selectFirst("script:containsData(vplayer)")?.data() - ?.let { script -> - JwPlayerHelper.extractStreamLinks(script, name, mainUrl, callback, subtitleCallback) - } + .selectFirst("script:containsData(vplayer)") + ?.data() + .orEmpty() + } + + protected open suspend fun parsePlayerScript( + script: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + JwPlayerHelper.extractStreamLinks(script, name, mainUrl, callback, subtitleCallback) + } + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val fileCode = getFileCode(url) + val script = getPlayerScript(fileCode, url, referer) + if (script.isBlank()) return + parsePlayerScript(script, subtitleCallback, callback) } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt index 86475aa3e45..5010409fece 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamRuby.kt @@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.JsUnpacker @@ -17,30 +16,33 @@ class StreamRubyCom : StreamRuby() { } @Prerelease -open class StreamRuby : ExtractorApi() { +open class StreamRuby : LuluStream() { override var name = "StreamRuby" override open var mainUrl = "https://rubyvidhub.com" - override val requiresReferer = true - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val embedUrl = url.trim().trimEnd('/') - val fileCode = embedUrl.substringAfterLast("/") + override fun getFileCode(url: String): String { + return url.trim().trimEnd('/') + .substringAfterLast("/") .removeSuffix(".html") .substringAfterLast("-") - if (fileCode.isBlank()) return + } + + override suspend fun getPlayerScript( + fileCode: String, + url: String, + referer: String? + ): String { + if (fileCode.isBlank()) return "" + + val embedUrl = url.trim().trimEnd('/') try { app.get(embedUrl, referer = referer ?: mainUrl) } catch (_: Exception) { - return + return "" } - val dlText = try { + return try { app.post( "$mainUrl/dl", data = mapOf( @@ -53,10 +55,16 @@ open class StreamRuby : ExtractorApi() { referer = embedUrl ).text } catch (_: Exception) { - return + "" } + } - val unpacked = JsUnpacker(dlText).unpack() ?: dlText + override suspend fun parsePlayerScript( + script: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val unpacked = JsUnpacker(script).unpack() ?: script val streamUrl = Regex("""file:\s*["']([^"']+\.m3u8[^"']*)["']""") .find(unpacked)?.groupValues?.get(1)