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:
@ -5,7 +5,7 @@ ext {
|
||||
extName = 'KickAssAnime'
|
||||
pkgNameSuffix = 'en.kickassanime'
|
||||
extClass = '.KickAssAnime'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user