KickAssAnime: Fix Sapphire, use reanimation jutsu on some dead server… (#1194)
This commit is contained in:
@ -1,15 +1,17 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'KickAssAnime'
|
extName = 'KickAssAnime'
|
||||||
pkgNameSuffix = 'en.kickassanime'
|
pkgNameSuffix = 'en.kickassanime'
|
||||||
extClass = '.KickAssAnime'
|
extClass = '.KickAssAnime'
|
||||||
extVersionCode = 13
|
extVersionCode = 14
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compileOnly libs.bundles.coroutines
|
||||||
implementation(project(':lib-streamsb-extractor'))
|
implementation(project(':lib-streamsb-extractor'))
|
||||||
implementation(project(':lib-dood-extractor'))
|
implementation(project(':lib-dood-extractor'))
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,13 @@ import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
|||||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
@ -29,6 +35,7 @@ import kotlinx.serialization.json.jsonArray
|
|||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -45,7 +52,12 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
override val name = "KickAssAnime"
|
override val name = "KickAssAnime"
|
||||||
|
|
||||||
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://www2.kickassanime.ro")!! }
|
override val baseUrl by lazy {
|
||||||
|
preferences.getString(
|
||||||
|
"preferred_domain",
|
||||||
|
"https://www2.kickassanime.ro"
|
||||||
|
)!!
|
||||||
|
}
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -67,24 +79,30 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
|
|
||||||
// Add non working server names here
|
// Add non working server names here
|
||||||
private val deadServers = listOf(
|
private val deadServers = listOf(
|
||||||
"BETA-SERVER", "BETASERVER1", "BETASERVER3", "DEVSTREAM",
|
"BETASERVER1", "BETASERVER3", "DEVSTREAM",
|
||||||
"THETA-ORIGINAL-V4", "DAILYMOTION", "KICKASSANIME1"
|
"THETA-ORIGINAL-V4", "KICKASSANIME1"
|
||||||
)
|
)
|
||||||
|
|
||||||
private val workingServers = arrayOf(
|
private val workingServers = arrayOf(
|
||||||
"StreamSB", "PINK-BIRD", "Doodstream", "MAVERICKKI",
|
"StreamSB", "PINK-BIRD", "Doodstream", "MAVERICKKI", "BETA-SERVER", "DAILYMOTION",
|
||||||
"BETAPLAYER", "Vidstreaming", "SAPPHIRE-DUCK", "KICKASSANIMEV2", "ORIGINAL-QUALITY-V2"
|
"BETAPLAYER", "Vidstreaming", "SAPPHIRE-DUCK", "KICKASSANIMEV2", "ORIGINAL-QUALITY-V2"
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/api/get_anime_list/all/$page")
|
override fun popularAnimeRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/api/get_anime_list/all/$page")
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
|
||||||
val data = responseObject["data"]!!.jsonArray
|
val data = responseObject["data"]!!.jsonArray
|
||||||
val animes = data.map { item ->
|
val animes = data.map { item ->
|
||||||
SAnime.create().apply {
|
SAnime.create().apply {
|
||||||
setUrlWithoutDomain(item.jsonObject["slug"]!!.jsonPrimitive.content.substringBefore("/episode"))
|
setUrlWithoutDomain(
|
||||||
thumbnail_url = "$baseUrl/uploads/" + item.jsonObject["poster"]!!.jsonPrimitive.content
|
item.jsonObject["slug"]!!.jsonPrimitive.content.substringBefore(
|
||||||
|
"/episode"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
thumbnail_url =
|
||||||
|
"$baseUrl/uploads/" + item.jsonObject["poster"]!!.jsonPrimitive.content
|
||||||
title = item.jsonObject["name"]!!.jsonPrimitive.content
|
title = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,50 +167,60 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
val resp = client.newCall(GET(link)).execute()
|
val resp = client.newCall(GET(link)).execute()
|
||||||
val sources = getVideoSource(resp.asJsoup())
|
val sources = getVideoSource(resp.asJsoup())
|
||||||
|
|
||||||
sources.forEach { source ->
|
|
||||||
when (source.jsonObject["name"]!!.jsonPrimitive.content) {
|
|
||||||
in deadServers -> {}
|
|
||||||
"BETAPLAYER" -> {
|
|
||||||
videoList.addAll(
|
videoList.addAll(
|
||||||
|
sources.parallelMap { source ->
|
||||||
|
runCatching {
|
||||||
|
when (source.jsonObject["name"]!!.jsonPrimitive.content) {
|
||||||
|
in deadServers -> { null }
|
||||||
|
"SAPPHIRE-DUCK" -> {
|
||||||
|
extractSapphireVideo(
|
||||||
|
source.jsonObject["src"]!!.jsonPrimitive.content,
|
||||||
|
source.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"BETAPLAYER" -> {
|
||||||
extractBetaVideo(
|
extractBetaVideo(
|
||||||
source.jsonObject["src"]!!.jsonPrimitive.content,
|
source.jsonObject["src"]!!.jsonPrimitive.content,
|
||||||
source.jsonObject["name"]!!.jsonPrimitive.content
|
source.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
"KICKASSANIMEV2", "ORIGINAL-QUALITY-V2" -> {
|
"KICKASSANIMEV2", "ORIGINAL-QUALITY-V2", "BETA-SERVER" -> {
|
||||||
videoList.addAll(
|
|
||||||
extractKickasssVideo(
|
extractKickasssVideo(
|
||||||
source.jsonObject["src"]!!.jsonPrimitive.content,
|
source.jsonObject["src"]!!.jsonPrimitive.content,
|
||||||
source.jsonObject["name"]!!.jsonPrimitive.content
|
source.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
"DAILYMOTION" -> {
|
||||||
|
extractDailymotion(
|
||||||
|
source.jsonObject["src"]!!.jsonPrimitive.content,
|
||||||
|
source.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
videoList.addAll(
|
|
||||||
extractVideo(
|
extractVideo(
|
||||||
source.jsonObject["src"]!!.jsonPrimitive.content,
|
source.jsonObject["src"]!!.jsonPrimitive.content,
|
||||||
source.jsonObject["name"]!!.jsonPrimitive.content
|
source.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}.filterNotNull().flatten()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return videoList
|
return videoList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVideo(serverLink: String, server: String): List<Video> {
|
private fun extractVideo(serverLink: String, server: String): List<Video> {
|
||||||
val playlist = mutableListOf<Video>()
|
val playlist = mutableListOf<Video>()
|
||||||
val subsList = mutableListOf<Track>()
|
val subsList = mutableListOf<Track>()
|
||||||
val data: MutableList<Pair<String, Headers>>
|
var vidHeader = headers
|
||||||
|
|
||||||
if (server == "MAVERICKKI") {
|
val resp = if (server == "MAVERICKKI") {
|
||||||
val apiLink = serverLink.replace("embed", "api/source")
|
val apiLink = serverLink.replace("embed", "api/source")
|
||||||
// for some reason the request to the api is only working reliably this way
|
val embedHeader = Headers.headersOf("referer", serverLink)
|
||||||
val apiResponse = client.newCall(GET(apiLink, headers)).execute().asJsoup().text()
|
val apiResponse = client.newCall(GET(apiLink, embedHeader)).execute()
|
||||||
val json = Json.decodeFromString<JsonObject>(apiResponse)
|
val json = Json.decodeFromString<JsonObject>(apiResponse.body!!.string())
|
||||||
val uri = Uri.parse(serverLink)
|
val uri = Uri.parse(serverLink)
|
||||||
|
|
||||||
json["subtitles"]!!.jsonArray.forEach {
|
json["subtitles"]!!.jsonArray.forEach {
|
||||||
@ -200,52 +228,46 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
val subUrl = "${uri.scheme}://${uri.host}" + it.jsonObject["src"]!!.jsonPrimitive.content
|
val subUrl = "${uri.scheme}://${uri.host}" + it.jsonObject["src"]!!.jsonPrimitive.content
|
||||||
try {
|
try {
|
||||||
subsList.add(Track(subUrl, subLang))
|
subsList.add(Track(subUrl, subLang))
|
||||||
} catch (e: Error) {}
|
} catch (_: Error) {}
|
||||||
}
|
}
|
||||||
data = mutableListOf(Pair("${uri.scheme}://${uri.host}" + json["hls"]!!.jsonPrimitive.content, headers))
|
vidHeader = embedHeader
|
||||||
|
client.newCall(GET("${uri.scheme}://${uri.host}" + json["hls"]!!.jsonPrimitive.content, embedHeader)).execute()
|
||||||
} else {
|
} else {
|
||||||
val playlistInterceptor = MasterPlaylistInterceptor()
|
val kickAssClient = client.newBuilder().addInterceptor(MasterPlaylistInterceptor()).build()
|
||||||
val kickAssClient = client.newBuilder().addInterceptor(playlistInterceptor).build()
|
|
||||||
kickAssClient.newCall(GET(serverLink, headers)).execute()
|
kickAssClient.newCall(GET(serverLink, headers)).execute()
|
||||||
data = playlistInterceptor.playlist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.forEach { (videoLink, headers) ->
|
resp.body!!.string().substringAfter("#EXT-X-STREAM-INF:")
|
||||||
val masterPlaylist = client.newCall(GET(videoLink, headers)).execute().body!!.string()
|
|
||||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
|
||||||
.split("#EXT-X-STREAM-INF:").map {
|
.split("#EXT-X-STREAM-INF:").map {
|
||||||
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p $server" +
|
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p $server" +
|
||||||
if (subsList.size > 0) { " (Toggleable Sub Available)" } else { "" }
|
if (subsList.size > 0) { " (Toggleable Sub Available)" } else { "" }
|
||||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
if (videoUrl.startsWith("https").not()) {
|
if (videoUrl.startsWith("https").not()) {
|
||||||
val pos = videoLink.lastIndexOf('/') + 1
|
videoUrl = resp.request.url.toString().substringBeforeLast("/") + "/$videoUrl"
|
||||||
videoUrl = videoLink.substring(0, pos) + videoUrl
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
playlist.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subsList, headers = headers))
|
playlist.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subsList, headers = vidHeader))
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
playlist.add(Video(videoUrl, quality, videoUrl, headers = headers))
|
playlist.add(Video(videoUrl, quality, videoUrl, headers = vidHeader))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractBetaVideo(serverLink: String, server: String): List<Video> {
|
private fun extractBetaVideo(serverLink: String, server: String): List<Video> {
|
||||||
val headers = Headers.headersOf("referer", "https://kaast1.com/")
|
val headers = Headers.headersOf("referer", "https://kaast1.com/")
|
||||||
val document = client.newCall(GET(serverLink, headers)).execute().asJsoup()
|
val document = client.newCall(GET(serverLink, headers)).execute().asJsoup()
|
||||||
val scripts = document.getElementsByTag("script")
|
|
||||||
var playlistArray = JsonArray(arrayListOf())
|
var playlistArray = JsonArray(arrayListOf())
|
||||||
for (element in scripts) {
|
|
||||||
if (element.data().contains("window.files")) {
|
document.selectFirst("script:containsData(window.files)")?.data()?.let {
|
||||||
val pattern = Pattern.compile(".*JSON\\.parse\\('(.*)'\\)")
|
val pattern = Pattern.compile(".*JSON\\.parse\\('(.*)'\\)")
|
||||||
val matcher = pattern.matcher(element.data())
|
val matcher = pattern.matcher(it)
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
playlistArray = json.decodeFromString(matcher.group(1)!!.toString())
|
playlistArray = json.decodeFromString(matcher.group(1)!!.toString())
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val playlist = mutableListOf<Video>()
|
val playlist = mutableListOf<Video>()
|
||||||
playlistArray.forEach {
|
playlistArray.forEach {
|
||||||
val quality = it.jsonObject["label"]!!.jsonPrimitive.content + " $server"
|
val quality = it.jsonObject["label"]!!.jsonPrimitive.content + " $server"
|
||||||
@ -258,26 +280,25 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun extractKickasssVideo(serverLink: String, server: String): List<Video> {
|
private fun extractKickasssVideo(serverLink: String, server: String): List<Video> {
|
||||||
val url = serverLink.replace("embed.php", "pref.php")
|
val url = serverLink.replace("(?:embed|player)\\.php".toRegex(), "pref.php")
|
||||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
val scripts = document.getElementsByTag("script")
|
|
||||||
var playlistArray = JsonArray(arrayListOf())
|
var playlistArray = JsonArray(arrayListOf())
|
||||||
for (element in scripts) {
|
|
||||||
if (element.data().contains("document.write")) {
|
document.selectFirst("script:containsData(document.write)")?.data()?.let {
|
||||||
val pattern = Pattern.compile(".*atob\\(\"(.*)\"\\)")
|
val pattern = if (server.contains("Beta", true)) Pattern.compile(".*decode\\(\"(.*)\"\\)")
|
||||||
val matcher = pattern.matcher(element.data())
|
else Pattern.compile(".*atob\\(\"(.*)\"\\)")
|
||||||
|
val matcher = pattern.matcher(it)
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
val player = Base64.decode(matcher.group(1)!!.toString(), Base64.DEFAULT).toString(Charsets.UTF_8)
|
val player = matcher.group(1)!!.toString().decodeBase64()
|
||||||
val playerPattern = Pattern.compile(".*setup\\(\\{sources:\\[(.*)\\]")
|
val playerPattern = Pattern.compile(".*sources:[ ]*\\[(.*)\\]")
|
||||||
val playerMatcher = playerPattern.matcher(player)
|
val playerMatcher = playerPattern.matcher(player)
|
||||||
if (playerMatcher.find()) {
|
if (playerMatcher.find()) {
|
||||||
val playlistString = "[" + playerMatcher.group(1)!!.toString() + "]"
|
val playlistString = "[" + playerMatcher.group(1)!!.toString() + "]"
|
||||||
playlistArray = json.decodeFromString(playlistString)
|
playlistArray = json.decodeFromString(playlistString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val playlist = mutableListOf<Video>()
|
val playlist = mutableListOf<Video>()
|
||||||
playlistArray.forEach {
|
playlistArray.forEach {
|
||||||
val quality = it.jsonObject["label"]!!.jsonPrimitive.content + " $server"
|
val quality = it.jsonObject["label"]!!.jsonPrimitive.content + " $server"
|
||||||
@ -289,6 +310,77 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
return playlist
|
return playlist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractDailymotion(serverLink: String, server: String): List<Video> {
|
||||||
|
val url = serverLink.replace("player.php", "pref.php")
|
||||||
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
var masterPlaylist = listOf<Video>()
|
||||||
|
|
||||||
|
document.selectFirst("script:containsData(Base64.decode)")?.data()?.let { iframe ->
|
||||||
|
val embedUrl = iframe.substringAfter("decode(\"").substringBefore("\"").decodeBase64()
|
||||||
|
.substringAfter("src=\"").substringBefore("\"").substringBefore("?")
|
||||||
|
.replace("/embed/", "/player/metadata/")
|
||||||
|
val response = client.newCall(GET(embedUrl, headers)).execute()
|
||||||
|
val decodedJson = json.decodeFromString<DailyQuality>(response.body!!.string())
|
||||||
|
masterPlaylist = decodedJson.qualities.auto.parallelMap { item ->
|
||||||
|
runCatching {
|
||||||
|
val resp = client.newCall(GET(item.url)).execute().body!!.string()
|
||||||
|
resp.substringAfter("#EXT-X-STREAM-INF:")
|
||||||
|
.split("#EXT-X-STREAM-INF:").map {
|
||||||
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
val proxy = videoUrl.substringAfter("proxy-").substringBefore(".")
|
||||||
|
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p $server" +
|
||||||
|
if (proxy.isNotBlank()) " $proxy" else ""
|
||||||
|
Video(videoUrl, quality, videoUrl, headers = Headers.headersOf("referer", "https://www.dailymotion.com/"))
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}.filterNotNull().flatten().distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterPlaylist
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.decodeBase64(): String {
|
||||||
|
return Base64.decode(this, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractSapphireVideo(serverLink: String, server: String): List<Video> {
|
||||||
|
val url = serverLink.toHttpUrl().newBuilder().addQueryParameter("action", "config").build()
|
||||||
|
val response = client.newCall(GET(url.toString(), Headers.headersOf("referer", serverLink))).execute()
|
||||||
|
val rawJson = response.body!!.string().let {
|
||||||
|
var decoded = it
|
||||||
|
while (!decoded.startsWith("{\"id")) decoded = decoded.decodeBase64()
|
||||||
|
return@let decoded
|
||||||
|
}
|
||||||
|
val decodedJson = json.decodeFromString<Sapphire>(rawJson)
|
||||||
|
val subsList = decodedJson.subtitles.mapNotNull {
|
||||||
|
try {
|
||||||
|
Track(it.url, it.language.getLocale())
|
||||||
|
} catch (_: Error) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodedJson.streams.filter { it.format == "adaptive_hls" }.parallelMap { stream ->
|
||||||
|
runCatching {
|
||||||
|
val playlist = client.newCall(GET(stream.url)).execute().body!!.string()
|
||||||
|
playlist.substringAfter("#EXT-X-STREAM-INF:")
|
||||||
|
.split("#EXT-X-STREAM-INF:").map {
|
||||||
|
val quality = it.substringAfter("RESOLUTION=").split(",")[0].split("\n")[0].substringAfter("x") + "p $server" +
|
||||||
|
(if (stream.audio.getLocale().isNotBlank()) " - Aud: ${stream.audio.getLocale()}" else "") +
|
||||||
|
(if (stream.hardSub.getLocale().isNotBlank()) " - HardSub: ${stream.hardSub}" else "")
|
||||||
|
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||||
|
try {
|
||||||
|
Video(videoUrl, quality, videoUrl, subtitleTracks = subsList)
|
||||||
|
} catch (e: Error) {
|
||||||
|
Video(videoUrl, quality, videoUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractGogoVideo(link: String): List<Video> {
|
private fun extractGogoVideo(link: String): List<Video> {
|
||||||
var url = decode(link).substringAfter("data=").substringBefore("&vref")
|
var url = decode(link).substringAfter("data=").substringBefore("&vref")
|
||||||
if (url.startsWith("https").not()) {
|
if (url.startsWith("https").not()) {
|
||||||
@ -298,7 +390,7 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||||
|
|
||||||
// Vidstreaming:
|
// Vidstreaming:
|
||||||
videoList.addAll(GogoCdnExtractor(network.client, json).videosFromUrl(url))
|
videoList.addAll(GogoCdnExtractor(client, json).videosFromUrl(url))
|
||||||
// Doodstream mirror:
|
// Doodstream mirror:
|
||||||
document.select("div#list-server-more > ul > li.linkserver:contains(Doodstream)")
|
document.select("div#list-server-more > ul > li.linkserver:contains(Doodstream)")
|
||||||
.firstOrNull()?.attr("data-video")
|
.firstOrNull()?.attr("data-video")
|
||||||
@ -311,41 +403,15 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString("preferred_quality", "1080")
|
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||||
val server = preferences.getString("preferred_server", "MAVERICKKI")
|
val server = preferences.getString("preferred_server", "MAVERICKKI")!!
|
||||||
|
|
||||||
if (quality != null || server != null) {
|
return this.sortedWith(
|
||||||
val qualityList = mutableListOf<Video>()
|
compareBy(
|
||||||
if (quality != null) {
|
{ it.quality.contains(quality) },
|
||||||
var preferred = 0
|
{ it.quality.contains(server) }
|
||||||
for (video in this) {
|
)
|
||||||
if (video.quality.contains(quality)) {
|
).reversed()
|
||||||
qualityList.add(preferred, video)
|
|
||||||
preferred++
|
|
||||||
} else {
|
|
||||||
qualityList.add(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qualityList.addAll(this)
|
|
||||||
}
|
|
||||||
val newList = mutableListOf<Video>()
|
|
||||||
if (server != null) {
|
|
||||||
var preferred = 0
|
|
||||||
for (video in qualityList) {
|
|
||||||
if (video.quality.contains(server)) {
|
|
||||||
newList.add(preferred, video)
|
|
||||||
preferred++
|
|
||||||
} else {
|
|
||||||
newList.add(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newList.addAll(qualityList)
|
|
||||||
}
|
|
||||||
return newList
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
@ -358,7 +424,8 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
val animes = animeList.map { item ->
|
val animes = animeList.map { item ->
|
||||||
SAnime.create().apply {
|
SAnime.create().apply {
|
||||||
setUrlWithoutDomain(item.jsonObject["slug"]!!.jsonPrimitive.content)
|
setUrlWithoutDomain(item.jsonObject["slug"]!!.jsonPrimitive.content)
|
||||||
thumbnail_url = "$baseUrl/uploads/" + item.jsonObject["poster"]!!.jsonPrimitive.content
|
thumbnail_url =
|
||||||
|
"$baseUrl/uploads/" + item.jsonObject["poster"]!!.jsonPrimitive.content
|
||||||
title = item.jsonObject["name"]!!.jsonPrimitive.content
|
title = item.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,12 +438,14 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
if (appData.isEmpty().not()) {
|
if (appData.isEmpty().not()) {
|
||||||
val ani = appData["anime"]!!.jsonObject
|
val ani = appData["anime"]!!.jsonObject
|
||||||
anime.title = ani["name"]!!.jsonPrimitive.content
|
anime.title = ani["name"]!!.jsonPrimitive.content
|
||||||
anime.genre = ani["genres"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content }
|
anime.genre =
|
||||||
|
ani["genres"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content }
|
||||||
anime.description = JSONUtil.unescape(ani["description"]!!.jsonPrimitive.content)
|
anime.description = JSONUtil.unescape(ani["description"]!!.jsonPrimitive.content)
|
||||||
anime.status = parseStatus(ani["status"]!!.jsonPrimitive.content)
|
anime.status = parseStatus(ani["status"]!!.jsonPrimitive.content)
|
||||||
|
|
||||||
val altName = "Other name(s): "
|
val altName = "Other name(s): "
|
||||||
json.decodeFromString<JsonArray>(ani["alternate"].toString().replace("\"\"", "[]")).let { altArray ->
|
json.decodeFromString<JsonArray>(ani["alternate"].toString().replace("\"\"", "[]"))
|
||||||
|
.let { altArray ->
|
||||||
if (altArray.isEmpty().not()) {
|
if (altArray.isEmpty().not()) {
|
||||||
anime.description = when {
|
anime.description = when {
|
||||||
anime.description.isNullOrBlank() -> altName + altArray.joinToString { it.jsonPrimitive.content }
|
anime.description.isNullOrBlank() -> altName + altArray.joinToString { it.jsonPrimitive.content }
|
||||||
@ -482,4 +551,72 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
|||||||
private fun encode(input: String): String = java.net.URLEncoder.encode(input, "utf-8")
|
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")
|
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
|
||||||
|
|
||||||
|
private fun String.getLocale(): String {
|
||||||
|
return arrayOf(
|
||||||
|
Pair("ar-ME", "Arabic"),
|
||||||
|
Pair("ar-SA", "Arabic (Saudi Arabia)"),
|
||||||
|
Pair("de-DE", "German"),
|
||||||
|
Pair("en-US", "English"),
|
||||||
|
Pair("es-419", "Spanish"),
|
||||||
|
Pair("es-ES", "Spanish (Spain)"),
|
||||||
|
Pair("es-LA", "Spanish (Spanish)"),
|
||||||
|
Pair("fr-FR", "French"),
|
||||||
|
Pair("ja-JP", "Japanese"),
|
||||||
|
Pair("it-IT", "Italian"),
|
||||||
|
Pair("pt-BR", "Portuguese (Brazil)"),
|
||||||
|
Pair("pl-PL", "Polish"),
|
||||||
|
Pair("ru-RU", "Russian"),
|
||||||
|
Pair("tr-TR", "Turkish"),
|
||||||
|
Pair("uk-UK", "Ukrainian"),
|
||||||
|
Pair("he-IL", "Hebrew"),
|
||||||
|
Pair("ro-RO", "Romanian"),
|
||||||
|
Pair("sv-SE", "Swedish")
|
||||||
|
).firstOrNull { it.first == this }?.second ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DailyQuality(
|
||||||
|
val qualities: Auto
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Auto(
|
||||||
|
val auto: List<Item>
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Item(
|
||||||
|
val type: String,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Sapphire(
|
||||||
|
val subtitles: List<Subtitle>,
|
||||||
|
val streams: List<Stream>
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Subtitle(
|
||||||
|
val language: String,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Stream(
|
||||||
|
@SerialName("audio_lang")
|
||||||
|
val audio: String,
|
||||||
|
@SerialName("hardsub_lang")
|
||||||
|
val hardSub: String,
|
||||||
|
val url: String,
|
||||||
|
val format: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Dopebox
|
||||||
|
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||||
|
runBlocking {
|
||||||
|
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import android.webkit.WebResourceRequest
|
|||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import okhttp3.Headers
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import okhttp3.Headers.Companion.toHeaders
|
import okhttp3.Headers.Companion.toHeaders
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -22,17 +22,17 @@ class MasterPlaylistInterceptor : Interceptor {
|
|||||||
|
|
||||||
private val context = Injekt.get<Application>()
|
private val context = Injekt.get<Application>()
|
||||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
val playlist = mutableListOf<Pair<String, Headers>>()
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
val newRequest = resolveWithWebView(originalRequest)
|
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Could not find playlist")
|
||||||
|
|
||||||
return chain.proceed(newRequest)
|
return chain.proceed(newRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun resolveWithWebView(request: Request): Request {
|
private fun resolveWithWebView(request: Request): Request? {
|
||||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||||
// OkHttp doesn't support asynchronous interceptors.
|
// OkHttp doesn't support asynchronous interceptors.
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
@ -42,6 +42,8 @@ class MasterPlaylistInterceptor : Interceptor {
|
|||||||
val origRequestUrl = request.url.toString()
|
val origRequestUrl = request.url.toString()
|
||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
|
|
||||||
|
var newRequest: Request? = null
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
webView = webview
|
webView = webview
|
||||||
@ -51,8 +53,7 @@ class MasterPlaylistInterceptor : Interceptor {
|
|||||||
databaseEnabled = true
|
databaseEnabled = true
|
||||||
useWideViewPort = false
|
useWideViewPort = false
|
||||||
loadWithOverviewMode = false
|
loadWithOverviewMode = false
|
||||||
userAgentString = request.header("User-Agent")
|
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0"
|
||||||
?: "\"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() {
|
webview.webViewClient = object : WebViewClient() {
|
||||||
@ -61,7 +62,7 @@ class MasterPlaylistInterceptor : Interceptor {
|
|||||||
request: WebResourceRequest,
|
request: WebResourceRequest,
|
||||||
): WebResourceResponse? {
|
): WebResourceResponse? {
|
||||||
if (request.url.toString().contains(".m3u8")) {
|
if (request.url.toString().contains(".m3u8")) {
|
||||||
playlist.add(Pair(request.url.toString(), request.requestHeaders.toHeaders()))
|
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
return super.shouldInterceptRequest(view, request)
|
return super.shouldInterceptRequest(view, request)
|
||||||
@ -81,6 +82,6 @@ class MasterPlaylistInterceptor : Interceptor {
|
|||||||
webView = null
|
webView = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
return newRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user