KickAssAnime: Fix Sapphire, use reanimation jutsu on some dead server… (#1194)

This commit is contained in:
Samfun75
2023-01-20 19:50:55 +03:00
committed by GitHub
parent 72f191c705
commit ea8ae3909e
3 changed files with 279 additions and 139 deletions

View File

@ -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'))
} }

View File

@ -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()
}
} }

View File

@ -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
} }
} }