New source: tr.turkanime
This commit is contained in:
parent
7804c2f258
commit
fe53f7a8c1
@ -20,7 +20,7 @@ import javax.crypto.spec.SecretKeySpec
|
||||
/**
|
||||
* Conforming with CryptoJS AES method
|
||||
*/
|
||||
@Suppress("unused", "FunctionName")
|
||||
@Suppress("unused")
|
||||
object CryptoAES {
|
||||
|
||||
private const val KEY_SIZE = 256
|
||||
@ -39,19 +39,41 @@ object CryptoAES {
|
||||
* @param password passphrase
|
||||
*/
|
||||
fun decrypt(cipherText: String, password: String): String {
|
||||
try {
|
||||
return try {
|
||||
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
|
||||
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
|
||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||
val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5)
|
||||
return decryptAES(
|
||||
decryptAES(
|
||||
cipherTextBytes,
|
||||
keyAndIV?.get(0) ?: ByteArray(32),
|
||||
keyAndIV?.get(1) ?: ByteArray(16),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return ""
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptWithSalt(cipherText: String, salt: String, password: String): String {
|
||||
return try {
|
||||
val ctBytes = Base64.decode(cipherText, Base64.DEFAULT)
|
||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||
val keyAndIV = generateKeyAndIV(
|
||||
32,
|
||||
16,
|
||||
1,
|
||||
salt.decodeHex(),
|
||||
password.toByteArray(Charsets.UTF_8),
|
||||
md5
|
||||
)
|
||||
decryptAES(
|
||||
ctBytes,
|
||||
keyAndIV?.get(0) ?: ByteArray(32),
|
||||
keyAndIV?.get(1) ?: ByteArray(16),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
19
lib/synchrony/build.gradle.kts
Normal file
19
lib/synchrony/build.gradle.kts
Normal file
@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
namespace = "eu.kanade.tachiyomi.lib.synchrony"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.kotlin.stdlib)
|
||||
compileOnly(libs.quickjs)
|
||||
}
|
289
lib/synchrony/src/main/assets/synchrony-v2.4.2.1.js
Normal file
289
lib/synchrony/src/main/assets/synchrony-v2.4.2.1.js
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.lib.synchrony
|
||||
|
||||
import app.cash.quickjs.QuickJs
|
||||
|
||||
/**
|
||||
* Helper class to deobfuscate JavaScript strings with synchrony.
|
||||
*/
|
||||
object Deobfuscator {
|
||||
fun deobfuscateScript(source: String): String? {
|
||||
val engine = QuickJs.create()
|
||||
val originalScript = javaClass.getResource("/assets/$SCRIPT_NAME")
|
||||
?.readText() ?: return null
|
||||
|
||||
// Sadly needed until QuickJS properly supports module imports:
|
||||
// Regex for finding one and two in "export{one as Deobfuscator,two as Transformer};"
|
||||
val regex = """export\{(.*) as Deobfuscator,(.*) as Transformer\};""".toRegex()
|
||||
val synchronyScript = regex.find(originalScript)!!.let { match ->
|
||||
val (deob, trans) = match.destructured
|
||||
val replacement = "const Deobfuscator = $deob, Transformer = $trans;"
|
||||
originalScript.replace(match.value, replacement)
|
||||
}
|
||||
engine.evaluate("globalThis.console = { log: () => {}, warn: () => {}, error: () => {}, trace: () => {} };")
|
||||
engine.evaluate(synchronyScript)
|
||||
|
||||
engine.set("source", TestInterface::class.java, object: TestInterface { override fun getValue() = source })
|
||||
val result = engine.evaluate("new Deobfuscator().deobfuscateSource(source.getValue())") as? String
|
||||
engine.close()
|
||||
return result
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private interface TestInterface {
|
||||
fun getValue(): String
|
||||
}
|
||||
}
|
||||
|
||||
// Update this when the script is updated!
|
||||
private const val SCRIPT_NAME = "synchrony-v2.4.2.1.js"
|
4
src/tr/turkanime/AndroidManifest.xml
Normal file
4
src/tr/turkanime/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.animeextension">
|
||||
</manifest>
|
22
src/tr/turkanime/build.gradle
Normal file
22
src/tr/turkanime/build.gradle
Normal file
@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib-cryptoaes"))
|
||||
implementation(project(":lib-synchrony"))
|
||||
implementation(project(":lib-voe-extractor"))
|
||||
implementation(project(":lib-streamsb-extractor"))
|
||||
}
|
||||
|
||||
ext {
|
||||
extName = 'Türk Anime TV'
|
||||
pkgNameSuffix = 'tr.turkanime'
|
||||
extClass = '.TurkAnime'
|
||||
extVersionCode = 1
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/tr/turkanime/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
BIN
src/tr/turkanime/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
src/tr/turkanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/tr/turkanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
src/tr/turkanime/res/web_hi_res_512.png
Normal file
BIN
src/tr/turkanime/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
@ -0,0 +1,65 @@
|
||||
package eu.kanade.tachiyomi.animeextension.tr.turkanime
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.OkHttpClient
|
||||
|
||||
class AlucardExtractor(private val client: OkHttpClient, private val json: Json, private val baseUrl: String) {
|
||||
private val refererHeader = Headers.headersOf("referer", baseUrl)
|
||||
|
||||
fun extractVideos(hosterLink: String, subber: String): List<Video> {
|
||||
return try {
|
||||
val sourcesId = hosterLink.substringBeforeLast("/true").substringAfterLast("/")
|
||||
val playerJs = client.newCall(GET("$baseUrl/js/player.js"))
|
||||
.execute().body.string()
|
||||
val csrf = "(?<=')[a-zA-Z]{64}(?=')".toRegex().find(playerJs)!!.value
|
||||
val sourcesResponse = client.newCall(
|
||||
GET(
|
||||
"$baseUrl/sources/$sourcesId/true",
|
||||
Headers.headersOf(
|
||||
"Referer",
|
||||
hosterLink,
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"Cookie",
|
||||
"__",
|
||||
"csrf-token",
|
||||
csrf,
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute().body.string()
|
||||
|
||||
val sources = json.decodeFromString<JsonObject>(sourcesResponse)["response"]!!
|
||||
.jsonObject["sources"]!!
|
||||
.jsonArray.first()
|
||||
.jsonObject["file"]!!
|
||||
.jsonPrimitive.content
|
||||
|
||||
val masterPlaylist = client.newCall(GET(sources, refererHeader))
|
||||
.execute().body.string()
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
masterPlaylist.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter("RESOLUTION=")
|
||||
.substringAfter("x")
|
||||
.substringBefore("\n") + "p"
|
||||
val videoUrl = it.substringAfter("\n")
|
||||
.substringBefore("\n")
|
||||
// TODO: This gives 403 in MPV
|
||||
Video(videoUrl, "$subber: Alucard: $quality", videoUrl, refererHeader)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
package eu.kanade.tachiyomi.animeextension.tr.turkanime
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
|
||||
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class TurkAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Türk Anime TV"
|
||||
|
||||
override val baseUrl = "https://www.turkanime.co"
|
||||
|
||||
override val lang = "tr"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val key: String
|
||||
get() = preferences.getString(PREF_KEY_KEY, DEFAULT_KEY)!!
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/ajax/rankagore?sayfa=$page", xmlHeader)
|
||||
|
||||
override fun popularAnimeSelector() = "div.panel-visible"
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "button.btn-default[data-loading-text*=Sonraki]"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val animeTitle = element.select("div.panel-title > a").first()!!
|
||||
val name = animeTitle.attr("title")
|
||||
.substringBefore(" izle")
|
||||
val img = element.select("img.media-object")
|
||||
val animeId = element.select("a.reactions").first()!!.attr("data-unique-id")
|
||||
val animeUrl = ("https:" + animeTitle.attr("href")).toHttpUrl()
|
||||
.newBuilder()
|
||||
.addQueryParameter("animeId", animeId)
|
||||
.build().toString()
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(animeUrl)
|
||||
title = name
|
||||
thumbnail_url = "https:" + img.attr("data-src")
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val img = document.select("div.imaj > img.media-object").ifEmpty { null }
|
||||
val studio = document.select("div#animedetay > table tr:contains(Stüdyo) > td:last-child a").ifEmpty { null }
|
||||
val desc = document.select("div#animedetay p.ozet").ifEmpty { null }
|
||||
val genres = document.select("div#animedetay > table tr:contains(Anime Türü) > td:last-child a")
|
||||
.ifEmpty { null }
|
||||
return SAnime.create().apply {
|
||||
title = document.select("div#detayPaylas div.panel-title").text()
|
||||
thumbnail_url = img?.let { "https:" + it.attr("data-src") }
|
||||
author = studio?.text()
|
||||
description = desc?.text()
|
||||
genre = genres?.joinToString { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val animeId = (baseUrl + anime.url).toHttpUrl().queryParameter("animeId")!!
|
||||
return GET("https://www.turkanime.co/ajax/bolumler?animeId=$animeId", xmlHeader)
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "ul.menum li"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val a = element.select("a:has(span.bolumAdi)")
|
||||
val title = a.attr("title")
|
||||
val substring = title.substringBefore(". Bölüm")
|
||||
val numIdx = substring.indexOfLast { !it.isDigit() } + 1
|
||||
val numbers = substring.slice(numIdx..substring.lastIndex)
|
||||
return SEpisode.create().apply {
|
||||
setUrlWithoutDomain("https:" + a.attr("href"))
|
||||
name = title
|
||||
episode_number = numbers.toFloatOrNull() ?: 1F
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
return super.episodeListParse(response).reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val fansubbers = document.select("div#videodetay div.pull-right button")
|
||||
return if (fansubbers.size == 1) {
|
||||
getVideosFromHosters(document, fansubbers.first()!!.text().trim())
|
||||
} else {
|
||||
val videoList = mutableListOf<Video>()
|
||||
fansubbers.parallelMap {
|
||||
val url = it.attr("onclick").trimOnClick()
|
||||
val subDoc = client.newCall(GET(url, xmlHeader)).execute().asJsoup()
|
||||
videoList.addAll(getVideosFromHosters(subDoc, it.text().trim()))
|
||||
}
|
||||
videoList
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVideosFromHosters(document: Document, subber: String): List<Video> {
|
||||
val selectedHoster = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-danger")
|
||||
val hosters = document.select("div#videodetay div.btn-group:not(.pull-right) > button.btn-default[onclick*=videosec]")
|
||||
|
||||
val videoList = mutableListOf<Video>()
|
||||
val selectedHosterName = selectedHoster.text().trim()
|
||||
if (selectedHosterName in SUPPORTED_HOSTERS) {
|
||||
val src = document.select("iframe").attr("src")
|
||||
videoList.addAll(getVideosFromSource(src, selectedHosterName, subber))
|
||||
}
|
||||
hosters.parallelMap {
|
||||
val hosterName = it.text().trim()
|
||||
if (hosterName !in SUPPORTED_HOSTERS) return@parallelMap
|
||||
val url = it.attr("onclick").trimOnClick()
|
||||
val videoDoc = client.newCall(GET(url, xmlHeader)).execute().asJsoup()
|
||||
val src = videoDoc.select("iframe").attr("src")
|
||||
videoList.addAll(getVideosFromSource(src, hosterName, subber))
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun getVideosFromSource(src: String, hosterName: String, subber: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val cipherParamsEncoded = src
|
||||
.substringAfter("/embed/#/url/")
|
||||
.substringBefore("?status")
|
||||
|
||||
val cipherParams = json.decodeFromString<CipherParams>(
|
||||
String(
|
||||
Base64.decode(cipherParamsEncoded, Base64.DEFAULT),
|
||||
),
|
||||
)
|
||||
|
||||
val hosterLink = "https:" + json.decodeFromString<JsonPrimitive>(
|
||||
CryptoAES.decryptWithSalt(
|
||||
cipherParams.ct,
|
||||
cipherParams.s,
|
||||
key,
|
||||
),
|
||||
).content
|
||||
|
||||
when (hosterName) {
|
||||
"STREAMSB" -> {
|
||||
videoList.addAll(StreamSBExtractor(client).videosFromUrl(hosterLink, refererHeader, "$subber:"))
|
||||
}
|
||||
"VOE" -> {
|
||||
VoeExtractor(client).videoFromUrl(hosterLink, "$subber: VOE")?.let { video -> videoList.add(video) }
|
||||
}
|
||||
"ALUCARD(BETA)" -> {
|
||||
videoList.addAll(AlucardExtractor(client, json, baseUrl).extractVideos(hosterLink, subber))
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class CipherParams(
|
||||
@Serializable
|
||||
val ct: String,
|
||||
@Serializable
|
||||
val iv: String,
|
||||
@Serializable
|
||||
val s: String,
|
||||
)
|
||||
|
||||
private fun String.trimOnClick() = baseUrl + "/" + this.substringAfter("IndexIcerik('").substringBefore("'")
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw Exception("not used")
|
||||
override fun videoListSelector(): String = throw Exception("not used")
|
||||
override fun videoUrlParse(document: Document): String = throw Exception("not used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
|
||||
POST(
|
||||
"$baseUrl/arama?sayfa=$page",
|
||||
Headers.headersOf("content-type", "application/x-www-form-urlencoded"),
|
||||
FormBody.Builder().add("arama", query).build(),
|
||||
)
|
||||
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/ajax/yenieklenenseriler?sayfa=$page", xmlHeader)
|
||||
|
||||
override fun latestUpdatesSelector() = popularAnimeSelector()
|
||||
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
// =============================== Preferences ===============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue("1080")
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private val xmlHeader = Headers.headersOf("X-Requested-With", "XMLHttpRequest")
|
||||
private val refererHeader = Headers.headersOf("Referer", baseUrl)
|
||||
|
||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||
runBlocking(Dispatchers.Default) {
|
||||
map { async { f(it) } }.awaitAll()
|
||||
}
|
||||
|
||||
private fun getKey() {
|
||||
val script4 = client.newCall(GET("$baseUrl/embed/#/")).execute().asJsoup()
|
||||
.select("script[defer]").getOrNull(1)
|
||||
?.attr("src") ?: return
|
||||
val embeds4 = client.newCall(GET(baseUrl + script4)).execute().body.string()
|
||||
val name = "(?<=')[0-9a-f]{16}(?=')".toRegex().findAll(embeds4).toList().firstOrNull()?.value
|
||||
|
||||
val file5 = client.newCall(GET("$baseUrl/embed/js/embeds.$name.js")).execute().body.string()
|
||||
val embeds5 = Deobfuscator.deobfuscateScript(file5) ?: return
|
||||
val key = "(?<=')\\S{100}(?=')".toRegex().find(embeds5)?.value ?: return
|
||||
preferences.edit().putString(PREF_KEY_KEY, key).apply()
|
||||
}
|
||||
|
||||
init {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.IO) { getKey() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SUPPORTED_HOSTERS = listOf(
|
||||
// TODO: Fix Alucard
|
||||
// "ALUCARD(BETA)",
|
||||
"STREAMSB",
|
||||
"VOE")
|
||||
|
||||
private const val PREF_KEY_KEY = "key"
|
||||
private const val DEFAULT_KEY = "710^8A@3@>T2}#zN5xK?kR7KNKb@-A!LzYL5~M1qU0UfdWsZoBm4UUat%}ueUv6E--*hDPPbH7K2bp9^3o41hw,khL:}Kx8080@M"
|
Loading…
x
Reference in New Issue
Block a user