fix(en/KickAssAnime): Fix video extractor (#1634)

* fix: Update Dynamic AES Extractor and fix video extractor on sapphire videos

* fix: Ignore exceptions on video extractor

* fix: Prevent empty video URLs

* chore: Bump version
This commit is contained in:
Claudemirovsky 2023-05-22 22:03:56 +00:00 committed by GitHub
parent e3e473953d
commit 600b10a176
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 24 deletions

View File

@ -9,7 +9,7 @@ ext {
pkgNameSuffix = 'en.kickassanime' pkgNameSuffix = 'en.kickassanime'
extClass = '.KickAssAnime' extClass = '.KickAssAnime'
libVersion = '13' libVersion = '13'
extVersionCode = 26 extVersionCode = 27
} }
dependencies { dependencies {

View File

@ -129,7 +129,9 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videos = response.parseAs<ServersDto>() val videos = response.parseAs<ServersDto>()
val extractor = KickAssAnimeExtractor(client, json, headers) val extractor = KickAssAnimeExtractor(client, json, headers)
return videos.servers.flatMap(extractor::videosFromUrl) return videos.servers.mapNotNull {
runCatching { extractor.videosFromUrl(it) }.getOrNull()
}.flatten()
} }
// =========================== Anime Details ============================ // =========================== Anime Details ============================

View File

