Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib/megacloud): save key to preferences #2758

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.lib.megacloudextractor

import android.content.SharedPreferences
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
Expand All @@ -10,6 +11,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonPrimitive
Expand All @@ -19,7 +21,11 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy

class MegaCloudExtractor(private val client: OkHttpClient, private val headers: Headers) {
class MegaCloudExtractor(
private val client: OkHttpClient,
private val headers: Headers,
private val preferences: SharedPreferences
) {
private val json: Json by injectLazy()

private val playlistUtils by lazy { PlaylistUtils(client, headers) }
Expand All @@ -36,56 +42,65 @@ class MegaCloudExtractor(private val client: OkHttpClient, private val headers:
private val SOURCES_KEY = arrayOf("1", "6")
private const val E1_SCRIPT_URL = "https://megacloud.tv/js/player/a/prod/e1-player.min.js"
private const val E6_SCRIPT_URL = "https://rapid-cloud.co/js/player/prod/e6-player-v2.min.js"
private const val E4_SCRIPT_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
private val INDEX_PAIRS_MAP = mutableMapOf("1" to emptyList<List<Int>>(), "6" to emptyList<List<Int>>())
private val MUTEX = Mutex()
private var shouldUpdateKey = false
private const val PREF_KEY_KEY = "megacloud_key_"
private const val PREF_KEY_DEFAULT = "[[0, 0]]"

private inline fun <reified R> runLocked(crossinline block: () -> R) = runBlocking(Dispatchers.IO) {
MUTEX.withLock { block() }
}
}

private fun getIndexPairs(type: String) = runLocked {
INDEX_PAIRS_MAP[type].orEmpty().ifEmpty {
val scriptUrl = when (type) {
"1" -> E1_SCRIPT_URL
"6" -> E6_SCRIPT_URL
"4" -> E4_SCRIPT_URL
else -> throw Exception("Unknown key type")
}
val script = noCacheClient.newCall(GET(scriptUrl, cache = cacheControl))
.execute()
.use { it.body.string() }
val regex =
Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
val matches = regex.findAll(script).toList()
val indexPairs = matches.map { match ->
val var1 = match.groupValues[1]
val var2 = match.groupValues[2]

val regexVar1 = Regex(",${var1}=((?:0x)?([0-9a-fA-F]+))")
val regexVar2 = Regex(",${var2}=((?:0x)?([0-9a-fA-F]+))")

val matchVar1 = regexVar1.find(script)?.groupValues?.get(1)?.removePrefix("0x")
val matchVar2 = regexVar2.find(script)?.groupValues?.get(1)?.removePrefix("0x")

if (matchVar1 != null && matchVar2 != null) {
try {
listOf(matchVar1.toInt(16), matchVar2.toInt(16))
} catch (e: NumberFormatException) {
emptyList()
}
} else {
// Stolen from TurkAnime
private fun getKey(type: String): List<List<Int>> = runLocked {
if (shouldUpdateKey) {
updateKey(type)
shouldUpdateKey = false
}
json.decodeFromString<List<List<Int>>>(
preferences.getString(PREF_KEY_KEY + type, PREF_KEY_DEFAULT)!!
)
}

private fun updateKey(type: String) {
val scriptUrl = when (type) {
"1" -> E1_SCRIPT_URL
"6" -> E6_SCRIPT_URL
else -> throw Exception("Unknown key type")
}
val script = noCacheClient.newCall(GET(scriptUrl, cache = cacheControl))
.execute()
.use { it.body.string() }
val regex =
Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
val matches = regex.findAll(script).toList()
val indexPairs = matches.map { match ->
val var1 = match.groupValues[1]
val var2 = match.groupValues[2]

val regexVar1 = Regex(",${var1}=((?:0x)?([0-9a-fA-F]+))")
val regexVar2 = Regex(",${var2}=((?:0x)?([0-9a-fA-F]+))")

val matchVar1 = regexVar1.find(script)?.groupValues?.get(1)?.removePrefix("0x")
val matchVar2 = regexVar2.find(script)?.groupValues?.get(1)?.removePrefix("0x")

if (matchVar1 != null && matchVar2 != null) {
try {
listOf(matchVar1.toInt(16), matchVar2.toInt(16))
} catch (e: NumberFormatException) {
emptyList()
}
}.filter { it.isNotEmpty() }
INDEX_PAIRS_MAP[type] = indexPairs
indexPairs
}
} else {
emptyList()
}
}.filter { it.isNotEmpty() }
val encoded = json.encodeToString(indexPairs)
preferences.edit().putString(PREF_KEY_KEY + type, encoded).apply()
}

private fun cipherTextCleaner(data: String, type: String): Pair<String, String> {
val indexPairs = getIndexPairs(type)
val indexPairs = getKey(type)
val (password, ciphertext, _) = indexPairs.fold(Triple("", data, 0)) { previous, item ->
val start = item.first() + previous.third
val end = start + item.last()
Expand All @@ -103,7 +118,7 @@ class MegaCloudExtractor(private val client: OkHttpClient, private val headers:
val (ciphertext, password) = cipherTextCleaner(ciphered, type)
return CryptoAES.decrypt(ciphertext, password).ifEmpty {
// Update index pairs
runLocked { INDEX_PAIRS_MAP[type] = emptyList<List<Int>>() }
shouldUpdateKey = true
tryDecrypting(ciphered, type, attempts + 1)
}
}
Expand Down Expand Up @@ -138,7 +153,7 @@ class MegaCloudExtractor(private val client: OkHttpClient, private val headers:

if (!data.encrypted) return json.decodeFromString<VideoDto>(srcRes)

val ciphered = data.sources.jsonPrimitive.content.toString()
val ciphered = data.sources.jsonPrimitive.content
val decrypted = json.decodeFromString<List<VideoLink>>(tryDecrypting(ciphered, keyType))

return VideoDto(decrypted, data.tracks)
Expand Down
2 changes: 1 addition & 1 deletion multisrc/overrides/zorotheme/kaido/src/Kaido.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Kaido : ZoroTheme(
)

private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers, preferences) }

override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
Expand Down
4 changes: 0 additions & 4 deletions multisrc/overrides/zorotheme/zoro/additional.gradle

This file was deleted.

2 changes: 1 addition & 1 deletion multisrc/overrides/zorotheme/zoro/src/AniWatch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AniWatch : ZoroTheme(
)

private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers) }
private val megaCloudExtractor by lazy { MegaCloudExtractor(client, headers, preferences) }

override fun extractVideo(server: VideoData): List<Video> {
return when (server.name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.multisrc.zorotheme.dto.HtmlResponse
import eu.kanade.tachiyomi.multisrc.zorotheme.dto.SourcesResponse
import eu.kanade.tachiyomi.network.GET
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import eu.kanade.tachiyomi.util.parallelMapNotNull
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl
Expand All @@ -41,11 +39,11 @@ abstract class ZoroTheme(

override val supportsLatest = true

override val client: OkHttpClient = network.cloudflareClient
override val client: OkHttpClient = network.client

private val json: Json by injectLazy()

private val preferences: SharedPreferences by lazy {
val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}

Expand Down Expand Up @@ -115,7 +113,7 @@ abstract class ZoroTheme(
addIfNotBlank("em", params.end_month)
addIfNotBlank("ed", params.end_day)
addIfNotBlank("genres", params.genres)
}.build().toString()
}.build()

return GET(url, docHeaders)
}
Expand Down Expand Up @@ -211,7 +209,9 @@ abstract class ZoroTheme(
val name: String,
)

override fun videoListParse(response: Response): List<Video> {
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val response = client.newCall(videoListRequest(episode)).execute()

val episodeReferer = response.request.header("referer")!!
val typeSelection = preferences.typeToggle
val hosterSelection = preferences.hostToggle
Expand All @@ -236,9 +236,7 @@ abstract class ZoroTheme(
}
}.flatten()

return embedLinks.parallelCatchingFlatMap(::extractVideo).ifEmpty {
throw Exception("Failed to extract videos.")
}
return embedLinks.parallelCatchingFlatMap(::extractVideo)
}

protected open fun extractVideo(server: VideoData): List<Video> {
Expand All @@ -253,16 +251,6 @@ abstract class ZoroTheme(

// ============================= Utilities ==============================

private inline fun <A, B> Iterable<A>.parallelMapNotNull(crossinline f: suspend (A) -> B?): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll().filterNotNull()
}

private inline fun <A, B> Iterable<A>.parallelCatchingFlatMap(crossinline f: suspend (A) -> Iterable<B>): List<B> =
runBlocking {
map { async(Dispatchers.Default) { runCatching { f(it) }.getOrElse { emptyList() } } }.awaitAll().flatten()
}

private inline fun <reified T> Response.parseAs(): T {
return use { it.body.string() }.let(json::decodeFromString)
}
Expand Down