Fix(lib/ChillxExtractor): Unify extraction logic, improve key fetching and subtitle handling (#169)
This commit is contained in:
parent
decac82e65
commit
b41c963dda
3 changed files with 19 additions and 59 deletions
|
@ -12,24 +12,19 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
class ChillxExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val REGEX_MASTER_JS by lazy { Regex("""JScript[\w+]?\s*=\s*'([^']+)""") }
|
private val REGEX_MASTER_JS by lazy { Regex("""\s*=\s*'([^']+)""") }
|
||||||
private val REGEX_EVAL_KEY by lazy { Regex("""eval\(\S+\("(\S+)",\d+,"(\S+)",(\d+),(\d+),""") }
|
|
||||||
private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") }
|
private val REGEX_SOURCES by lazy { Regex("""sources:\s*\[\{"file":"([^"]+)""") }
|
||||||
private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") }
|
private val REGEX_FILE by lazy { Regex("""file: ?"([^"]+)"""") }
|
||||||
private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""") }
|
private val REGEX_SOURCE by lazy { Regex("""source = ?"([^"]+)"""") }
|
||||||
|
private val REGEX_SUBS by lazy { Regex("""\[(.*?)\](https?://[^\s,]+)""") }
|
||||||
// matches "[language]https://...,"
|
private const val KEY_SOURCE = "https://raw.githubusercontent.com/Rowdy-Avocado/multi-keys/keys/index.html"
|
||||||
private val REGEX_SUBS by lazy { Regex("""\[(.*?)\](.*?)"?\,""") }
|
|
||||||
private const val KEY_SOURCE = "https://rowdy-avocado.github.io/multi-keys/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
|
fun videoFromUrl(url: String, referer: String, prefix: String = "Chillx - "): List<Video> {
|
||||||
|
@ -42,7 +37,7 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
|
|
||||||
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
val master = REGEX_MASTER_JS.find(body)?.groupValues?.get(1) ?: return emptyList()
|
||||||
val aesJson = json.decodeFromString<CryptoInfo>(master)
|
val aesJson = json.decodeFromString<CryptoInfo>(master)
|
||||||
val key = fetchKey()
|
val key = fetchKey() ?: throw ErrorLoadingException("Unable to get key")
|
||||||
val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
|
val decryptedScript = decryptWithSalt(aesJson.ciphertext, aesJson.salt, key)
|
||||||
.replace("\\n", "\n")
|
.replace("\\n", "\n")
|
||||||
.replace("\\", "")
|
.replace("\\", "")
|
||||||
|
@ -52,29 +47,10 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
|
?: REGEX_SOURCE.find(decryptedScript)?.groupValues?.get(1)
|
||||||
?: return emptyList()
|
?: return emptyList()
|
||||||
|
|
||||||
val subtitleList = buildList<Track> {
|
val subtitleList = buildList {
|
||||||
body.takeIf { it.contains("<track kind=\"captions\"") }
|
val subtitles = REGEX_SUBS.findAll(decryptedScript)
|
||||||
?.let(Jsoup::parse)
|
subtitles.forEach {
|
||||||
?.select("track[kind=captions]")
|
add(Track(it.groupValues[2], decodeUnicodeEscape(it.groupValues[1])))
|
||||||
?.forEach {
|
|
||||||
add(Track(it.attr("src"), it.attr("label")))
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptedScript.takeIf { it.contains("subtitle:") }
|
|
||||||
?.substringAfter("subtitle: ")
|
|
||||||
?.substringBefore("\n")
|
|
||||||
?.let(REGEX_SUBS::findAll)
|
|
||||||
?.forEach { add(Track(it.groupValues[2], it.groupValues[1])) }
|
|
||||||
|
|
||||||
decryptedScript.takeIf { it.contains("tracks:") }
|
|
||||||
?.substringAfter("tracks: ")
|
|
||||||
?.substringBefore("\n")
|
|
||||||
?.also {
|
|
||||||
runCatching {
|
|
||||||
json.decodeFromString<List<TrackDto>>(it)
|
|
||||||
.filter { it.kind == "captions" }
|
|
||||||
.forEach { add(Track(it.file, it.label)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,25 +63,15 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
private fun fetchKey(): String {
|
private fun fetchKey(): String? {
|
||||||
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.get(0)
|
return client.newCall(GET(KEY_SOURCE)).execute().parseAs<KeysData>().keys.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getKey(body: String): String {
|
private fun decodeUnicodeEscape(input: String): String {
|
||||||
val (encrypted, pass, offset, index) = REGEX_EVAL_KEY.find(body)!!.groupValues.drop(1)
|
val regex = Regex("u([0-9a-fA-F]{4})")
|
||||||
val decrypted = decryptScript(encrypted, pass, offset.toInt(), index.toInt())
|
return regex.replace(input) {
|
||||||
return decrypted.substringAfter("'").substringBefore("'")
|
it.groupValues[1].toInt(16).toChar().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptScript(encrypted: String, pass: String, offset: Int, index: Int): String {
|
|
||||||
val trimmedPass = pass.substring(0, index)
|
|
||||||
val bits = encrypted.split(pass[index]).map { item ->
|
|
||||||
trimmedPass.foldIndexed(item) { index, acc, it ->
|
|
||||||
acc.replace(it.toString(), index.toString())
|
|
||||||
}
|
|
||||||
}.filter(String::isNotBlank)
|
|
||||||
|
|
||||||
return bits.joinToString("") { Char(it.toInt(index) - offset).toString() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -116,16 +82,10 @@ class ChillxExtractor(private val client: OkHttpClient, private val headers: Hea
|
||||||
val salt: String,
|
val salt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TrackDto(
|
|
||||||
val kind: String,
|
|
||||||
val label: String = "",
|
|
||||||
val file: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KeysData(
|
data class KeysData(
|
||||||
@SerialName("chillx")
|
@SerialName("chillx")
|
||||||
val keys: List<String>
|
val keys: List<String>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
class ErrorLoadingException(message: String) : Exception(message)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Tokuzilla'
|
extName = 'Tokuzilla'
|
||||||
extClass = '.Tokuzilla'
|
extClass = '.Tokuzilla'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.AnimeSAGA'
|
extClass = '.AnimeSAGA'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://www.animesaga.in'
|
baseUrl = 'https://www.animesaga.in'
|
||||||
overrideVersionCode = 10
|
overrideVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue