Skip to content

Commit c01da21

Browse files
TMDB Integration, Provider Updates, Extractors and UI Refinement
Interface & UI: - Immersive Mode: Added an option in the mobile version settings to toggle immersive mode, allowing users to hide system bars thanks to Or-Cr. - Cast Section: Fixed focus and zoom behavior for elements within the Cast section thanks to Or-Cr. - Seasons Layout: - Changed the image format from horizontal (2:1) to vertical (2:3) with a fixed height of 200dp thanks to Or-Cr. - Limited the zoom effect exclusively to the image to prevent text distortion thanks to Or-Cr. - Increased the margin between image and text to 20dp thanks to Or-Cr. - Added a paddingBottom of 40dp to the seasons list to ensure text isn't cut off by the screen edge when an item is in focus thanks to Or-Cr. Providers & Metadata: - StreamingCommunity: Updated the default domain to a static one thanks to Or-Cr. - Season Posters: Added TMDB seasonal poster support for Streaming Community, StreamingIta, GuardaFlix, GuardaSerie, Altadefinizione01 and CB01. - Full TMDB Support: Implemented total integration (season posters, episode metadata, banners and ratings) for the HDFilme, Einschalten and AniWorld providers. - FilmPalast: Integrated TMDB with automatic title cleaning (removal of SxxExx tags) for accurate matching and added support for dynamic banners. - StreamingIta: Added TMDB ratings to the home slider using parallel asynchronous loading for better performance. - Einschalten: Updated the provider to support the new API structure and load home categories via API. Extractors: - Vidnest: Added the new Vidnest extractor. - Closeload: Restored the original decryption methods that were removed during refactoring to ensure maximum resilience. - VidHide: Added the specific URL alias used by the Moflix provider. - StreamUp: Fixed the extraction logic to correctly handle links containing the /v/ pattern and added the referrer to the headers. - Gupload: Updated the logic for the new decodePayload system and added the headers.
1 parent 6886287 commit c01da21

27 files changed

+561
-263
lines changed

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ android {
3030
applicationId "com.streamflixreborn.streamflix"
3131
minSdk 21
3232
targetSdk 35
33-
versionCode 111
34-
versionName "1.7.82"
33+
versionCode 112
34+
versionName "1.7.83"
3535

3636
buildConfigField "String", "APP_LAYOUT", "\"${properties.getProperty("APP_LAYOUT")}\""
3737
buildConfigField "String", "TMDB_API_KEY", "\"${properties.getProperty("TMDB_API_KEY")}\""

app/src/main/java/com/streamflixreborn/streamflix/activities/main/MainMobileActivity.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import android.view.View
77
import android.widget.Toast
88
import androidx.activity.OnBackPressedCallback
99
import androidx.activity.viewModels
10+
import androidx.core.view.WindowCompat
11+
import androidx.core.view.WindowInsetsCompat
12+
import androidx.core.view.WindowInsetsControllerCompat
1013
import androidx.fragment.app.FragmentActivity
1114
import androidx.lifecycle.Lifecycle
1215
import androidx.lifecycle.flowWithLifecycle
@@ -36,6 +39,9 @@ class MainMobileActivity : FragmentActivity() {
3639
override fun onCreate(savedInstanceState: Bundle?) {
3740
setTheme(R.style.AppTheme_Mobile)
3841
super.onCreate(savedInstanceState)
42+
// Abilita la modalità immersiva (schermo intero)
43+
updateImmersiveMode()
44+
3945
_binding = ActivityMainMobileBinding.inflate(layoutInflater)
4046
setContentView(binding.root)
4147

@@ -86,6 +92,8 @@ class MainMobileActivity : FragmentActivity() {
8692
binding.bnvMain.visibility = View.VISIBLE
8793
// Update tab visibility based on provider
8894
updateNavigationVisibility()
95+
// Riabilita la modalità immersiva nel caso fosse stata interrotta
96+
updateImmersiveMode()
8997
}
9098
else -> binding.bnvMain.visibility = View.GONE
9199
}
@@ -157,4 +165,21 @@ class MainMobileActivity : FragmentActivity() {
157165
if (Provider.supportsTvShows(provider)) View.VISIBLE else View.GONE
158166
}
159167
}
168+
169+
fun updateImmersiveMode() {
170+
val window = window
171+
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
172+
173+
if (UserPreferences.immersiveMode) {
174+
// Modalità immersiva attiva: nasconde le barre
175+
insetsController.systemBarsBehavior =
176+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
177+
insetsController.hide(WindowInsetsCompat.Type.systemBars())
178+
} else {
179+
// Modalità immersiva disattivata: mostra le barre
180+
insetsController.show(WindowInsetsCompat.Type.systemBars())
181+
insetsController.systemBarsBehavior =
182+
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
183+
}
184+
}
160185
}

app/src/main/java/com/streamflixreborn/streamflix/extractors/CloseloadExtractor.kt

Lines changed: 119 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import retrofit2.converter.gson.GsonConverterFactory
1313
import retrofit2.http.GET
1414
import retrofit2.http.Header
1515
import retrofit2.http.Url
16+
import java.nio.charset.Charset
1617

1718
class 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 {

app/src/main/java/com/streamflixreborn/streamflix/extractors/Extractor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ abstract class Extractor {
9292
VidrockExtractor(),
9393
VideasyExtractor(),
9494
VidzeeExtractor(),
95+
VidnestExtractor()
9596
)
9697

9798
suspend fun extract(link: String, server: Video.Server? = null): Video {

0 commit comments

Comments
 (0)