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:
parent
e3e473953d
commit
600b10a176
@ -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 {
|
||||||
|
@ -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 ============================
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user