@@ -13,6 +13,7 @@ import retrofit2.converter.gson.GsonConverterFactory
1313import retrofit2.http.GET
1414import retrofit2.http.Header
1515import retrofit2.http.Url
16+ import java.nio.charset.Charset
1617
1718class CloseloadExtractor : Extractor () {
1819
@@ -23,95 +24,137 @@ class CloseloadExtractor : Extractor() {
2324 val service = Service .build(mainUrl)
2425 val document = service.get(link, RidomoviesProvider .URL )
2526 val html = document.toString()
26-
27- val unpacked = JsUnpacker (html).unpack() ? : html
28-
29- // 1. DYNAMIC PARAMETER DETECTION
30- // Try to find the match constants used in the unmix loop: (charCode - 399756995 % (i + 5))
31- var magicNum = 399_756_995L
27+
28+ val unpacker = JsUnpacker (html)
29+ val unpacked = if (unpacker.detect()) unpacker.unpack() ? : html else html
30+
31+ // --- 1. DYNAMIC PARAMETER DETECTION ---
32+ var magicNum = 399756995L
3233 var offset = 5
3334 val matchConst = Regex (""" (\d+)\s*%\s*\(\s*i\s*\+\s*(\d+)\s*\)""" ).find(unpacked)
3435 if (matchConst != null ) {
3536 magicNum = matchConst.groupValues[1 ].toLong()
3637 offset = matchConst.groupValues[2 ].toInt()
3738 }
3839
39- // 2. DATA CANDIDATE COLLECTION
40- val candidates = mutableListOf<String >()
40+ // --- 2. CANDIDATE COLLECTION ---
41+ val inputs = mutableListOf<String >()
42+
43+ // A. DC Hello Pattern
44+ val varNameMatch = Regex (""" myPlayer\.src\(\{\s*src:\s*(\w+)\s*,""" ).find(unpacked)
45+ if (varNameMatch != null ) {
46+ val varName = varNameMatch.groupValues[1 ]
47+ val dcHelloMatch = Regex (""" var\s+$varName \s*=\s*dc_hello\("([^"]+)"\)""" ).find(unpacked)
48+ if (dcHelloMatch != null ) {
49+ inputs.add(dcHelloMatch.groupValues[1 ])
50+ }
51+ }
4152
42- // A. Arrays of strings (usually split URLs)
43- val arrayRegex = Regex (""" \[\s*((?:"[^"]+",?\s*)+)\]""" )
44- arrayRegex.findAll(unpacked).forEach { match ->
45- val parts = Regex (" \" ([^\" ]+)\" " ).findAll(match.groupValues[1 ])
46- .map { it.groupValues[1 ] }
47- .toList()
53+ // B. Arrays of strings
54+ Regex (""" \[\s*((?:"[^"]+",?\s*)+)\]""" ).findAll(unpacked).forEach { match ->
55+ val parts = Regex (" \" ([^\" ]+)\" " ).findAll(match.groupValues[1 ]).map { it.groupValues[1 ] }.toList()
4856 if (parts.size > 5 ) {
49- candidates .add(parts.joinToString(" " ))
57+ inputs .add(parts.joinToString(" " ))
5058 }
5159 }
5260
53- // B. Long strings in function calls
54- val stringCallRegex = Regex (""" \(\s*"([a-zA-Z0-9+/=]{30,})"\s*\)""" )
55- stringCallRegex.findAll(unpacked).forEach { match ->
56- candidates.add(match.groupValues[1 ])
61+ // C. Long strings in function calls
62+ Regex (""" \(\s*"([a-zA-Z0-9+/=]{30,})"\s*\)""" ).findAll(unpacked).forEach { match ->
63+ inputs.add(match.groupValues[1 ])
5764 }
5865
59- // 3. URL EXTRACTION
60- // Try smart brute force on all candidates, fallback to pure base64
61- val source = candidates.firstNotNullOfOrNull { smartBruteForce(it, magicNum, offset) }
62- ? : extractPureBase64(unpacked)
63- ? : error(" Unable to fetch video URL" )
66+ // --- 3. EXECUTE BRUTE FORCE ---
67+ // Try to find the URL in gathered inputs using smart brute force
68+ var source = inputs.firstNotNullOfOrNull { smartBruteForce(it, magicNum, offset) }
6469
65- return Video (
66- source = source,
67- headers = mapOf (" Referer" to mainUrl),
68- type = MimeTypes .APPLICATION_M3U8
69- )
70+ // D. Fallback: Search for Pure Base64 strings if nothing else worked
71+ if (source == null ) {
72+ source = Regex (" [\" '](aHR0[a-zA-Z0-9+/=]{20,})[\" ']" ).findAll(unpacked)
73+ .mapNotNull { safeBase64Decode(it.groupValues[1 ]) }
74+ .map { String (it, Charsets .UTF_8 ) }
75+ .firstOrNull { it.startsWith(" http" ) }
76+ }
77+
78+ if (source == null ) throw Exception (" No video found" )
79+
80+ return Video (source, headers = mapOf (" Referer" to mainUrl), type = MimeTypes .APPLICATION_M3U8 )
7081 }
7182
7283 /* *
73- * Tries all possible combinations of Reverse, ROT13, and Base64 transformations.
74- * This makes the extractor resilient to changes in obfuscation order.
84+ * Smart Brute Force: Tries all permutations of:
85+ * 1. String Transforms (Reverse, ROT13)
86+ * 2. Base64 Decode
87+ * 3. (Optional) Intermediate Transforms + Second Base64 Decode
88+ * 4. Byte Transforms (Reverse, ROT13)
89+ * 5. (Optional) Decryption Loop
90+ *
91+ * Returns the extracted URL string if found, otherwise null.
7592 */
7693 private fun smartBruteForce (inputData : String , magicNum : Long , offset : Int ): String? {
7794 val stringTransforms = listOf< (String ) -> String > (
78- { it }, // No change
79- { it.reversed() }, // Reverse string
80- { rot13(it) }, // ROT13
81- { rot13(it.reversed()) }, // Reverse -> ROT13
82- { rot13(it).reversed() } // ROT13 -> Reverse
95+ { it }, // No change
96+ { it.reversed() }, // Reverse
97+ { rot13(it) }, // ROT13
98+ { rot13(it.reversed()) }, // Reverse -> ROT13
99+ { rot13(it).reversed() } // ROT13 -> Reverse
83100 )
84101
85102 val byteTransforms = listOf< (ByteArray ) -> ByteArray > (
86- { it }, // No change
87- { it.reversedArray() }, // Reverse byte array
88- { rot13Bytes(it) }, // ROT13 bytes
103+ { it }, // No change
104+ { it.reversedArray() }, // Reverse bytes
105+ { rot13Bytes(it) }, // ROT13 bytes
89106 { rot13Bytes(it.reversedArray()) }, // Reverse -> ROT13 bytes
90107 { rot13Bytes(it).reversedArray() } // ROT13 -> Reverse bytes
91108 )
92109
93110 for (sTrans in stringTransforms) {
94111 for (bTrans in byteTransforms) {
95112 try {
113+ // Phase 1: String Transform -> Base64
96114 val sRes = sTrans(inputData)
97115 val b64Res = safeBase64Decode(sRes) ? : continue
98- val finalBytesCandidate = bTrans(b64Res)
99116
100- // Try with the decryption loop (using dynamic constants)
101- val adjusted = unmixLoop(finalBytesCandidate, magicNum, offset)
102- val url = String (adjusted, Charsets .UTF_8 ).trim()
103- if (url.startsWith(" http" ) && url.contains(" .mp4" )) {
104- return url
117+ // Collect candidates for Phase 2 (Byte Transform & Loop)
118+ // We store: (bytes, description)
119+ val candidates = mutableListOf<ByteArray >()
120+ candidates.add(b64Res) // Standard Logic
121+
122+ // Phase 1.5: Double Base64 Logic
123+ try {
124+ val firstDecodeStr = String (b64Res, Charsets .ISO_8859_1 ) // Keep byte values
125+
126+ // Variation A: Direct Double Decode
127+ val b64Res2 = safeBase64Decode(firstDecodeStr)
128+ if (b64Res2 != null ) candidates.add(b64Res2)
129+
130+ // Variation B: Reverse before Double Decode (B64 -> Reverse -> B64)
131+ val b64Res2Reversed = safeBase64Decode(firstDecodeStr.reversed())
132+ if (b64Res2Reversed != null ) candidates.add(b64Res2Reversed)
133+
134+ } catch (e: Exception ) { }
135+
136+ // Phase 2: Byte Transform -> Decryption Loop -> Validate
137+ for (candidateBytes in candidates) {
138+ val finalBytes = bTrans(candidateBytes)
139+
140+ // Try WITH decryption loop
141+ try {
142+ val adjusted = unmixLoop(finalBytes, magicNum, offset)
143+ val url = String (adjusted, Charsets .UTF_8 ).trim()
144+ if (url.startsWith(" http" ) && url.contains(" .mp4" )) {
145+ return url
146+ }
147+ } catch (e: Exception ) {}
148+
149+ // Try WITHOUT decryption loop
150+ try {
151+ val urlPlain = String (finalBytes, Charsets .UTF_8 ).trim()
152+ if (urlPlain.startsWith(" http" ) && urlPlain.contains(" .mp4" )) {
153+ return urlPlain
154+ }
155+ } catch (e: Exception ) {}
105156 }
106157
107- // Try without decryption loop (some variants don't use it)
108- val urlPlain = String (finalBytesCandidate, Charsets .UTF_8 ).trim()
109- if (urlPlain.startsWith(" http" ) && urlPlain.contains(" .mp4" )) {
110- return urlPlain
111- }
112-
113- // Try with unmix loop BEFORE byte transforms (rare but possible order variation)
114- // If the order is Base64 -> Unmix -> Reverse/ROT13... unlikely given the loop nature but cheap to try
115158 } catch (e: Exception ) {
116159 continue
117160 }
@@ -120,60 +163,43 @@ class CloseloadExtractor : Extractor() {
120163 return null
121164 }
122165
123- private fun rot13Bytes (input : ByteArray ): ByteArray {
124- val output = input.clone()
125- for (i in output.indices) {
126- val b = output[i].toInt()
127- output[i] = when (b) {
128- in ' A' .code.. ' Z' .code -> (' A' .code + (b - ' A' .code + 13 ) % 26 ).toByte()
129- in ' a' .code.. ' z' .code -> (' a' .code + (b - ' a' .code + 13 ) % 26 ).toByte()
130- else -> output[i]
131- }
132- }
133- return output
166+ private fun safeBase64Decode (str : String ): ByteArray? = try {
167+ Base64 .decode(str, Base64 .DEFAULT )
168+ } catch (e: IllegalArgumentException ) {
169+ null
134170 }
135171
136- private fun extractPureBase64 (unpacked : String ): String? {
137- val pureB64 = Regex (""" ["'](aHR0[a-zA-Z0-9+/=]{20,})["']""" ).find(unpacked)
138- if (pureB64 != null ) {
139- val decoded = safeBase64Decode(pureB64.groupValues[1 ])
140- if (decoded != null ) {
141- val urlCandidate = String (decoded, Charsets .UTF_8 ).trim()
142- if (urlCandidate.startsWith(" http" )) {
143- return urlCandidate
144- }
172+ private fun rot13 (input : String ): String = input.map {
173+ when (it) {
174+ in ' A' .. ' Z' -> ' A' + (it - ' A' + 13 ) % 26
175+ in ' a' .. ' z' -> ' a' + (it - ' a' + 13 ) % 26
176+ else -> it
177+ }
178+ }.joinToString(" " )
179+
180+ private fun rot13Bytes (data : ByteArray ): ByteArray {
181+ val res = ByteArray (data.size)
182+ for (i in data.indices) {
183+ val b = data[i].toInt()
184+ res[i] = when (b) {
185+ in 65 .. 90 -> (65 + (b - 65 + 13 ) % 26 ).toByte()
186+ in 97 .. 122 -> (97 + (b - 97 + 13 ) % 26 ).toByte()
187+ else -> b.toByte()
145188 }
146189 }
147- return null
190+ return res
148191 }
149192
150193 private fun unmixLoop (decodedBytes : ByteArray , magicNum : Long , offset : Int ): ByteArray {
151194 val finalBytes = ByteArray (decodedBytes.size)
152195 for (i in decodedBytes.indices) {
153196 val b = decodedBytes[i].toInt() and 0xFF
154197 val adjustment = (magicNum % (i + offset)).toInt()
155- finalBytes[i] = ((b - adjustment + 255 + 1 ) % 256 ).toByte() // Using +256
198+ finalBytes[i] = ((b - adjustment + 256 ) % 256 ).toByte()
156199 }
157200 return finalBytes
158201 }
159-
160- private fun safeBase64Decode (str : String ): ByteArray? {
161- return try {
162- val cleaned = str.replace(Regex (" [^a-zA-Z0-9+/]" ), " " )
163- Base64 .decode(cleaned, Base64 .DEFAULT )
164- } catch (e: Exception ) {
165- null
166- }
167- }
168-
169- private fun rot13 (input : String ): String = input.map {
170- when (it) {
171- in ' A' .. ' Z' -> ' A' + (it - ' A' + 13 ) % 26
172- in ' a' .. ' z' -> ' a' + (it - ' a' + 13 ) % 26
173- else -> it
174- }
175- }.joinToString(" " )
176-
202+
177203 private interface Service {
178204 companion object {
179205 fun build (baseUrl : String ): Service {
0 commit comments