fix(lib/lycoris&lulu) Repair decode json and work LuluStream #810

Merged
Hayanek merged 14 commits from fix-lycoris into main 2025-04-06 13:41:23 -05:00
3 changed files with 70 additions and 67 deletions
Showing only changes of commit de4383ff6f - Show all commits

View file

@ -7,22 +7,22 @@ import okhttp3.OkHttpClient
import java.util.regex.Pattern
class LuluExtractor(private val client: OkHttpClient) {
//Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lulustream.py
fun videosFromUrl(url: String, prefix: String, headers: Headers): List<Video> {
val luluHeaders = headers.newBuilder()
.add("Referer", "https://luluvdo.com")
.add("Origin", "https://luluvdo.com")
.build()
private val headers = Headers.Builder()
.add("Referer", "https://luluvdo.com")
.add("Origin", "https://luluvdo.com")
.build()
fun videosFromUrl(url: String, prefix: String): List<Video> {
val videos = mutableListOf<Video>()
try {
val html = client.newCall(GET(url, headers)).execute().use { it.body.string() }
val html = client.newCall(GET(url, luluHeaders)).execute().use { it.body.string() }
val m3u8Url = extractM3u8Url(html) ?: return emptyList()
val fixedUrl = fixM3u8Link(m3u8Url)
val quality = getResolution(fixedUrl)
val quality = getResolution(fixedUrl, luluHeaders)
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl))
videos.add(Video(fixedUrl, "${prefix}Lulu - $quality", fixedUrl, luluHeaders))
} catch (e: Exception) {
e.printStackTrace()
}
@ -50,37 +50,42 @@ class LuluExtractor(private val client: OkHttpClient) {
cuong-tran commented 2025-04-02 11:49:27 -05:00 (Migrated from github.com)

user urlBuilder instead so it can handles encoding

user `urlBuilder` instead so it can handles encoding
cuong-tran commented 2025-04-02 11:50:01 -05:00 (Migrated from github.com)
    private fun getResolution(m3u8Url: String): String {
```suggestion private fun getResolution(m3u8Url: String): String { ```
private fun fixM3u8Link(link: String): String {
val paramOrder = listOf("t", "s", "e", "f")
val baseUrl = link.split("?").first()
val params = link.split("?").getOrNull(1)?.split("&") ?: emptyList()
val params = Pattern.compile("[?&]([^=]*)=([^&]*)").matcher(link).let { matcher ->
generateSequence { if (matcher.find()) matcher.group(1) to matcher.group(2) else null }.toList()
}
val paramMap = mutableMapOf<String, String>()
val extraParams = mutableMapOf(
"i" to "0.3",
"sp" to "0"
)
val paramDict = mutableMapOf<String, String>()
val extraParams = mutableMapOf<String, String>()
params.forEachIndexed { index, param ->
val parts = param.split("=")
when {
parts.size == 2 -> {
val (key, value) = parts
if (key in paramOrder) paramMap[key] = value
else extraParams[key] = value
params.forEachIndexed { index, (key , value) ->
if (key.isNullOrEmpty()) {
if (index < paramOrder.size) {
if (value != null) {
paramDict[paramOrder[index]] = value
}
}
} else {
if (value != null) {
extraParams[key] = value
}
index < paramOrder.size -> paramMap[paramOrder[index]] = parts.firstOrNull() ?: ""
}
}
return buildString {
append(baseUrl)
append("?")
append(paramOrder.joinToString("&") { "$it=${paramMap[it]}" })
append("&")
append(extraParams.map { "${it.key}=${it.value}" }.joinToString("&"))
}
extraParams["i"] = "0.3"
extraParams["sp"] = "0"
val baseUrl = link.split("?")[0]
val fixedLink = StringBuilder(baseUrl)
val orderedParams = paramOrder.filter { paramDict.containsKey(it) }.map { "$it=${paramDict[it]}" }
fixedLink.append("?").append(orderedParams.joinToString("&"))
fixedLink.append("&").append(extraParams.entries.joinToString("&") { "${it.key}=${it.value}" })
return fixedLink.toString()
}
private fun getResolution(m3u8Url: String): String {
private fun getResolution(m3u8Url: String, headers: Headers): String {
return try {
val content = client.newCall(GET(m3u8Url, headers)).execute()
.use { it.body.string() }
@ -98,31 +103,29 @@ class LuluExtractor(private val client: OkHttpClient) {
}
object JavaScriptUnpacker {
private val UNPACK_REGEX = Regex(
"""}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""",
RegexOption.DOT_MATCHES_ALL
)
fun unpack(encodedJs: String): String? {
val match = UNPACK_REGEX.find(encodedJs) ?: return null
val (payload, radixStr, countStr, symtabStr) = match.destructured
val radix = radixStr.toIntOrNull() ?: return null
val count = countStr.toIntOrNull() ?: return null
val symtab = symtabStr.split('|')
if (symtab.size != count) throw IllegalArgumentException("Invalid symtab size")
val baseDict = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.take(radix)
.withIndex()
.associate { it.value to it.index }
return Regex("""\b\w+\b""").replace(payload) { mr ->
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
}.replace("\\", "")
private val UNPACK_REGEX by lazy {
Regex("""\}\('(.*)', *(\d+), *(\d+), *'(.*?)'\.split\('\|'\)""")
}
fun unpack(encodedJs: String): String? {
val match = UNPACK_REGEX.find(encodedJs) ?: return null
val (payload, radixStr, countStr, symtabStr) = match.destructured
val radix = radixStr.toIntOrNull() ?: return null
val count = countStr.toIntOrNull() ?: return null
val symtab = symtabStr.split('|')
if (symtab.size != count) throw IllegalArgumentException("Invalid symtab size")
val baseDict = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.take(radix)
.withIndex()
.associate { it.value to it.index }
return Regex("""\b\w+\b""").replace(payload) { mr ->
symtab.getOrNull(unbase(mr.value, radix, baseDict)) ?: mr.value
}.replace("\\", "")
}
private fun unbase(value: String, radix: Int, dict: Map<Char, Int>): Int {
var result = 0
var multiplier = 1

View file

@ -18,6 +18,16 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
private val GETLNKURL = "https://www.lycoris.cafe/api/watch/getLink"
private val wordsRegex by lazy {
Regex(
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
"""\\([0-7]{1,3})|""" + // \OOO (octal)
"""\\([btnfr"'$\\])""" // \n, \t, itd.
)
}
// Credit: https://github.com/skoruppa/docchi-stremio-addon/blob/main/app/players/lycoris.py
cuong-tran commented 2025-03-12 11:51:48 -05:00 (Migrated from github.com)

this will send a string "null" to json.decode and cause a crash

this will send a string `"null"` to json.decode and cause a crash
cuong-tran commented 2025-03-12 11:52:59 -05:00 (Migrated from github.com)

use script.parseAs<ScriptBody>()

use `script.parseAs<ScriptBody>()`
cuong-tran commented 2025-03-12 11:53:47 -05:00 (Migrated from github.com)

same here

same here
cuong-tran commented 2025-03-12 11:54:37 -05:00 (Migrated from github.com)
        val linkList: String? = fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false)

also fix fetchAndDecodeVideo() & decodeVideoLinks() so they both return String?

```suggestion val linkList: String? = fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false) ``` also fix `fetchAndDecodeVideo()` & `decodeVideoLinks()` so they both return `String?`
cuong-tran commented 2025-03-12 12:18:42 -05:00 (Migrated from github.com)
        val fhdLink = fetchAndDecodeVideo(client, data.episodeInfo.FHD.toString(), isSecondary = true)
        val sdLink = fetchAndDecodeVideo(client, data.episodeInfo.SD.toString(), isSecondary = true)
        val hdLink = fetchAndDecodeVideo(client, data.episodeInfo.HD.toString(), isSecondary = true)
```suggestion val fhdLink = fetchAndDecodeVideo(client, data.episodeInfo.FHD.toString(), isSecondary = true) val sdLink = fetchAndDecodeVideo(client, data.episodeInfo.SD.toString(), isSecondary = true) val hdLink = fetchAndDecodeVideo(client, data.episodeInfo.HD.toString(), isSecondary = true) ```
cuong-tran commented 2025-03-12 12:27:09 -05:00 (Migrated from github.com)
            if (!fhdLink.isNullOrBlank()) {
```suggestion if (!fhdLink.isNullOrBlank()) { ```
Hayanek commented 2025-03-12 12:49:49 -05:00 (Migrated from github.com)

with this I was aware of it but had no idea how to fix it so that such a problem would not occur

with this I was aware of it but had no idea how to fix it so that such a problem would not occur
cuong-tran commented 2025-03-12 23:47:41 -05:00 (Migrated from github.com)

Add ?. before toString and conditional action if it's null

Add `?.` before `toString` and conditional action if it's null
fun getVideosFromUrl(url: String, headers: Headers, prefix: String): List<Video> {
cuong-tran commented 2025-03-13 08:15:11 -05:00 (Migrated from github.com)
    private fun decodeVideoLinks(encodedUrl: String): String? {
        if (encodedUrl.isBlank()) {
```suggestion private fun decodeVideoLinks(encodedUrl: String): String? { if (encodedUrl.isBlank()) { ```
cuong-tran commented 2025-03-24 00:41:52 -05:00 (Migrated from github.com)
        val script = document.selectFirst("script[type='application/json']")?.data() ?: return emptyList()
```suggestion val script = document.selectFirst("script[type='application/json']")?.data() ?: return emptyList() ```
cuong-tran commented 2025-03-24 01:03:44 -05:00 (Migrated from github.com)
        val linkList = data.episodeInfo.id?.let { fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false) }

        val fhdLink = data.episodeInfo.FHD?.let { fetchAndDecodeVideo(client, data.episodeInfo.FHD, isSecondary = true) }
        val sdLink = data.episodeInfo.SD?.let { fetchAndDecodeVideo(client, data.episodeInfo.SD, isSecondary = true) }
        val hdLink = data.episodeInfo.HD?.let { fetchAndDecodeVideo(client, data.episodeInfo.HD, isSecondary = true) }
```suggestion val linkList = data.episodeInfo.id?.let { fetchAndDecodeVideo(client, data.episodeInfo.id.toString(), isSecondary = false) } val fhdLink = data.episodeInfo.FHD?.let { fetchAndDecodeVideo(client, data.episodeInfo.FHD, isSecondary = true) } val sdLink = data.episodeInfo.SD?.let { fetchAndDecodeVideo(client, data.episodeInfo.SD, isSecondary = true) } val hdLink = data.episodeInfo.HD?.let { fetchAndDecodeVideo(client, data.episodeInfo.HD, isSecondary = true) } ```
cuong-tran commented 2025-03-24 01:04:12 -05:00 (Migrated from github.com)
```suggestion ```
cuong-tran commented 2025-03-24 01:04:54 -05:00 (Migrated from github.com)
            } ?: hdLink?.takeIf { it.contains("https://") }?.let {
                videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it))
            }
```suggestion } ?: hdLink?.takeIf { it.contains("https://") }?.let { videos.add(Video(it, "${prefix}lycoris.cafe - 720p", it)) } ```
cuong-tran commented 2025-03-24 01:05:40 -05:00 (Migrated from github.com)
            } ?: sdLink?.takeIf { it.contains("https://") }?.let {
```suggestion } ?: sdLink?.takeIf { it.contains("https://") }?.let { ```
cuong-tran commented 2025-03-24 01:05:47 -05:00 (Migrated from github.com)

could we use checkLinks() instead of it.contains()?

could we use `checkLinks()` instead of `it.contains()`?
cuong-tran commented 2025-03-24 01:05:55 -05:00 (Migrated from github.com)

could we use checkLinks() instead of it.contains()?

could we use `checkLinks()` instead of `it.contains()`?
@ -145,17 +155,7 @@ class LycorisCafeExtractor(private val client: OkHttpClient) {
// 1. Obsługa kontynuacji linii (backslash + newline)
val withoutLineContinuation = text.replace("\\\n", "")
// 2. Regex do wykrywania wszystkich sekwencji escape
val regex = Regex(
"""\\U([0-9a-fA-F]{8})|""" + // \UXXXXXXXX
"""\\u([0-9a-fA-F]{4})|""" + // \uXXXX
"""\\x([0-9a-fA-F]{2})|""" + // \xHH
"""\\([0-7]{1,3})|""" + // \OOO (octal)
"""\\([btnfr"'$\\])""" // \n, \t, itd.
)
return regex.replace(withoutLineContinuation) { match ->
return wordsRegex.replace(withoutLineContinuation) { match ->
val (u8, u4, x2, octal, simple) = match.destructured
when {
u8.isNotEmpty() -> handleUnicode8(u8)

View file

@ -203,7 +203,7 @@ class Docchi : ConfigurableAnimeSource, AnimeHttpSource() {
}
serverUrl.contains("luluvdo.com") -> {
luluExtractor.videosFromUrl(serverUrl, prefix)
luluExtractor.videosFromUrl(serverUrl, prefix, headers)
}
serverUrl.contains("drive.google.com") -> {