@ -70,7 +70,14 @@ data class VideoDto(
val dash: String = "", val dash: String = "",
val subtitles: List<SubtitlesDto> = emptyList(), val subtitles: List<SubtitlesDto> = emptyList(),
) { ) {
val playlistUrl by lazy { if (hls.isBlank()) "https:$dash" else hls } val playlistUrl by lazy {
hls.ifEmpty { dash }.let { uri ->
when {
uri.startsWith("//") -> "https:$uri"
else -> uri
}
}
}
@Serializable @Serializable
data class SubtitlesDto(val name: String, val language: String, val src: String) data class SubtitlesDto(val name: String, val language: String, val src: String)

View File

@ -6,10 +6,10 @@ import android.os.Looper
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES.decodeHex
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -18,21 +18,27 @@ class AESKeyExtractor(private val client: OkHttpClient) {
val KEY_MAP = mutableMapOf( val KEY_MAP = mutableMapOf(
// Default key. if it changes, the extractor will update it. // Default key. if it changes, the extractor will update it.
"PinkBird" to "7191d608bd4deb4dc36f656c4bbca1b7".toByteArray(), "PinkBird" to "7191d608bd4deb4dc36f656c4bbca1b7".toByteArray(),
"SapphireDuck" to "f04274d54a9e01ed4a728c5c1889886e".toByteArray(), // i hate sapphire "SapphireDuck" to "2940ba141ba490377b3f0a28ce56641a".toByteArray(), // i hate sapphire
) )
private const val ERROR_MSG_GENERIC = "the AES key was not found." // ..... dont try reading them.
private const val ERROR_MSG_VAR = "the AES key variable was not found"
private val KEY_VAR_REGEX by lazy { Regex("\\.AES\\[.*?\\]\\((\\w+)\\),") } private val KEY_VAR_REGEX by lazy { Regex("\\.AES\\[.*?\\]\\((\\w+)\\),") }
private val KEY_FUNC_REGEX by lazy {
Regex(",\\w+\\.SHA1\\)\\(\\[.*?function.*?\\(\\w+=(\\w+).*?function")
}
} }
private val context = Injekt.get<Application>() private val context: Application by injectLazy()
private val handler by lazy { Handler(Looper.getMainLooper()) } private val handler by lazy { Handler(Looper.getMainLooper()) }
class ExtractorJSI(private val latch: CountDownLatch, private val prefix: String) { class ExtractorJSI(private val latch: CountDownLatch, private val prefix: String) {
@JavascriptInterface @JavascriptInterface
fun setKey(key: String) { fun setKey(key: String) {
AESKeyExtractor.KEY_MAP.set(prefix, key.toByteArray()) val keyBytes = when {
key.length == 32 -> key.toByteArray()
else -> key.decodeHex()
}
AESKeyExtractor.KEY_MAP.set(prefix, keyBytes)
latch.countDown() latch.countDown()
} }
} }
@ -78,7 +84,7 @@ class AESKeyExtractor(private val client: OkHttpClient) {
webView = null webView = null
} }
return AESKeyExtractor.KEY_MAP.get(prefix) ?: throw Exception(ERROR_MSG_GENERIC) return AESKeyExtractor.KEY_MAP.get(prefix) ?: throw Exception()
} }
private fun patchScriptFromUrl(url: String): String { private fun patchScriptFromUrl(url: String): String {
@ -90,18 +96,37 @@ class AESKeyExtractor(private val client: OkHttpClient) {
} }
private fun patchScriptFromHtml(baseUrl: String, body: String): String { private fun patchScriptFromHtml(baseUrl: String, body: String): String {
val scriptPath = body.substringAfter("script src=\"").substringBefore('"') val scriptPath = body.substringAfter("src=\"assets/").substringBefore('"')
val scriptUrl = "$baseUrl/$scriptPath" val scriptUrl = "$baseUrl/assets/$scriptPath"
val scriptBody = client.newCall(GET(scriptUrl)).execute().body.string() val scriptBody = client.newCall(GET(scriptUrl)).execute().body.string()
val varWithKeyName = KEY_VAR_REGEX.find(scriptBody) return when {
?.groupValues KEY_FUNC_REGEX.containsMatchIn(scriptBody) -> patchScriptWithFunction(scriptBody) // Sapphire
?.last() KEY_VAR_REGEX.containsMatchIn(scriptBody) -> patchScriptWithVar(scriptBody) // PinkBird
?: Exception(ERROR_MSG_VAR) else -> throw Exception() // ????
}
}
val varWithKeyBody = scriptBody.substringAfter("var $varWithKeyName=") private fun patchScriptWithVar(script: String): String {
val varWithKeyName = KEY_VAR_REGEX.find(script)
?.groupValues
?.lastOrNull()
?: throw Exception()
val varWithKeyBody = script.substringAfter("var $varWithKeyName=")
.substringBefore(";") .substringBefore(";")
return scriptBody.replace(varWithKeyBody, "AESKeyExtractor.setKey($varWithKeyBody)") return script.replace(varWithKeyBody, "AESKeyExtractor.setKey($varWithKeyBody)")
}
private fun patchScriptWithFunction(script: String): String {
val (match, functionName) = KEY_FUNC_REGEX.find(script)
?.groupValues
?: throw Exception()
val patchedMatch = match.replace(
";function",
";AESKeyExtractor.setKey($functionName().toString());function",
)
return script.replace(match, patchedMatch)
} }
} }

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES.decodeHex
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.Jsoup import org.jsoup.Jsoup
@ -83,7 +84,7 @@ class KickAssAnimeExtractor(
return when { return when {
videoObject.hls.isBlank() -> videoObject.hls.isBlank() ->
extractVideosFromDash(masterPlaylist, prefix, subtitles) extractVideosFromDash(masterPlaylist, prefix, subtitles)
else -> extractVideosFromHLS(masterPlaylist, prefix, subtitles) else -> extractVideosFromHLS(masterPlaylist, prefix, subtitles, videoObject.playlistUrl)
} }
} }
@ -111,15 +112,20 @@ class KickAssAnimeExtractor(
} }
} }
private fun extractVideosFromHLS(playlist: String, prefix: String, subs: List<Track>): List<Video> { private fun extractVideosFromHLS(playlist: String, prefix: String, subs: List<Track>, playlistUrl: String): List<Video> {
val separator = "#EXT-X-STREAM-INF" val separator = "#EXT-X-STREAM-INF"
return playlist.substringAfter(separator).split(separator).map { return playlist.substringAfter(separator).split(separator).mapNotNull {
val resolution = it.substringAfter("RESOLUTION=") val resolution = it.substringAfter("RESOLUTION=")
.substringBefore("\n") .substringBefore("\n")
.substringAfter("x") .substringAfter("x")
.substringBefore(",") + "p" .substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n") val videoUrl = it.substringAfter("\n").substringBefore("\n").let { url ->
when {
url.startsWith("/") -> "https://" + playlistUrl.toHttpUrl().host + url
else -> url
}
}.ifEmpty { return@mapNotNull null }
Video(videoUrl, "$prefix - $resolution", videoUrl, subtitleTracks = subs) Video(videoUrl, "$prefix - $resolution", videoUrl, subtitleTracks = subs)
} }
@ -143,7 +149,6 @@ class KickAssAnimeExtractor(
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun Element.formatBits(attribute: String = "bandwidth"): String? { private fun Element.formatBits(attribute: String = "bandwidth"): String? {
var bits = attr(attribute).toLongOrNull() ?: 0L var bits = attr(attribute).toLongOrNull() ?: 0L