KickAssAnime: Fixes (#758)

* KickAssAnime: handle gogo redirects for old anime

* KickAssAnime: extract more reliable from MAVERICKKI

* KickAssAnime: wait a bit more when intercepting playlists to mitigate slow networks
This commit is contained in:
Samfun75
2022-08-11 23:57:37 +03:00
committed by GitHub
parent bf56fe6a83
commit 263ad88b92
6 changed files with 270 additions and 15 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'KickAssAnime'
pkgNameSuffix = 'en.kickassanime'
extClass = '.KickAssAnime'
extVersionCode = 2
extVersionCode = 3
libVersion = '13'
}

View File

@ -5,6 +5,9 @@ import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors.StreamSBExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -28,7 +31,6 @@ import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -90,9 +92,16 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
val data = getAppdata(response.asJsoup())
val episode = data["episode"]!!.jsonObject
val link1 = episode["link1"]!!.jsonPrimitive.content
val videoList = mutableListOf<Video>()
if (link1.contains("gogoplay4.com")) {
videoList.addAll(
extractGogoVideo(link1)
)
return videoList
}
val resp = client.newCall(GET(link1)).execute()
val sources = getVideoSource(resp.asJsoup())
val videoList = mutableListOf<Video>()
sources.forEach { source ->
when (source.jsonObject["name"]!!.jsonPrimitive.content) {
@ -119,29 +128,33 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun extractVideo(serverLink: String, server: String): List<Video> {
val playlistInterceptor = MasterPlaylistInterceptor()
val kickAssClient = client.newBuilder().addInterceptor(playlistInterceptor).build()
kickAssClient.newCall(GET(serverLink)).execute()
val data = playlistInterceptor.playlist
val playlist = mutableListOf<Video>()
val subsList = mutableListOf<Track>()
val data: MutableList<Pair<String, Headers>>
if (server == "MAVERICKKI") {
val subLink = serverLink.replace("embed", "api/source")
val subResponse = Jsoup.connect(subLink).ignoreContentType(true).execute().body()
val json = Json.decodeFromString<JsonObject>(subResponse)
val apiLink = serverLink.replace("embed", "api/source")
// for some reason the request to the api is only working reliably this way
val apiResponse = client.newCall(GET(apiLink, headers)).execute().asJsoup().text()
val json = Json.decodeFromString<JsonObject>(apiResponse)
val uri = Uri.parse(serverLink)
json["subtitles"]!!.jsonArray.forEach {
val subLang = it.jsonObject["name"]!!.jsonPrimitive.content
val uri = Uri.parse(serverLink)
val subUrl = "${uri.scheme}://${uri.host}" + it.jsonObject["src"]!!.jsonPrimitive.content
try {
subsList.add(Track(subUrl, subLang))
} catch (e: Error) {}
}
data = mutableListOf(Pair("${uri.scheme}://${uri.host}" + json["hls"]!!.jsonPrimitive.content, headers))
} else {
val playlistInterceptor = MasterPlaylistInterceptor()
val kickAssClient = client.newBuilder().addInterceptor(playlistInterceptor).build()
kickAssClient.newCall(GET(serverLink, headers)).execute()
data = playlistInterceptor.playlist
}
data.forEach { playlistPair ->
val (videoLink, headers) = playlistPair
data.forEach { (videoLink, headers) ->
val masterPlaylist = client.newCall(GET(videoLink, headers)).execute().body!!.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
@ -188,6 +201,24 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
return playlist
}
private fun extractGogoVideo(link: String): List<Video> {
val url = "https:" + decode(link).substringAfter("data=").substringBefore("&vref")
val videoList = mutableListOf<Video>()
val document = client.newCall(GET(url)).execute().asJsoup()
// Vidstreaming:
videoList.addAll(GogoCdnExtractor(network.client, json).videosFromUrl(url))
// Doodstream mirror:
document.select("div#list-server-more > ul > li.linkserver:contains(Doodstream)")
.firstOrNull()?.attr("data-video")
?.let { videoList.addAll(DoodExtractor(client).videosFromUrl(it)) }
// StreamSB mirror:
document.select("div#list-server-more > ul > li.linkserver:contains(StreamSB)")
.firstOrNull()?.attr("data-video")
?.let { videoList.addAll(StreamSBExtractor(client).videosFromUrl(it, headers)) }
return videoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")
@ -323,4 +354,6 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun encode(input: String): String = java.net.URLEncoder.encode(input, "utf-8")
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
}

View File

@ -52,7 +52,7 @@ class MasterPlaylistInterceptor : Interceptor {
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/88.0.705.63\""
}
webview.webViewClient = object : WebViewClient() {
@ -73,7 +73,7 @@ class MasterPlaylistInterceptor : Interceptor {
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(7, TimeUnit.SECONDS)
latch.await(9, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DoodExtractor(private val client: OkHttpClient) {
fun videosFromUrl(serverUrl: String): List<Video> {
try {
val response = client.newCall(GET(serverUrl)).execute()
val doodTld = serverUrl.substringAfter("https://dood.").substringBefore("/")
val content = response.body!!.string()
if (!content.contains("'/pass_md5/")) return emptyList()
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", serverUrl)
)
).execute().body!!.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
return listOf(Video(serverUrl, "Doodstream", videoUrl, headers = doodHeaders(doodTld)))
} catch (e: Exception) {
return emptyList()
}
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
private fun doodHeaders(tld: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://dood.$tld/")
}.build()
}

View File

@ -0,0 +1,115 @@
package eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.lang.Exception
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi
class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) {
fun videosFromUrl(serverUrl: String): List<Video> {
try {
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
val iv = document.select("div.wrapper")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val secretKey = document.select("body[class]")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val decryptionKey = document.select("div.videocontent")
.attr("class").substringAfter("videocontent-")
.filter { it.isDigit() }.toByteArray()
val encryptAjaxParams = cryptoHandler(
document.select("script[data-value]")
.attr("data-value"),
iv, secretKey, false
).substringAfter("&")
val httpUrl = serverUrl.toHttpUrl()
val host = "https://" + httpUrl.host + "/"
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
val encryptedId = cryptoHandler(id, iv, secretKey)
val token = httpUrl.queryParameter("token")
val qualityPrefix = if (token != null) "Gogostream: " else "Vidstreaming: "
val jsonResponse = client.newCall(
GET(
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
Headers.headersOf(
"X-Requested-With", "XMLHttpRequest"
)
)
).execute().body!!.string()
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
val fileURL = array[0].jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(fileURL)).execute().body!!.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
if (!videoUrl.startsWith("http")) {
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
}
videoList.add(Video(videoUrl, qualityPrefix + quality, videoUrl))
}
} else array.forEach {
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
.trim('"').replace(" ", "")
val fileURL = it.jsonObject["file"].toString().trim('"')
val videoHeaders = Headers.headersOf("Referer", serverUrl)
if (label == "auto") autoList.add(
Video(
fileURL,
qualityPrefix + label,
fileURL,
headers = videoHeaders
)
)
else videoList.add(Video(fileURL, qualityPrefix + label, fileURL, headers = videoHeaders))
}
return videoList.sortedByDescending {
it.quality.substringAfter(qualityPrefix).substringBefore("p").toIntOrNull() ?: -1
} + autoList
} catch (e: Exception) {
return emptyList()
}
}
private fun cryptoHandler(
string: String,
iv: ByteArray,
secretKeyString: ByteArray,
encrypt: Boolean = true
): String {
val ivParameterSpec = IvParameterSpec(iv)
val secretKey = SecretKeySpec(secretKeyString, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
}
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import okhttp3.Headers
import okhttp3.OkHttpClient
@ExperimentalSerializationApi
class StreamSBExtractor(private val client: OkHttpClient) {
private val hexArray = "0123456789ABCDEF".toCharArray()
private fun bytesToHex(bytes: ByteArray): String {
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = hexArray[v ushr 4]
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
}
return String(hexChars)
}
fun videosFromUrl(url: String, headers: Headers): List<Video> {
try {
val sbHeaders = headers.newBuilder()
.set("Referer", url)
.set(
"User-Agent",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0"
)
.set("Accept-Language", "en-US,en;q=0.5")
.set("watchsb", "streamsb")
.build()
val sbUrl = url.substringBefore("/e")
val id = url.substringAfter("e/").substringBefore("?")
val bytes = id.toByteArray()
val bytesToHex = bytesToHex(bytes)
val master = "$sbUrl/sources43/566d337678566f743674494a7c7c${bytesToHex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362"
val json = Json.decodeFromString<JsonObject>(
client.newCall(GET(master, sbHeaders))
.execute().body!!.string()
)
val masterUrl = json["stream_data"]!!.jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(masterUrl, sbHeaders)).execute().body!!.string()
val videoList = mutableListOf<Video>()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
.forEach {
val quality = "StreamSB: " + it.substringAfter("RESOLUTION=").substringAfter("x")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, headers = sbHeaders))
}
return videoList.reversed()
} catch (e: Exception) {
return emptyList()
}
}
}